樹與二叉樹
一, 樹的基本術語
- 樹中一個結點的子結點個數稱爲該結點的度, 樹中結點的最大度數稱爲樹的度。如結點B的度爲2,結點D的度爲3,樹的度爲3.
- 度大於0的結點稱爲分支結點(又稱非終端結點),度爲0(沒有子女結點)的結點稱爲葉子結點(又稱終端結點)。在分支結點中,每個結點的分支數就是該結點的度。
- 結點的深度,高度和層次。
二, 樹的性質
- 樹中的結點數等於所有結點的度數加1。
- 度爲m的樹中第i層上至多有m^(i-1)個結點(i>=0)。
- 高度爲h的m叉樹至多有(m^h-1)/(m-1)個結點。
- 具有n個結點的m叉樹的最小高度爲[log m(n(m-1)+1)]。
三, 二叉樹的概念
1, 幾種特殊的二叉樹
- 滿二叉樹。一顆高度爲h。且含有2^h - 1個結點的二叉樹稱爲滿二叉樹,即樹中的每層都含有最多的結點,上圖所示。滿二叉樹的葉子結點都集中在二叉樹的最下一層,並且除葉子結點之外的每個結點度數均爲2。
可以對滿二叉樹按層續編號:約定編號從跟結點(跟結點編號爲1)起,自上而下,自左而右。這樣,每個結點對應一個編號,對於編號爲i的結點,若有雙親,則其雙親爲[i/2],若有左孩子,則左孩子爲2i; 若有右孩子,則右孩子爲2i+1。
- 完全二叉樹。設一個高度爲h,有n個結點的二叉樹,當且僅當其每個結點都與高度爲h的滿二叉樹中編號爲1到n的結點一一對應時,稱爲完全二叉樹,如上圖。這種樹的特點如下:
①. 如i<[i/2],則結點i爲分支結點,否則爲葉子結點。
②. 葉子節點只可能在層次最大的兩層上出現。對於最大層次中的葉子結點,都依賴排列在該層最左邊的位置上。
③. 若有度爲1的結點,則只可能有一個,且該結點只有左孩子而無右孩子。
④. 按層序編號後,一旦出現某個結點(編號爲i)爲葉子結點或只有左孩子, 則編號大於i的結點均爲葉子結點。
⑤. 若n爲奇數,則每個分支結點都有左子女和右子女; 若n爲二數,則編號最大的分支結點(編號爲n/2)只有左子女,沒有右子女,其中分支結點左,右子女都有。
2, 二叉樹的性質
- 非空二叉樹上的葉子結點數等於度爲2的結點數加1,即n0 = n2 + 1。
- 非空二叉樹上第k層上至多有2^k-1結點(k>=1)。
- 高度爲h的二叉樹至多有2^h-1個結點(h>=1)。
- 對於完全二叉樹按從上到下
- 具有n個(n>0)結點的完全二叉樹的高度爲[log2(n+1)]或者[|log2n|+1]。
四, 二叉樹的四種遍歷和線索二叉樹
1, 二叉樹的四種遍歷
所謂二叉樹的遍歷,是指按某條搜索路徑訪問樹中的每個結點,使得每個結點均被服務一次,而且僅被訪問一次。
由二叉樹的遞歸定義可知,遍歷一顆二叉樹便要決定根結點N,左子樹L和右子樹R的訪問順序。按照先遍歷左子樹再遍歷右子樹的原則,常見的遍歷次序有先序(NLR),中序(LNR),和後序(LRN)三種遍歷算法,其中"序"指的是根結點再何時被訪問。
① 先序遍歷
先序遍歷(preorder)的操作過程如下:
若二叉樹爲空,則什麼也不做,否則,
- 訪問根結點;
- 先序遍歷左子樹;
- 先序遍歷右子樹;
對應的遞歸算法如下:
void preorder(BiTree T)
{
if (T)
{
//訪問根結點
show(T);
//遞歸遍歷左子樹
preorder(T->lchild);
//遞歸遍歷右子樹
preorder(T->rchild);
}
}
如上圖所示, 先序遍歷所得的結點序列爲 1, 2, 4, 8, 9, 5, 10, 11, 3, 6, 12, 13, 7, 14, 15
② 中序遍歷
中序遍歷(inorder)的操作過程如下:
若二叉樹爲空,則什麼也不做,否則:
- 中序遍歷左子樹,
- 訪問根結點
- 中序遍歷右子樹
對應的遞歸算法如下:
void inorder(BiTree T)
{
if (T)
{
// 遞歸遍歷左子樹
inorder(T->lchild);
//訪問根結點
show(T);
// 遞歸遍歷右子樹
inorder(T->rchild);
}
}
對應上圖二叉樹, 中序遍歷所得到的結點序列爲 8, 4, 9, 2, 10, 5, 11, 1, 12, 6, 13, 3, 14, 15, 7
③ 後序遍歷
後序遍歷(postorder)的操作過程如下
若二叉樹爲空,則什麼也不做;否則
- 後序遍歷左子樹
- 後序遍歷右子樹
- 訪問根結點
對應的遞歸算法如下
void postorder(BiTree T)
{
if (T)
{
//遞歸遍歷左子樹
postorder(T->lchild);
// 遞歸遍歷右子樹
postorder(T->rchild);
//訪問根結點
show(T);
}
}
對應上圖二叉樹 :後序遍歷得到的結點序列爲 8, 9, 4, 10, 11, 5, 2, 12, 13, 6, 14, 15, 7, 3, 1
總結三種遍歷算法中,遞歸遍歷左,右子樹的順序都是固定的, 只是訪問根結點的順序不同。不管採用哪種遍歷算法,每個結點都訪問一次且僅訪問一次,故時間複雜度都是O(n)。在遞歸遍歷中,遞歸工作棧的棧深恰好爲樹的深度,所以在再壞情況下,二叉樹是有n個結點且深度爲n的單支樹,遍歷算法的空間複雜度爲O(n)。
④ 遞歸算法和非遞歸算法的轉換
藉助棧,可以將二叉樹的遞歸遍歷算法轉換爲非遞歸算法。下面以中序遍歷爲列子給出中序遍歷的非遞歸算法。先掃描(並非訪問)根結點的所有左結點並將它們一一進棧,然後出棧一個結點p(顯然結點p沒有左孩子結點或者左孩子結點已經被訪問過),訪問它。然後掃描該結點的右孩子結點,將其進棧,再掃描該右孩子結點的所有左結點並一一進棧,如此繼續,直到棧空爲止。
中序遍歷的非遞歸算法如下:
void inorder2(BiTree T)
{
//初始化棧
initstatck(s);
//p是遍歷指針
TiTree p = T;
// 棧不空或者p不空時循環
while (p || !s.emptr())
{
if (p)
{
//根指針進棧,遍歷左子樹
s.push(p);
//每遇到非空二叉樹先向左走
p = p->lchild;
}
else
{
// 根指針退棧, 訪問根結點,遍歷右子樹
//退棧, 訪問根結點
s.pop(p);
show(p);
// 再向右子樹走
p = p->rchild;
}
}
}
後序遍歷算法如下
void postorder2(BiTree T)
{
if (T)
{
s.push(T);
BiTree p = NULL;
while (!s.empty())
{
p = s.top();
// 判斷當前lcheild和rchild是否已經再棧中
if (p->lchild && p->lchild != T && p->rchild != T)
{
s.push(p);
}
else if (p->rchild && p->rchild != T)
{
s.push(p);
}
else
{
s.pop(p);
show(p);
T = p;
}
}
}
}
再寫非遞歸遍歷時要有遞歸壓棧的模型 來使用棧的數據結構
⑤ 層次遍歷
如上圖所示爲二叉樹的層次遍歷,即按照箭頭所指方向,按照1, 2, 3, 4 層次順序,對二叉樹中的各個結點進行訪問。
要進行層次遍歷,需要藉助一個隊列。先將二叉樹根結點入隊,然後出隊,訪問該結點,若它有左子樹,則將左子樹根結點入隊,如它有右子樹,則將右子樹根結點入隊。然後出隊,對出隊結點訪問,如此反覆,直到隊列爲空。
二叉樹的層次遍歷算法如下
void level_order(BiTree T)
{
initqueue(q);
BiTree p;
//將根結點入隊列中
q.endqueue(T);
//隊列不空循環
while (!q.empty())
{
//隊頭元素出隊
q.dequeue(p);
show(p);
if (p->lchild)
{
//左子樹不空,則左子樹入隊列
q.endqueue(p->lchild);
}
if (p->rchild)
{
//右子樹不空, 則右子樹入隊列
q.endqueue(p->rchild);
}
}
}
五, 樹與二叉樹的應用
1, 平衡二叉樹
① 平衡二叉樹的定義
爲避免樹的高度增長過快,降低二叉排序樹的性能, 我們規定在插入和刪除二叉樹的結點的時, 要保證任意結點的左, 右子樹高度差的絕對不超過1, 將這樣的二叉樹稱爲平衡二叉樹(Balanced Binary Tree), 簡稱平衡樹(AVL)。定義結點左子樹和右子樹的高度差爲該結點的平衡因子,則平衡二叉樹結點的平衡因子的值只可能是-1, 0或1。
因此,平衡二叉樹可定義的爲或者是一顆空樹, 或者是具有下列性質的二叉樹; 它的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的高度差的絕對值不超過1。如上圖a所示 ,是平衡二叉樹,如上圖b所示是不平衡二叉樹。結點種的值爲該結點的平衡因子。
② 平衡二叉樹的插入
二叉排序樹保證平衡的基本思想如下:每當在二叉排序樹種插入(或刪除)一個結點時,首先檢查其插入路徑上的結點因爲此次操作而導致了不平衡。若導致了不平衡,在保持二叉排序樹特性的前提下,調整各結點的位置關係,使之重新達到平衡。
注意:每次調整的對象都是最小的不平衡子樹,即以插入路徑上離插入結點最近的平衡因子的絕對值大於1的結點作爲根的子樹。如上圖所示框的爲最小不平衡子樹。
平衡二叉樹的插入過程的前半部分與二叉排序樹相同,但在新結點插入後,若造成查找路徑上的某個結點不再平衡,則需要做出相應的調整。一般可將失去平衡後進行調整的規律歸納爲下列4種情況:
1, LL平衡旋轉(右單旋轉)
由於在結點A的左孩子(L)的左子樹(L)上插入了新的結點,A的平衡因子由1曾至2, 導致以A爲根的子樹失去平衡, 需要一次向右的旋轉操作。將A的左孩子B向右上旋轉代替A成爲根結點,將A結點向右下旋轉成爲B的右子樹的根結點,而B的原右子樹則作爲A結點的左子樹。
2, RR平衡旋轉(左單旋轉)
由於在結點A的右孩子®的右子樹®上插入了新結點,A的平衡因子由-1減至-2,導致以A爲根的子樹失去平衡,需要一次向左的旋轉操作。將A的右孩子B向左上旋轉替代A成爲根結點,將A結點向左下旋轉成爲B的左子樹的根結點,而B的原右子樹則作爲A結點的右子樹,
3, LR平衡旋轉(先左後右雙旋轉)
由於在A的左孩子(L)的右子樹®上插入新的結點,A的平衡因子由1曾至2,導致以A爲根的子樹失去平衡,需要進行二次旋轉操作,先左旋轉後右旋轉。先將A結點的左孩子B的右子樹的根結點C向左上旋轉提升到B結點的位置,然後再把該C結點向右上旋轉提升到A結點的位置。
4, RL平衡旋轉(先右後左雙旋轉)
由於在A的右孩子®的左子樹(L)上插入新的結點,A的平衡因子由-1減至-2,導致以A爲根的子樹失去平衡,需要進行二次旋轉操作,先右旋轉後左旋轉。先將A結點的右孩子B的左子樹的根結點C向右上旋轉提升到B結點的位置,然後再把該結點向左上旋轉提升到A結點的位置。