#二叉樹(binary)
二叉樹就是節點的度不大於2的樹,即樹中每個節點的子節點最多隻有兩個。每個節點的子節點分爲左子節點和右子節點,並且左右子節點的順序不能改變。
1. 二叉樹分類
二叉樹分爲滿二叉樹、完全二叉樹和完美二叉樹。
1.1滿二叉樹(full binary tree)
滿二叉樹是除了葉子節點外,每個節點都有兩個子節點的樹。
如下圖所示的兩棵樹,左邊的樹是滿二叉樹,因爲除了葉子節點以外,每個節點都有兩個子節點。但是右邊的不是滿二叉樹,因爲除了葉子節點以外,第二層的第一個節點只有一個子節點。
1.2 完全二叉樹(complete binary tree)
完全二叉樹除了最後一層節點以外,其餘的節點形成一個滿二叉樹,並且最後一層節點從左往右依次填滿,沒有空出來的節點。
1.3 完美二叉樹(perfect binary tree)
完美二叉樹的葉子節點在同一層,並且所有的內部節點都有兩個子節點。
使用下面的圖更好的區分這幾個概念
2. 二叉樹的存儲
同堆棧一樣,二叉樹的實現也可以採用數組和鏈表形式實現。
2.1 數組形式
二叉樹的數組存儲形式其實就是層級遍歷的結果:從一層逐層往下遍歷,每層從左往右遍歷。假設一個節點在數組中的下標爲i,則其左子節點的下標爲2*i, 右子節點的下標爲2*i+1,其父節點的下標爲\lfloor\frac{i}{2}} \rfloor N
假設有如下二叉樹,則其數組存儲結果爲:
2.2 鏈式形式
二叉樹的鏈式存儲結構中,將每個節點封裝成一個結構體,存儲節點的值以及兩個分別指向左右子節點的指針。葉子節點的左右子節點指針都爲空指針。
typedef struct node
{
int val;
struct node *left;
struct node *right;
}Node;
3. 二叉樹查找樹及其基本操作
下面使用鏈式存儲形式說明二叉樹的相關操作。
通常我們建立一棵樹,自然是希望這棵樹能夠表示一定的意義方便找到需要的元素或進行相關操作,所以建立的二叉樹都不是無意義的。現在建立一棵樹使得每個節點左邊節點的值都小於它,右邊的節點的值都大於它。(有時候可能需要記錄每個數據元素,爲了保留相同的元素有兩種方法:1. 約定每個節點的左邊節點的值小於等於這個節點的值,右邊節點的值都大於這個節點的值。這種方法十分直觀,但是會造成空間浪費; 2. 約定每個節點左邊節點的值都小於它,右邊的節點的值都大於它,使用一個計數器記錄有多少個與當前節點值相同的元素。這裏只考慮最簡單的情況,即沒有重複的元素)這樣得到的二叉樹叫做二叉查找樹(Binary Search Tree,也叫作二叉搜索樹)。
3.1 查找
查找的過程如下:
1. 如果二叉樹爲空樹,查找失敗,返回NULL
2. 如果key等於根節點的值,則查找成功,返回root
3. 如果key小於根節點的值,遞歸查找左子樹
4. 如果key大於根節點的值,遞歸查找右子樹
//recursive find a target node
Node* Find (Node* root, int key)
{
//skip loop if empty tree(root = NULL), or not find key(root = NULL), or find key (root->val = key)
if (root == NULL || key == root->val) return root;
if (key < root->left)
return Find (root->left);
else
return Find (root->right);
}
//*********************************************//
//non-recursive find a target node
Node* Find (Node* root, int key)
{
Node *temp = root;
//skip loop if empty tree(root = NULL), or not find key(temp = NULL), or find key (temp->val = key)
while(temp != NULL && temp->val != key)
{
if (key < temp->val)
temp = temp->left;
else
temp = temp->right;
}
return temp;
}
3.2 查找最大值&最小值
由於二叉查找樹中,每個節點的左節點都小於它,所以最小值一定是樹中最左邊的節點;同樣的,最大值一定是樹中最右邊的節點。
//find the maximum element
Node* FindMin(Node* root)
{
if (root == NULL) return NULL;
Node* temp = root;
while (temp->left != NULL)
temp = temp->left;
return temp;
}
//find the minimum element
Node *FindMax(Node *root)
{
if (root == NULL) return NULL;
Node* temp = root;
while (temp->right != NULL)
temp = temp->right;
return temp;
}
3.3 查找Floor和Ceiling
Floor即查找比key小的最大值,Ceiling即查找比key大的最小值。
先考慮查找Floor(比key小的最大值),過程如下:
1. 將key與root的值比較
2. 如果key比root的值小,那麼目標節點肯定在root的左子樹上
3. 如果key比root的值大,那麼目標節點有是root,也有可能是在root的右子樹上的某個節點。
4. 接着第3步,繼續查找root的右子樹。如果沒有找到目標節點,則說明root右子樹上的所有節點都大於key,因此目標節點就是root;否則就在右子樹上找到了目標節點。
//find the maximum element that smaller than key
Node* Floor(Node* root, int key)
{
if (root == NULL) return NULL;
if (key < root->val)
return Floor(root->left, key);
else
{
Node* temp = Floor(root->right, key);
if (temp == NULL) return root;
else return temp;
}
}
Ceiling的過程其實跟Floor的一樣,代碼實現如下:
//find the minimum element that bigger than key
Node* Ceiling(Node* root, int key)
{
if (root == NULL) return NULL;
if (key > root->val)
return Floor(root->right, key);
else
{
Node* temp = Floor(root->left, key);
if (temp == NULL) return root;
else return temp;
}
}
3.4 插入
插入的過程與查找類似:
1. 首先查找新節點需要插入的位置
2. 插入新節點
Node* CreateNode(int val)
{
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("space error!\n");
return NULL;
}
newNode->val = val;
newNode->left = newNode->right = NULL;
return newNode;
}
// recursive insert new value
void Insert (Node *root, int val) {
if (root == NULL) root = CreateNode(val);
if (val < root->val)
root->left = Insert (root->left, val);
else
root->right = Insert (root->right, val);
}
//non-recursive Insert new value
void Insert (Node *root, int val)
{
if (root == NULL) root = CreateNode(val);
//find target location
Node *pre = NULL, *cur = root;
while (cur != NULL) {
pre = cur;
if (val < cur->val) cur = cur->left;
if (val > cur->val) cur = cur->right;
}
//Insert value
if (val < pre->val) pre->left = CreateNode(val);
else pre->right = CreateNode(val);
}
3.5 刪除
刪除操作比較複雜,需要考慮多種情況
- 如果要刪除的是葉子節點,則可以直接刪除
如果要刪除的節點有一個子節點,則將其父節點指向被刪除節點的子節點即可。
如下圖所示,加入要刪除節點(4),則將節點(2)的右指針指向節點(4)的左子樹。
如果要刪除的節點有兩個子節點,需要進行一下步驟
1> 由二叉搜索樹的特點可知,每個節點(T)的左子樹中最右邊的節點(R)是左子樹中最大的節點,R比左子樹中所有節點大,同時比T的右子樹中所有節點的值都小
2>可以將R中的值替換爲T中的值,這並沒有破壞二叉樹搜索樹的性質
3> 刪除掉R節點(此時R中的值爲原來T的值)
4> 當然也可以使用右子樹中最左邊的節點替換被刪除的節點
void Delete(Node *root, int key)
{
if (root == NULL) return; //not find key
if (key < root->val)
return Delete(root->left, key);
else if (key > root>val)
return Delete(root->right, key);
else {
Node *toDeleteNode = root;
if (toDeleteNode->left == NULL){ //no left child or no children
root = root->right;
free(toDeleteNode);
}
else if (toDeleteNode->right == NULL) { //no right child
root = root->left;
free(toDeleteNode);
}
else //has two children
{
//find the min element in right subtree
Node *toDeleteNode = root->right;
while (toDeleteNode->left != NULL) toDeleteNode = toDeleteNode->left;
root->val = toDeleteNode->val;
Delete(toDeleteNode, toDeleteNode->val);
}
}
}
複雜度分析
二叉查找樹的時間複雜度與樹的形狀有關,最好情況下二叉查找樹是完全平衡的,從各節點到葉子節點的深度爲O(log(N)),最壞情況下二叉樹形成一條鏈,導致複雜度增加到O(N)。爲了解決最壞情況,可以使用平衡二叉樹、紅黑樹等存儲結構。