樹是一種“一對多”的數據結構,是n(n≥0)個結點的有限集,其中n=0時稱為空樹
樹滿足的一些性質和概念
- n>0時,根結點唯一
- n>1時,除去根結點的其他結點構成若干個互不相交的有限集T1,T2...,其中每一個集合又是一棵樹,稱為根的子樹
- 結點擁有的子樹數稱為結點的度(Degree),度為0的結點稱為葉子結點
- 樹的度是樹內各結點的度的最大值
- 結點的層數是從根開始定義起,根為第一層,根的孩子是第二層,以此類推。樹中結點的最大層次稱為樹的深度(Depth)或高度
- 如果各子樹看成從左至右不可互換的,則稱為有序樹,否則為無序樹
- 森林是互不相交的樹的集合,某個結點的子樹可以看做是森林
樹的存儲結構:雙親表示法、孩子表示法、孩子兄弟表示法(這個表示法充分利用了二叉樹的特性和算法來處理這棵樹)
二叉樹的定義:每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”和“右子樹”
二叉樹滿足的一些性質和概念
- 二叉樹不存在度大於2的結點
- 左右子樹是有順序的,即使某結點只有一棵子樹,也要區分它是左子樹還是右子樹
- 根據(2)所說,二叉樹具有五種基本形態:空二叉樹、只有一個根節點、只有左子樹、只有右子樹、左右子樹都有
- 二叉樹第i層上至多有2^(i-1)個結點(i≥1)
- 深度為k的二叉樹至多有2^k - 1個結點(k≥1),此時為滿二叉樹
- 對任何一棵二叉樹T,如果其葉子結點數為n0,度為2的結點數為n2,則n0 = n2 + 1。這個的推導:設結點數為n,可以知道結點間連接線數為n-1。於是有兩個式子:n-1 = n1 + 2*n2 和 n = n0 + n1 +n2,聯合解出n0 = n2 + 1
- 具有n個結點的完全二叉樹的深度為log2 n向下取整然后加1 => 通過滿二叉樹2^n - 1可以推出
- 對於完全二叉樹,在有左右子結點的情況下,設根結點的編號是n(這個編號從1開始),則左孩子的編號是2n,右孩子的編號是2n+1
特殊的二叉樹
- 斜樹:所有的結點都只有左子樹或右子樹,特點是結點的個數與二叉樹的深度相同
- 滿二叉樹:所有的分支結點(非葉子)都存在左子樹和右子樹,並且所有的葉子都在同一層(完全對稱,非葉子結點的度一定是2,結點數是2^n - 1)
- 完全二叉樹:允許在滿二叉樹中去掉若干個最后的結點,但是存在的結點序號一定與滿二叉樹位置一致(比滿二叉樹要求低一點,所以滿二叉樹一定是完全二叉樹,反之則不成立。如果某結點的度為1,則該結點只有左孩子)
二叉樹的存儲結構
1) 順序存儲:根據二叉樹概念第8點,可以知道完全二叉樹的父結點和孩子結點是有算術關系的,所以用一維數組存儲很方便。但是對於一般的二叉樹則會耗費很多存儲空間(如有5層的斜樹,只有1,2,4,8,16這幾個索引值是存了值的,其他空間都沒有作用)
2) 二叉鏈表:一個結點用(左孩子指針, 右孩子指針, 數據域)來表示,如果沒有孩子結點,則指針域指向空即可,可以節省很多空間(當然如果有必要指向父結點,也可以構造三叉鏈表,略) ,以下是鏈表結構體定義:
typedef struct BiTNode { TElemType data; struct BiTNode *lchild, *rchild; } BiTNode, *BiTree;
二叉樹的遍歷
由於(鏈表結構的)二叉樹沒有明確的“次序”一說(不存在唯一的前驅和后繼關系),所以只要是按照某種次序依次訪問二叉樹中所有結點,使得每個結點被訪問且僅被訪問一次就是二叉樹的遍歷。
習慣的方法有四種:前序遍歷,中序遍歷,后序遍歷,層序遍歷
前序遍歷:先訪問根結點,然后前序遍歷左子樹,再前序遍歷右子樹
=> 注意到這里是一個遞歸的過程,后面的遍歷方法采用的也是這個思想。另外,這個“序”針對的是根結點的訪問時機
中序遍歷:先中序遍歷根結點的左子樹,然后訪問根結點,再中序遍歷右子樹
后序遍歷:先后序遍歷根結點的左子樹,然后后序遍歷右子樹,再訪問根結點
層序遍歷:從上到下從左到右對結點逐個訪問
以上的遍歷方法都是把樹中的結點變成某種意義的線性序列,給程序的實現帶來好處。以下例子:
# 代表指針域null
前序遍歷:ABDCE
中序遍歷:DBAEC
后序遍歷:DBECA
層序遍歷:ABCDE
前序遍歷算法示例(其他算法可以參考這個)
void PreOrderTraverse(BiTree T) { if(T == NULL) return; access(T->data); // 訪問結點 PreOrderTraverse(T->lchild); // 遞歸前序遍歷左子樹 PreOrderTraverse(T->rchild); // 遞歸前序遍歷右子樹 }
有個性質:已知中序遍歷和其他兩種遍歷中的其中一種,可以唯一確定一棵二叉樹。(證明略)
二叉樹的建立
需要引入“擴展二叉樹”的概念,其實就是在所有的葉子結點后添加一個空指針的標記,用#表示,就如上面的圖所示。
上面的圖前序遍歷的結果是(包括#的情況):ABD###CE###,將這段結果輸入到程序中,即可生成二叉樹結構:
void CreateBiTree(BiTree *T) { TElemType ch; scanf("%c", &ch); if(ch == '#') *T = NULL; else{ *T = (BiTree) malloc (sizeof(BiTNode)); if(! *T) exit(OVERFLOW); (*T)->data = ch; CreateBiTree(& (*T)->lchild); CreateBiTree(& (*T)->rchild); } }
以上是基於前序遍歷的建立二叉樹的函數。
線索二叉樹
上面的CreateBiTree方法有一個浪費空間的點,就是有很多空指針域的存在(雖然已經比順序結構少了很多),可以把這些空間利用起來。
首先要知道有多少個空指針域:對於一個有n個結點的二叉鏈表,一共有2n個指針域,而n個結點對應有n-1條分支線數,所以就存在n+1個空指針域
由於遍歷是根據前面的4個算法來得到結果,但是遍歷之后由於沒有記錄信息,所以不能直接知道某個結點的前驅和后繼是誰,可以考慮在創建就(利用這幾個空指針域)記住這些前驅和后繼,我們把這種指向前驅和后繼的指針稱為線索,加上線索的二叉鏈表稱為線索鏈表,響應的二叉樹叫線索二叉樹。對二叉樹以某種次序遍歷使其變為線索二叉樹的過程稱做是線索化。
過程:對於空的左孩子指針域,指向該父結點的前驅;對於空的右孩子指針域,指向該父結點的后繼。另外,需要一個區分標志,用於辨別到底下一個是左右孩子還是前驅后繼,於是,結構體變成了(lchild, rchild, ltag, rtag, data),其中tag為0的時候表示指向該結點的左右孩子,為1的時候指向前驅后繼。
例:中序遍歷線索化的過程
BiThrTree pre; // 全局變量,指向剛剛訪問的結點 // 中序遍歷 void InThreading(BiThrTree p) { if(p) { InThreading(p->lchild); if(!p->lchild) { p->LTag = Thread; // 這是個常量 p->lchild = pre; } if(!pre->rchild) { pre->RTag = Thread; pre->rchild = p; } pre = p; InThreading(p->rchild); } }
如果所用的二叉樹需經常遍歷或查找結點時需要某種遍歷序列中的前驅和后繼,那么采用線索二叉鏈表的存儲結構就是非常不錯的選擇
赫夫曼樹
概念:給定n個權值作為n的葉子結點,構造一棵二叉樹,若帶權路徑長度(WPL)達到最小,稱這樣的二叉樹為最優二叉樹,也稱為哈夫曼樹
路徑長度:從樹的一個結點到另一個結點之間的分支構成兩個結點之間的路徑,路徑上的分支數目稱為路徑長度(其實就是兩個結點之間的線有多少根)
樹的路徑長度:從樹根到每一個結點的路徑長度之和
結點之間的帶權路徑長度:從樹的一個結點到另一個結點之間的分支上的權值的總和
樹的帶權路徑長度:樹中所有葉子結點的帶權路徑長度之和(需要使用到這個)
赫夫曼樹(最優二叉樹)的作用是優化對樹中結點的訪問次數,使權值大(通常是訪問次數多的)盡可能放在靠近根結點的位置 => 樹的帶權路徑長度最短
構造方法:

