本文主講二叉樹系列
樹的概念
鏈表通常可以提供比數組更大的靈活性,但由于鏈表是線性結構,所以很難使用它們來組織對象的分層表示。雖然隊列反映了某些層次,但它們是一維的,為了避免這種限制,創建了一個新型數據類型,稱為樹,樹由節點和弧組成。
樹在計算機科學里,是一種十分基礎的數據結構。幾乎所有操作系統都將文件存放在樹狀結構里;幾乎所有的編譯器都需要實現一個表達式樹;文件壓縮所用的哈夫曼算法需要用到哈夫曼樹;數據庫所使用的B樹,以及map,set等容器底層數據結構紅黑樹。
圖 1(A) 是使用樹結構存儲的集合 {A,B,C,D,E,F,G,H,I,J,K,L,M} 的示意圖。對于數據 A 來說,和數據 B、C、D 有關系;對于數據 B 來說,和 E、F 有關系。這就是“一對多”的關系。
將具有“一對多”關系的集合中的數據元素按照圖 1(A)的形式進行存儲,整個存儲形狀在邏輯結構上看,類似于實際生活中倒著的樹(圖 1(B)倒過來),所以稱這種存儲結構為“樹型”存儲結構。
什么是二叉樹
簡單地理解,滿足以下兩個條件的樹就是二叉樹:
1.本身是有序樹;
2.樹中包含的各個節點的度不能超過 2,即只能是 0、1 或者 2;
經過前人的總結,二叉樹具有以下幾個性質:
1.二叉樹中,第 i 層最多有 2^(i-1) 個結點。
2.如果二叉樹的深度為 K,那么此二叉樹最多有 2^K-1 個結點。
3.二叉樹中,終端結點數(葉子結點數)為 n0,度為 2 的結點數為 n2,則 n0=n2+1。
性質 3 的計算方法為:對于一個二叉樹來說,除了度為 0 的葉子結點和度為 2 的結點,剩下的就是度為 1 的結點(設為 n1),那么總結點 n=n0+n1+n2。
同時,對于每一個結點來說都是由其父結點分支表示的,假設樹中分枝數為 B,那么總結點數 n=B+1。而分枝數是可以通過 n1 和 n2 表示的,即 B=n1+2 * n2。所以,n 用另外一種方式表示為 n=n1+2 * n2+1。
兩種方式得到的 n 值組成一個方程組,就可以得出 n0=n2+1。
二叉樹還可以繼續分類,衍生出滿二叉樹和完全二叉樹
滿二叉樹
如果二叉樹中除了葉子結點,每個結點的度都為 2,則此二叉樹稱為滿二叉樹。
如圖所示就是一棵滿二叉樹。
滿二叉樹除了滿足普通二叉樹的性質,還具有以下性質:
1.)滿二叉樹中第 i 層的節點數為 2^(n-1) 個。
2.)深度為 k 的滿二叉樹必有 2^k-1 個節點 ,葉子數為 2^(k-1)。
3.)滿二叉樹中不存在度為 1 的節點,每一個分支點中都兩棵深度相同的子樹,且葉子節點都在最底層。
4.)具有 n 個節點的滿二叉樹的深度為 log2(n+1)。
完全二叉樹
如果二叉樹中除去最后一層節點為滿二叉樹,且最后一層的結點依次從左到右分布,則此二叉樹被稱為完全二叉樹。
如圖 a) 所示是一棵完全二叉樹,圖 b) 由于最后一層的節點沒有按照從左向右分布,因此只能算作是普通的二叉樹。
對于任意一個完全二叉樹來說,如果將含有的結點按照層次從左到右依次標號(如圖 3a))(從1開始標號,即根節點標號為1,若從0開始標號,則結論有所不同),對于任意一個結點 i ,完全二叉樹還有以下幾個結論成立:
1)當 i>0 時,父親結點為結點 [i/2] 。(i=1 時,表示的是根結點,無父親結點)
2)如果 2i>n(總結點的個數) ,則結點 i 肯定沒有左孩子(為葉子結點);否則其左孩子是結點 2i 。
3)如果 2i+1>n ,則結點 i 肯定沒有右孩子;否則右孩子是結點 2i+1 。
二叉樹的遍歷
在這里給出二叉樹的非遞歸遍歷算法
以下圖為例:
可在leetcode的探索中去刷關于樹的遍歷的題。(以下給出遍歷代碼皆在LeetCode中通過)
前序遍歷
前序遍歷結果:[1,2,4,5,3,6,7]
用棧來存儲節點:
1)節點1,2,4依次入棧,節點4 的左孩子為空,停止入棧;[1,2,4]
2)節點4 出棧,其右孩子為空,則不操作 接著節點2出棧,其右孩子不為空則進棧,節點5進棧,其左孩子為空停止進棧;[1,5]
3)節點5 出棧,右孩子為空;接著節點1(根節點)出棧,右孩子不為空則節點3進棧,節點6為其左孩子則繼續進棧;[3,6]
4)節點6,3出棧,節點7進棧;[7]
5)最后節點7出棧,左右孩子皆為空,棧也為空,則遍歷結束。
具體代碼如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
if(root==nullptr) return result;
stack<TreeNode*> s;
TreeNode* p=root;
while(!s.empty() || p!=nullptr) //當回到根節點,判斷右節點,此時 棧是空的,因此加一個p不為空的判斷n
{
while(p!=nullptr)
{
//前序遍歷,先保存遍歷結果
result.push_back(p->val);
s.push(p);
p=p->left;
}
TreeNode* t=s.top();
s.pop();
if(t->right!=nullptr)
{
p=t->right;
}
}
return result;
}
};
中序遍歷
中序遍歷結果:[4,2,5,1,6,3,7]
步驟和前序遍歷相同,不在贅述,只是打印結果位置不同,中序遍歷在節點出棧時打印結果,前序遍歷在節點進棧時打印結果。
具體代碼如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
if(root==nullptr)
{
return result;
}
TreeNode* p=root;
stack<TreeNode*> s;
while(!s.empty() || p!=nullptr)
{
while(p!=nullptr)
{
s.push(p);
p=p->left;
}
TreeNode* t=s.top();
s.pop();
//出棧時打印結果
result.push_back(t->val);
if(t->right!=nullptr) p=t->right;
}
return result;
}
};
后序遍歷
后序遍歷結果:[4,5,2,6,7,3,1]
過程稍微有些不同,類似于中序遍歷,左邊的節點都是要第一個進行訪問,在這里只是右節點和根節點的區別,同樣采用一個棧來解決這個問題。
只是后序遍歷在決定是否可以輸出當前節點的值的時候,需要考慮其左右子樹是否都已經遍歷完成。
所以需要設置一個lastVisit游標。
若lastVisit等于當前考查節點的右子樹,表示該節點的左右子樹都已經遍歷完成,則可以輸出當前節點。
并把lastVisit節點設置成當前節點,將當前游標節點node設置為空,下一輪就可以訪問棧頂元素
具體代碼:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
if(root==nullptr)
{
return result;
}
TreeNode* p=root;
TreeNode* lastvisit=nullptr;
stack<TreeNode*> s;
while(!s.empty() || p!=nullptr)
{
while(p!=nullptr)
{
s.push(p);
p=p->left;
}
TreeNode* t=s.top();
if(t->right!=nullptr && lastvisit!=t->right)
{
p=t->right;
}
else
{
result.push_back(t->val);
lastvisit=t;
s.pop();
}
}
return result;
}
};
層序遍歷
層序遍歷結果:[1,2,3,4,5,6,7]
層序遍歷相對簡單,用一個隊列來完成,首先根節點入隊列,當節點處隊列的時候判斷左右子節點是否為空,不為空則依次入隊列,直到隊列為空則遍歷完畢。
class Solution {
public:
vector<int> levelOrder(TreeNode* root)
{
vector<int> ret;
queue<TreeNode*> q;
if(root)
q.push(root);
while(!q.empty())
{
TreeNode* temp = q.front();
ret.push_back(temp->val);
q.pop();
if(temp->left)
q.push(temp->left);
if(temp->right)
q.push(temp->right);
}
return ret;
}
};
如果層序遍歷按層打印結果:[ [1],[2,3],[4,5,6,7] ],則需要記錄每層節點的數量,具體代碼如下:
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if(root==nullptr) return result;
vector<int> t;
queue<TreeNode*> q;
TreeNode* p=root;
int levelcount=1;
int nextlevelcount=0;//記錄每層的節點數目
q.push(root);
while(!q.empty())
{
TreeNode* tmp=q.front();
t.push_back(tmp->val);
levelcount--;
q.pop();
if(tmp->left!=nullptr)
{
q.push(tmp->left);
nextlevelcount++;
}
if(tmp->right!=nullptr)
{
q.push(tmp->right);
nextlevelcount++;
}
if(levelcount==0)
{
result.push_back(t);
t.clear();
levelcount=nextlevelcount;
nextlevelcount=0;
}
}
return result;
}
};
二叉搜索樹
完整代碼:https://github.com/songqiyuan/DataStruct/blob/master/Tree/searchTree.hpp
二叉搜索樹(BST)是二叉樹的一種特殊表示形式,它滿足如下特性:
1.每個節點中的值必須大于(或等于)存儲在其左側子樹中的任何值。
2每個節點中的值必須小于(或等于)存儲在其右子樹中的任何值。
二叉排序樹的操作主要有:
1.查找:遞歸查找是否存在key。
2.插入:原樹中不存在key,插入key返回true,否則返回false。
3.構造:循環的插入操作。
4.刪除:(1)葉子節點:直接刪除,不影響原樹。
(2)僅僅有左或右子樹的節點:節點刪除后,將它的左子樹或右子樹整個移動到刪除節點的位置就可以,子承父業。
(3)既有左又有右子樹的節點:找到須要刪除的節點p的直接前驅或者直接后繼s(尋找前驅和后繼節點),用s來替換節點p,然后再刪除節點s
具體實現代碼如下:
void _deleteNode(int y)
{
TreeNode* node = findKey(y);
if (node == nullptr) return ;
if (node->right == nullptr)
{
TransPlant( node, node->left); //情況1,情況2
}
else if (node->left == nullptr)
{
//輔助函數
TransPlant( node, node->right);//情況1,情況2
}
else
{
//情況3
//查找后繼節點
TreeNode* tmp=increment(node);
if (tmp->parent != node)// && tmp->right!=nullptr)
{
TransPlant(tmp, tmp->right);
tmp->right = node->right;
tmp->right->parent = tmp;
}
TransPlant(node, tmp);
tmp->left = node->left;
tmp->left->parent = tmp;
}
resetNode(node);
}
紅黑樹
完整代碼:https://github.com/songqiyuan/DataStruct/blob/master/Tree/rbtree.hpp
在學習紅黑樹前先理解旋轉的概念:
也許因為輸入的值不夠隨機,也許因為經過某些插入刪除操作二叉搜索樹可能會失去平衡,搜索效率降低,如圖:
所謂平衡與否,并沒有一個絕對的測量標準,“平衡”的大致意義是:沒有任何一個節點深度過大,不同的平衡條件造就出不同的效率表現以及不同的實現復雜度。AVL-tree,RB-tree均可實現出平衡二叉樹。
由于刪除和添加操作對樹做了修改,結果可能違反了紅黑樹的性質,為了維護這些性質,必須要修改樹中某些節點的顏色以及指針結構。
指針結構的修改是通過旋轉來完成的,這是能保持二叉搜索樹性質的搜索樹局部操作。如圖的左旋和右旋:
當在某個節點x上做左旋時,假設它的右孩子為y而不是NULL(x可以為其右孩子節點不是NULL節點的樹內任意節點),左旋以x到y的鏈為“支軸”進行。它使y成為樹的新的根節點,x成為y的左孩子,y的左孩子成為x的右孩子,右旋轉類似,具體代碼如下:
//左旋和右旋
//左旋
void RotateLeft(TreeNode* node)
{
TreeNode* y = node->right;
node->right = y->left;
if (y->left != nullptr)
{
y->left->parent = node;
}
y->parent = node->parent;
if (node->parent == nullptr)
{
_root = y;
}
else if (node == node->parent->left)
{
node->parent->left = y;
}
else if (node == node->parent->right)
{
node->parent->right = y;
}
y->left = node;
node->parent = y;
}
//右旋
void RotateRight(TreeNode* node)
{
TreeNode* y = node->left;
node->left = y->right;
if (y->right != nullptr)
{
y->right->parent = node;
}
y->parent = node->parent;
if (node->parent == nullptr)
{
_root = y;
}
else if (node == node->parent->left)
{
node->parent->left = y;
}
else if (node == node->parent->right)
{
node->parent->right = y;
}
y->right = node;
node->parent = y;
}
紅黑樹的性質
1.每個節點是紅色或者黑色;
2.根節點是黑色的;
3.每個葉節點是黑色的;
4.如果一個節點是紅色的,則它的兩個子節點都是黑色的;
5.對于每個節點,從該節點到其所有后代葉節點的簡單路徑上,均包含相同數目的黑節點。
下面主要講講可能破壞紅黑樹性質的刪除和插入操作。
紅黑樹的插入操作
紅黑樹的插入操作和二叉搜索樹的插入過程是類似的,只是插入的最后有所改變:將插入的節點著為紅色(為什么是紅色,因為要保持紅黑樹的性質5,如果插入的顏色為黑色,則必然改變某條路徑上的黑節點的個數,破壞紅黑樹的性質),然后調整紅黑樹(插入的節點為紅色,可能違背性質1或4)。
為了理解RBInsertFixup()函數是如何工作的,首先,要確定當節點被插入并著色后,紅黑樹的性質哪些是被破壞的。當插入為根節點時性質2(根節點為黑色)被破壞,當插入節點的父節點為紅色是性質4被破壞。下圖是一個修正過程:
情況1:z的叔父節點y是紅色的(由性質4知,祖父節點為黑色)
情況2:z的叔父節點y是黑色的且z是一個右孩子
情況3:z的叔父節點y是黑色的且z是一個左孩子
代碼如下:
//插入操作
void _Insert(int key)
{
TreeNode* node = _root;
TreeNode* pNode = nullptr;
while (node != nullptr)
{
pNode = node;
if (key > node->val)
{
node = node->right;
}
else
{
node = node->left;
}
}
TreeNode* pInsertNode = new TreeNode(key);
//根節點為空,此時無數據
if (pNode == nullptr)
{
_root = pInsertNode;
}
else if (pNode->val < key)
{
pNode->right = pInsertNode;
pInsertNode->parent = pNode;
}
else
{
pNode->left = pInsertNode;
pInsertNode->parent = pNode;
}
//修正紅黑樹
RBInsertFixup(pInsertNode);
}
//修正插入紅黑性質
void RBInsertFixup(TreeNode* node)
{
//當前插入節點 ,其父節點為紅色
while (node->parent != nullptr && node->parent->color == RED)
{
if (node->parent->parent->left == node->parent)
{
TreeNode* uncle = node->parent->parent->right;
if (uncle!=nullptr && uncle->color == RED)
{
//若父節點為紅色或叔父節點為紅色,則祖父節點一定為黑色
node->parent->color = BLACK;
uncle->color = BLACK;
node->parent->parent->color = RED;
node = node->parent->parent;
}
else
{
if (node->parent->right == node)
{
node = node->parent;
RotateLeft(node);
}
node->parent->color = BLACK;
node->parent->parent->color = RED;
RotateRight(node->parent->parent);
}
}
else if(node->parent->parent->right==node->parent)
{
TreeNode* uncle = node->parent->parent->left;
if (uncle != nullptr && uncle->color == RED)
{
node->parent->color = BLACK;
uncle->color = BLACK;
node->parent->parent->color = RED;
node = node->parent->parent;
}
else
{
if (node == node->parent->left)
{
node = node->parent;
RotateRight(node);
}
node->parent->color = BLACK;
node->parent->parent->color = RED;
RotateLeft(node->parent->parent);
}
}
}
_root->color = BLACK;
}
紅黑樹的刪除
下面所有情況都在真實刪除的節點的顏色為黑的前提下,刪除節點顏色為紅色并不違反紅黑樹性質。
情況1:x的兄弟節點w是紅色的
由于刪除節點為黑色,而w為紅色,則w必定有黑色子節點(性質4),所以可以改變w和x.p的顏色,并做一次左旋(如圖中的(a)),而不違反紅黑樹的任何性質。旋轉后x的新兄弟節點為w的一個子節點,其顏色為黑色。這樣就將情況轉換為2,3或4處理。
情況2:x的兄弟節點w是黑色的,而且w的兩個子節點都是黑色的
如圖中(b)所示,將兄弟節點w著色為紅色,這樣以父節點為根的子樹到葉節點的黑色節點是相同的,但比其他路徑少一個黑色節點,因此將父節點設置為為新的x節點。此時父節點的顏色未知紅色或黑色,當為紅色時可以看出違背了性質4,不過此時我們只需將當前x節點設置為黑色就可以了,此時紅黑樹性質全部恢復。當為黑色節點時,轉換為其他情況。
情況3:x的兄弟節點w是黑色,w的左孩子是紅色,w的右孩子是黑色
如圖中(c),可以交換w和其左孩子的顏色,然后對w進行右旋而不違反紅黑樹的性質(不理解的話可以畫圖驗證一下),現在x新的兄弟節點,w的左孩子。這樣我們就把情況3轉換為了情況4.
情況4:x的兄弟節點w是黑色的,且w的右孩子是紅色的
如圖中(d)所示,想辦法用紅色節點來來填補左子樹(x.p.left)中減少的黑節點數,通過改變一些節點的顏色來實現:a.將w著色為其父節點的顏色;b.將w.right由紅色改為黑色;c.將x.p的顏色改為黑色(用該節點來填補左子樹中缺失的黑色節點數);d.將x.p節點左旋,到此紅黑樹性質恢復,將x設置為根節點,結束修正。
代碼實現:
void _deleteNode(int y)
{
TreeNode* node = findKey(y);
if (node == nullptr) return;
//記錄刪除節點的顏色
int DeleteNodeColor = node->color;
//需要調整的節點
TreeNode* FixNode = nullptr;
if (node->right == nullptr)
{
FixNode = node->left;
TransPlant(node, node->left);
}
else if (node->left == nullptr)
{
FixNode = node->right;
TransPlant(node, node->right);
}
else
{
//查找后繼節點
TreeNode* tmp = increment(node);
DeleteNodeColor = tmp->color;
FixNode = tmp->right;
if (tmp->parent == node)
{
FixNode->parent = tmp;
}
if (tmp->parent != node)// && tmp->right!=nullptr)
{
TransPlant(tmp, tmp->right);
tmp->right = node->right;
tmp->right->parent = tmp;
}
TransPlant(node, tmp);
tmp->left = node->left;
tmp->left->parent = tmp;
tmp->color = node->color;
}
if (DeleteNodeColor == BLACK)
{
//處理平衡
RBDeleteFixUp(FixNode);
}
resetNode(node);
}
void RBDeleteFixUp(TreeNode* node)
{
while (node != nullptr && node->color == BLACK && node != _root)
{
if (node == node->parent->left)
{
TreeNode* bnode = node->parent->right;
//如果node為黑色節點,怎為了保持紅黑樹性質怎,其兄弟節點必定存在,即bnode不為空
if (bnode->color == RED)
{
//此時父節點一定為黑色
bnode->color = BLACK;
node->parent->color = RED;
RotateLeft(node->parent);
bnode = node->parent->right;
}
if (bnode->left->color == BLACK && bnode->right->color == BLACK)
{
bnode->color = RED;
node = node->parent;
}
else
{
if (bnode->right->color == BLACK)
{
//根據上一個條件判斷,則此時左節點必為紅色,父節點必為黑色
bnode->left->color = BLACK;
bnode->parent->color = RED;
RotateRight(bnode);
bnode = node->parent->right;
}
//如果兄弟節點的右節點為紅色
bnode->color = node->parent->color;
node->parent->color = BLACK;
bnode->right->color = BLACK;
RotateLeft(node->parent);
node = _root;//結束循環,達到平衡
}
}
else if (node == node->parent->right)
{
TreeNode* bnode = node->parent->left;
if (bnode->color == RED)
{
bnode->color = BLACK;
node->parent->color = RED;
RotateRight(node->parent);
bnode = node->parent->left;
}
if (bnode->left->color == BLACK && bnode->right->color == BLACK)
{
bnode->color = RED;
node = node->parent;
}
else
{
if (bnode->left->color == BLACK)
{
bnode->color = RED;
bnode->right->color = BLACK;
RotateLeft(bnode);
bnode = node->parent->left;
}
bnode->color = node->parent->color;
node->parent->color = BLACK;
bnode->left->color = BLACK;
RotateLeft(node->parent);
node = _root;
}
}
}
if (node != nullptr)
{
node->color = BLACK;
}
}
紅黑樹測試結果: