数据结构:树(Tree)【详解】
友情鏈接:數據結構專欄
目錄
- 樹
- 【知識框架】
- 一、樹的基本概念
- 1、樹的定義
- 2、基本術語
- 3、樹的性質
- 二、樹的存儲結構
- 1、雙親表示法
- 2、孩子表示法
- 3、孩子兄弟表示法
- 二叉樹
- 一、二叉樹的概念
- 1、二叉樹的定義
- 2、幾個特殊的二叉樹
- (1)斜樹
- (2)滿二叉樹
- (3)完全二叉樹
- (4)二叉排序樹
- (5)平衡二叉樹
- 3、二叉樹的性質
- 4、二叉樹的存儲結構
- (1)順序存儲結構
- (2)鏈式存儲結構
- 二、遍歷二叉樹
- 1、先序遍歷
- 2、中序遍歷
- 3、后序遍歷
- 4、遞歸算法和非遞歸算法的轉換
- (1)中序遍歷的非遞歸算法
- (2)先序遍歷的非遞歸算法
- (3)后序遍歷的非遞歸算法
- 5、層次遍歷
- 6、由遍歷序列構造二叉樹
- 三、線索二叉樹
- 1、線索二叉樹原理
- 2、線索二叉樹的結構實現
- 3、二叉樹的線索化
- (1)中序線索二叉樹
- (2)先序和后序線索二叉樹
- 四、樹、森林與二叉樹的轉化
- 1、樹轉換為二叉樹
- 2、森林轉化為二叉樹
- 五、樹和森林的遍歷
- 1、樹的遍歷
- 2、森林的遍歷
- 樹與二叉樹的應用
- 一、二叉排序樹
- 1、定義
- 2、二叉排序樹的常見操作
- (1)查找操作
- (2)插入操作
- (3)刪除操作
- 3、小結(引申出平衡二叉樹)
- 二、平衡二叉樹
- 1、定義
- 2、平衡二叉樹的查找
- 3、平衡二叉樹的插入
- 三、哈夫曼樹和哈夫曼編碼
- 1、哈夫曼樹的定義和原理
- 2、哈夫曼樹的構造
- 3、哈夫曼編碼
- 附錄
- 上文鏈接
- 下文鏈接
- 專欄
- 參考資料
樹
【知識框架】
一、樹的基本概念
1、樹的定義
樹是n(n>=0)個結點的有限集。當n = 0時,稱為空樹。在任意一棵非空樹中應滿足:
顯然,樹的定義是遞歸的,即在樹的定義中又用到了自身,樹是一種遞歸的數據結構。樹作為一種邏輯結構,同時也是一種分層結構,具有以下兩個特點:
因此n個結點的樹中有n-1條邊。
2、基本術語
下面結合圖示來說明一下樹的一些基本術語和概念。
結點的層次從樹根開始定義,根結點為第1層,它的子結點為第2層,以此類推。雙親在同一層的結點互為堂兄弟,圖中結點G與E,F,H,I,J互為堂兄弟。
結點的深度是從根結點開始自頂向下逐層累加的。
結點的高度是從葉結點開始自底向上逐層累加的。
樹的高度(或深度)是樹中結點的最大層數。圖中樹的高度為4。
注意:由于樹中的分支是有向的,即從雙親指向孩子,所以樹中的路徑是從上向下的,同一雙親的兩個孩子之間不存在路徑。
注意:上述概念無須刻意記憶, 根據實例理解即可。
3、樹的性質
樹具有如下最基本的性質:
二、樹的存儲結構
在介紹以下三種存儲結構的過程中,我們都以下面這個樹為例子。
1、雙親表示法
我們假設以一組連續空間存儲樹的結點,同時在每個結點中,附設一個指示器指示其雙親結點到鏈表中的位置。也就是說,每個結點除了知道自已是誰以外,還知道它的雙親在哪里。
其中data是數據域,存儲結點的數據信息。而parent是指針域,存儲該結點的雙親在數組中的下標。
以下是我們的雙親表示法的結點結構定義代碼。
這樣的存儲結構,我們可以根據結點的parent 指針很容易找到它的雙親結點,所用的時間復雜度為0(1),直到parent為-1時,表示找到了樹結點的根。可如果我們要知道結點的孩子是什么,對不起,請遍歷整個結構才行。
2、孩子表示法
具體辦法是,把每個結點的孩子結點排列起來,以單鏈表作存儲結構,則n個結點有n個孩子鏈表,如果是葉子結點則此單鏈表為空。然后n個頭指針又組成-一個線性表,采用順序存儲結構,存放進一個一維數組中,如圖所示。
為此,設計兩種結點結構,一個是孩子鏈表的孩子結點。
其中child是數據域,用來存儲某個結點在表頭數組中的下標。next 是指針域,用來存儲指向某結點的下一個孩子結點的指針。
另一個是表頭數組的表頭結點。
其中data是數據域,存儲某結點的數據信息。firstchild 是頭指針域,存儲該結點的孩子鏈表的頭指針。
以下是我們的孩子表示法的結構定義代碼。
/*樹的孩子表示法結構定義*/ #define MAX_TREE_SIZE 100 /*孩子結點*/ typedef struct CTNode{int child;struct CTNode *next; }*ChildPtr; /*表頭結點*/ typedef struct{TElemType data;ChildPtr firstchild; }CTBox; /*樹結構*/ typedef struct{CTBox nodes[MAX_TREE_SIZE]; //結點數組int r, n; //根的位置和結點數 }這樣的結構對于我們要查找某個結點的某個孩子,或者找某個結點的兄弟,只需要查找這個結點的孩子單鏈表即可。對于遍歷整棵樹也是很方便的,對頭結點的數組循環即可。
但是,這也存在著問題,我如何知道某個結點的雙親是誰呢?比較麻煩,需要整棵樹遍歷才行,難道就不可以把雙親表示法和孩子表示法綜合一下嗎? 當然是可以,這個讀者可自己嘗試結合一下,在次不做贅述。
3、孩子兄弟表示法
剛才我們分別從雙親的角度和從孩子的角度研究樹的存儲結構,如果我們從樹結點的兄弟的角度又會如何呢?當然,對于樹這樣的層級結構來說,只研究結點的兄弟是不行的,我們觀察后發現,任意一棵樹, 它的結點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。 因此,我們設置兩個指針,分別指向該結點的第一個孩子和此結點的右兄弟。
結點的結構如下:
其中data是數據域,firstchild 為指針域,存儲該結點的第一個孩子結點的存儲地址,rightsib 是指針域,存儲該結點的右兄弟結點的存儲地址。
這種表示法,給查找某個結點的某個孩子帶來了方便。
結構定義代碼如下。
于是通過這種結構,我們就把原來的樹變成了這個樣子:
這不就是個二叉樹么?
沒錯,其實這個表示法的最大好處就是它把一棵復雜的樹變成了一棵二叉樹。
接下來,我們詳細介紹二叉樹。
二叉樹
一、二叉樹的概念
1、二叉樹的定義
二叉樹是另一種樹形結構,其特點是每個結點至多只有兩棵子樹( 即二叉樹中不存在度大于2的結點),并且二叉樹的子樹有左右之分,其次序不能任意顛倒。
與樹相似,二叉樹也以遞歸的形式定義。二叉樹是n (n≥0) 個結點的有限集合:
二叉樹是有序樹,若將其左、右子樹顛倒,則成為另一棵不同的二叉樹。即使樹中結點只有一棵子樹,也要區分它是左子樹還是右子樹。二叉樹的5種基本形態如圖所示。
2、幾個特殊的二叉樹
(1)斜樹
所有的結點都只有左子樹的二叉樹叫左斜樹。所有結點都是只有右子樹的二叉樹叫右斜樹。這兩者統稱為斜樹。
(2)滿二叉樹
一棵高度為hhh,且含有2h?12^h-12h?1個結點的二叉樹稱為滿二叉樹,即樹中的每層都含有最多的結點。滿二叉樹的葉子結點都集中在二叉樹的最下一層,并且除葉子結點之外的每個結點度數均為222。可以對滿二叉樹按層序編號:約定編號從根結點(根結點編號為111)起,自上而下,自左向右。這樣,每個結點對應一個編號,對于編號為i的結點,若有雙親,則其雙親為i/2i/2i/2,若有左孩子,則左孩子為2i2i2i;若有右孩子,則右孩子為2i+12i+12i+1。
(3)完全二叉樹
高度為hhh、有nnn個結點的二叉樹,當且僅當其每個結點都與高度為hhh的滿二叉樹中編號為1~n的結點一一對應時,稱為完全二叉樹,如圖所示。其特點如下:
(4)二叉排序樹
左子樹上所有結點的關鍵字均小于根結點的關鍵字;右子樹上的所有結點的關鍵字均大于根結點的關鍵字;左子樹和右子樹又各是一棵二叉排序樹。
(5)平衡二叉樹
樹上任一結點的左子樹和右子樹的深度之差不超過1。
3、二叉樹的性質
- i>1i>1i>1時,結點iii的雙親的編號為i/2i/2i/2,即當iii為偶數時, 它是雙親的左孩子;當i為奇數時,它是雙親的右孩子。
- 當2i≤n2i≤n2i≤n時,結點iii的左孩子編號為2i2i2i, 否則無左孩子。
- 當2i+1≤n2i+1≤n2i+1≤n時,結點iii的右孩子編號為2i+12i+12i+1,否則無右孩子。
- 結點iii所在層次(深度)為{log2i}+1\{log_2i\}+ 1{log2?i}+1。
4、二叉樹的存儲結構
(1)順序存儲結構
二叉樹的順序存儲是指用一組地址連續的存儲單元依次自上而下、自左至右存儲完全二叉樹上的結點元素,即將完全二叉樹上編號為iii的結點元素存儲在一維數組下標為i?1i-1i?1的分量中。
依據二叉樹的性質,完全二叉樹和滿二叉樹采用順序存儲比較合適,樹中結點的序號可以唯一地反映結點之間的邏輯關系,這樣既能最大可能地節省存儲空間,又能利用數組元素的下標值確定結點在二叉樹中的位置,以及結點之間的關系。
但對于一般的二叉樹,為了讓數組下標能反映二叉樹中結點之間的邏輯關系,只能添加一些并不存在的空結點,讓其每個結點與完全二叉樹上的結點相對照,再存儲到一維數組的相應分量中。然而,在最壞情況下,一個高度為hhh且只有hhh個結點的單支樹卻需要占據近2h?12h-12h?1個存儲單元。二叉樹的順序存儲結構如圖所示,其中0表示并不存在的空結點。
(2)鏈式存儲結構
既然順序存儲適用性不強,我們就要考慮鏈式存儲結構。二叉樹每個結點最多有兩個孩子,所以為它設計一個數據域和兩個指針域是比較自然的想法,我們稱這樣的鏈表叫做二叉鏈表。
其中data是數據域,lchild 和rchild都是指針域,分別存放指向左孩子和右孩子的指針。
以下是我們的二叉鏈表的結點結構定義代碼。
容易驗證,在含有nnn個結點的二叉鏈表中,含有n+1n + 1n+1個空鏈域。
二、遍歷二叉樹
二叉樹的遍歷( traversing binary tree )是指從根結點出發,按照某種次序依次訪問二叉樹中所有結點,使得每個結點被訪問一次且僅被訪問一次。
1、先序遍歷
先序遍歷(PreOrder) 的操作過程如下:
若二叉樹為空,則什么也不做,否則,
1)訪問根結點;
2)先序遍歷左子樹;
3)先序遍歷右子樹。
對應的遞歸算法如下:
2、中序遍歷
中序遍歷( InOrder)的操作過程如下:
若二叉樹為空,則什么也不做,否則,
1)中序遍歷左子樹;
2)訪問根結點;
3)中序遍歷右子樹。
對應的遞歸算法如下:
3、后序遍歷
后序遍歷(PostOrder) 的操作過程如下:
若二叉樹為空,則什么也不做,否則,
1)后序遍歷左子樹;
2)后序遍歷右子樹;
3)訪問根結點。
對應的遞歸算法如下:
三種遍歷算法中,遞歸遍歷左、右子樹的順序都是固定的,只是訪問根結點的順序不同。不管采用哪種遍歷算法,每個結點都訪問一次且僅訪問一次,故時間復雜度都是O(n)。在遞歸遍歷中,遞歸工作棧的棧深恰好為樹的深度,所以在最壞情況下,二叉樹是有n個結點且深度為n的單支樹,遍歷算法的空間復雜度為O(n)。
4、遞歸算法和非遞歸算法的轉換
我們以下圖的樹為例子。
(1)中序遍歷的非遞歸算法
借助棧,我們來分析中序遍歷的訪問過程:
棧頂D出棧并訪問,它是中序序列的第一個結點; D右孩子為空,棧頂B出棧并訪問; B右孩子不空,將其右孩子E入棧,E左孩子為空,棧頂E出棧并訪問; E右孩子為空,棧頂A出棧并訪問; A右孩子不空,將其右孩子C入棧,C左孩子為空,棧頂C出棧并訪問。由此得到中序序列DBEAC。
根據分析可以寫出中序遍歷的非遞歸算法如下:
(2)先序遍歷的非遞歸算法
先序遍歷和中序遍歷的基本思想是類似的,只需把訪問結點操作放在入棧操作的前面。先序遍歷的非遞歸算法如下:
void PreOrder2(BiTree T){InitStack(S); //初始化棧SBiTree p = T; //p是遍歷指針while(p || !IsEmpty(S)){ //棧不空或p不空時循環if(p){visit(p); //訪問出棧結點Push(S, p); //當前節點入棧p = p->lchild; //左孩子不空,一直向左走}else{Pop(S, p); //棧頂元素出棧p = p->rchild; //向右子樹走,p賦值為當前結點的右孩子}} }(3)后序遍歷的非遞歸算法
后序遍歷的非遞歸實現是三種遍歷方法中最難的。因為在后序遍歷中,要保證左孩了和右孩子都已被訪問并且左孩子在右孩子前訪問才能訪問根結點,這就為流程的控制帶來了難題。
算法思想:后序非遞歸遍歷二叉樹是先訪問左子樹,再訪問右子樹,最后訪問根結點。
棧頂D的右孩子為空,出棧并訪問,它是后序序列的第一個結點;棧頂B的右孩子不空且未被訪問過,E入棧,棧頂E的左右孩子均為空,出棧并訪問;棧頂B的右孩子不空但已被訪問,B出棧并訪問;棧項A的右孩子不空且未被訪問過,C入棧,棧項C的左右孩子均為空,出棧并訪問;棧頂A的右孩子不空但已被訪問,A出棧并訪問。由此得到后序序列DEBCA。
在上述思想的第②步中,必須分清返回時是從左子樹返回的還是從右子樹返回的,因此設定一個輔助指針r,指向最近訪問過的結點。也可在結點中增加一個標志域,記錄是否已被訪問。
后序遍歷的非遞歸算法如下:
void PostOrder2(BiTree T){InitStack(S);p = T;r = NULL;while(p || !IsEmpty(S)){if(p){ //走到最左邊push(S, p);p = p->lchild;}else{ //向右GetTop(S, p); //讀棧頂元素(非出棧)//若右子樹存在,且未被訪問過if(p->rchild && p->rchild != r){p = p->rchild; //轉向右push(S, p); //壓入棧p = p->lchild; //再走到最左}else{ //否則,彈出結點并訪問pop(S, p); //將結點彈出visit(p->data); //訪問該結點r = p; //記錄最近訪問過的結點p = NULL;}}} }5、層次遍歷
下圖為二叉樹的層次遍歷,即按照箭頭所指方向,按照1,2,3, 4的層次順序,對二叉樹中的各個結點進行訪問。
要進行層次遍歷,需要借助一個隊列。先將二叉樹根結點入隊,然后出隊,訪問出隊結點,若它有左子樹,則將左子樹根結點入隊;若它有右子樹,則將右子樹根結點入隊。然后出隊,訪問出隊結…如此反復,直至隊列為空。
二叉樹的層次遍歷算法如下:
6、由遍歷序列構造二叉樹
由二叉樹的先序序列和中序序列可以唯一地確定一棵二叉樹。
在先序遍歷序列中,第一個結點一定是二叉樹的根結點;而在中序遍歷中,根結點必然將中序序列分割成兩個子序列,前一個子序列是根結點的左子樹的中序序列,后一個子序列是根結點的右子樹的中序序列。根據這兩個子序列,在先序序列中找到對應的左子序列和右子序列。在先序序列中,左子序列的第一個結點是左子樹的根結點,右子序列的第一個結點是右子樹的根結點。
如此遞歸地進行下去,便能唯一地確定這棵二叉樹
同理,由二叉樹的后序序列和中序序列也可以唯一地確定一棵二叉樹。
因為后序序列的最后一個結點就如同先序序列的第一個結點,可以將中序序列分割成兩個子序列,然后采用類似的方法遞歸地進行劃分,進而得到一棵二叉樹。
由二叉樹的層序序列和中序序列也可以唯一地確定一棵二叉樹。
要注意的是,若只知道二叉樹的先序序列和后序序列,則無法唯一確定一棵二叉樹。
例如,求先序序列( ABCDEFGH)和中序序列( BCAEDGHFI)所確定的二叉樹
首先,由先序序列可知A為二叉樹的根結點。中序序列中A之前的BC為左子樹的中序序列,EDGHFI為右子樹的中序序列。然后由先序序列可知B是左子樹的根結點,D是右子樹的根結點。以此類推,就能將剩下的結點繼續分解下去,最后得到的二叉樹如圖?所示。
三、線索二叉樹
1、線索二叉樹原理
遍歷二叉樹是以一定的規則將二叉樹中的結點排列成一個線性序列,從而得到幾種遍歷序列,使得該序列中的每個結點(第一個和最后一個結點除外)都有一個直接前驅和直接后繼。
傳統的二叉鏈表存儲僅能體現一種父子關系,不能直接得到結點在遍歷中的前驅或后繼。
首先我們要來看看這空指針有多少個呢?對于一個有n個結點的二叉鏈表,每個結點有指向左右孩子的兩個指針域,所以一共是2n個指針域。而n個結點的二叉樹一共有n-1 條分支線數,也就是說,其實是存在2n- (n-1) =n+1個空指針域。
由此設想能否利用這些空指針來存放指向其前驅或后繼的指針?這樣就可以像遍歷單鏈表那樣方便地遍歷二叉樹。引入線索二叉樹正是為了加快查找結點前驅和后繼的速度。
我們把這種指向前驅和后繼的指針稱為線索,加上線索的二叉鏈表稱為線索鏈表,相應的二叉樹就稱為線索二叉樹(Threaded Binary Tree)。
其結點結構如下所示:
其中
- ltag為0時指向該結點的左孩子,為1時指向該結點的前驅。
- rtag為0時指向該結點的右孩子,為1時指向該結點的后繼。
因此對于上圖的二叉鏈表圖可以修改為下圖的樣子。
2、線索二叉樹的結構實現
二叉樹的線索存儲結構代碼如下:
typedef struct ThreadNode{ElemType data; //數據元素struct ThreadNode *lchild, *rchild; //左、右孩子指針int ltag, rtag; //左、右線索標志 }ThreadNode, *ThreadTree;3、二叉樹的線索化
二叉樹的線索化是將二叉鏈表中的空指針改為指向前驅或后繼的線索。而前驅或后繼的信息只有在遍歷時才能得到,因此線索化的實質就是遍歷一次二叉樹,線索化的過程就是在遍歷的過程中修改空指針的過程。
(1)中序線索二叉樹
以中序線索二叉樹的建立為例。附設指針pre指向剛剛訪問過的結點,指針p指向正在訪問的結點,即pre指向p的前驅。在中序遍歷的過程中,檢查p的左指針是否為空,若為空就將它指向pre;檢查pre的右指針是否為空,若為空就將它指向p,如下圖所示。
通過中序遍歷對二叉樹線索化的遞歸算法如下:
你會發現,除了中間的代碼,和二叉樹中序遍歷的遞歸代碼幾乎完全一樣。只不過將本是訪問結點的功能改成了線索化的功能。
通過中序遍歷建立中序線索二叉樹的主過程算法如下:
void CreateInThread(ThreadTree T){ThreadTree pre = NULL;if(T != NULL){InThread(T, pre); //線索化二叉樹pre->rchild = NULL; //處理遍歷的最后一個結點pre->rtag = 1;} }為了方便,可以在二叉樹的線索鏈表上也添加一個頭結點,令其lchild域的指針指向二叉樹的根結點,其rchild域的指針指向中序遍歷時訪問的最后一個結點;令二叉樹中序序列中的第一個結點的lchild域指針和最后一個結點的rchild域指針均指向頭結點。這好比為二叉樹建立了一個雙向線索鏈表,方便從前往后或從后往前對線索二叉樹進行遍歷,如下圖所示。
遍歷的代碼如下:
從這段代碼也可以看出,它等于是一個鏈表的掃描,所以時間復雜度為0(n)。
由于它充分利用了空指針域的空間(這等于節省了空間),又保證了創建時的一次遍歷就可以終生受用前驅后繼的信息(這意味著節省了時間)。所以在實際問題中,如果所用的二叉樹需經常遍歷或查找結點時需要某種遍歷序列中的前驅和后繼,那么采用線索二叉鏈表的存儲結構就是非常不錯的選擇。
(2)先序和后序線索二叉樹
上面給出了建立中序線索二叉樹的代碼,建立先序線索二叉樹和后序線索二叉樹的代碼類似,只需變動線索化改造的代碼段與調用線索化左右子樹遞歸函數的位置。
以圖(a)的二叉樹為例,其先序序列為ABCDF,后序序列為CDBFA,可得出其先序和后序線索二叉樹分別如圖(b)和( c)所示:
如何在先序線索二叉樹中找結點的后繼?如果有左孩子,則左孩子就是其后繼;如果無左孩子但有右孩子,則右孩子就是其后繼;如果為葉結點,則右鏈域直接指示了結點的后繼。
在后序線索二叉樹中找結點的后繼較為復雜,可分3種情況:①若結點x是二叉樹的根,則其后繼為空;②若結點x是其雙親的右孩子,或是其雙親的左孩子且其雙親沒有右子樹,則其后繼即為雙親;③若結點x是其雙親的左孩子,且其雙親有右子樹,則其后繼為雙親的右子樹上按后序遍歷列出的第一個結點。圖( c)中找結點B的后繼無法通過鏈域找到,可見在后序線索二叉樹上找后繼時需知道結點雙親,即需采用帶標志域的三叉鏈表作為存儲結構。
四、樹、森林與二叉樹的轉化
在講樹的存儲結構時,我們提到了樹的孩子兄弟法可以將一棵樹用二叉鏈表進行存儲,所以借助二叉鏈表,樹和二叉樹可以相互進行轉換。從物理結構來看,它們的二叉鏈表也是相同的,只是解釋不太一樣而已。 因此,只要我們設定一定的規則,用二叉樹來表示樹,甚至表示森林都是可以的,森林與二叉樹也可以互相進行轉換。
1、樹轉換為二叉樹
樹轉換為二義樹的規則:每個結點左指針指向它的第一個孩子,右指針指向它在樹中的相鄰右兄弟,這個規則又稱“左孩子右兄弟”。由于根結點沒有兄弟,所以對應的二叉樹沒有右子樹。
樹轉換成二叉樹的畫法:
2、森林轉化為二叉樹
森林是由若干棵樹組成的,所以完全可以理解為,森林中的每一棵樹都是兄弟,可以按照兄弟的處理辦法來操作。
森林轉換成二叉樹的畫法:
至于二叉樹轉換為樹或者二叉樹轉換為森林只不過是上面步驟的逆過程,在此不做贅述。
五、樹和森林的遍歷
1、樹的遍歷
樹的遍歷是指用某種方式訪問樹中的每個結點,且僅訪問一次。主要有兩種方式:
下圖的樹的先根遍歷序列為ABEFCDG,后根遍歷序列為EFBCGDA。
另外,樹也有層次遍歷,與二叉樹的層次遍歷思想基本相同,即按層序依次訪問各結點。
2、森林的遍歷
按照森林和樹相互遞歸的定義,可得到森林的兩種遍歷方法。
●訪問森林中第一棵樹的根結點。
●先序遍歷第一棵樹中根結點的子樹森林。
●先序遍歷除去第一棵樹之后剩余的樹構成的森林。
●后序遍歷森林中第一棵樹的根結點的子樹森林。
●訪問第一棵樹的根結點。
●后序遍歷除去第一棵樹之后剩余的樹構成的森林。
圖5.17的森林的先序遍歷序列為ABCDEFGHI,后序遍歷序列為BCDAFEHIG。
當森林轉換成二叉樹時,其第一棵樹的子樹森林轉換成左子樹,剩余樹的森林轉換成右子樹,可知森林的先序和后序遍歷即為其對應二叉樹的先序和中序遍歷。
樹與二叉樹的應用
一、二叉排序樹
1、定義
二叉排序樹(也稱二叉查找樹)或者是一棵空樹,或者是具有下列特性的二叉樹:
根據二叉排序樹的定義,左子樹結點值<根結點值<右子樹結點值,所以對二叉排序樹進行中序遍歷,可以得到一個遞增的有序序列。例如,下圖所示二叉排序樹的中序遍歷序列為123468。
2、二叉排序樹的常見操作
構造一個二叉樹的結構:
/*二叉樹的二叉鏈表結點結構定義*/ typedef struct BiTNode {int data; //結點數據struct BiTNode *lchild, *rchild; //左右孩子指針 } BiTNode, *BiTree;(1)查找操作
/* 遞歸查找二叉排序樹T中是否存在key 指針f指向T的雙親,其初始調用值為NULL 若查找成功,則指針p指向該數據元素結點,并返回TRUE 否則指針p指向查找路徑上訪問的最后一個結點并返回FALSE */ bool SearchBST(BiTree T, int key, BiTree f, BiTree *p){if(!T){*p = f;return FALSE;}else if(key == T->data){//查找成功*p = T;return TRUE;}else if(key < T->data){return SearchBST(T->lchild, key, T, p); //在左子樹繼續查找}else{return SearchBST(T->rchild, key, T, p); //在右子樹繼續查找} }(2)插入操作
有了二叉排序樹的查找函數,那么所謂的二叉排序樹的插入,其實也就是將關鍵字放到樹中的合適位置而已。
/* 當二叉排序樹T中不存在關鍵字等于key的數據元素時 插入key并返回TRUE,否則返回FALSE */ bool InsertBST(BiTree *T, int key){BiTree p, s;if(!SearchBST(*T, key, NULL, &p)){//查找不成功s = (BiTree)malloc(sizeof(BiTNode));s->data = key;s->lchild = s->rchild = NULL;if(!p){*T = s; //插入s為新的根節點}else if(key < p->data){p->lchild = s; //插入s為左孩子}else{p->rchild = s; //插入s為右孩子}return TRUE;}else{return FALSE; //樹種已有關鍵字相同的結點,不再插入} }有了二叉排序樹的插入代碼,我們要實現二叉排序樹的構建就非常容易了,幾個例子:
int i; int a[10] = {62, 88, 58, 47, 35, 73, 51, 99, 37, 93}; BiTree T = NULL; for(i = 0; i<10; i++){InsertBST(&T, a[i]); }上面的代碼就可以創建一棵下圖這樣的樹。
(3)刪除操作
二叉排序樹的查找和插入都很簡單,但是刪除操作就要復雜一些,此時要刪除的結點有三種情況:
前兩種情況都很簡單,第一種只需刪除該結點不需要做其他操作;第二種刪除后需讓被刪除結點的直接后繼接替它的位置;復雜就復雜在第三種,此時我們需要遍歷得到被刪除結點的直接前驅或者直接后繼來接替它的位置,然后再刪除。
第三種情況如下圖所示:
代碼如下:
/* 若二叉排序樹T中存在關鍵字等于key的數據元素時,則刪除該數據元素結點, 并返回TRUE;否則返回FALSE */ bool DeleteBST(BiTree *T, int key){if(!*T){return FALSE; }else{if(key == (*T)->data){//找到關鍵字等于key的數據元素return Delete(T);}else if(key < (*T) -> data){return DeleteBST((*T) -> lchild, key);}else{return DeleteBST((*T) -> rchild, key);}} }下面是Delete()方法:
/*從二叉排序樹中刪除結點p,并重接它的左或右子樹。*/ bool Delete(BiTree *p){BiTree q, s;if(p->rchild == NULL){//右子樹為空則只需重接它的左子樹q = *p;*p = (*p)->lchild;free(q);}else if((*p)->lchild == NULL){//左子樹為空則只需重接它的右子樹q = *p;*p = (*p)->rchild;free(q);}else{//左右子樹均不空q = *p;s = (*p)->lchild; //先轉左while(s->rchild){//然后向右到盡頭,找待刪結點的前驅q = s;s = s->rchild;}//此時s指向被刪結點的直接前驅,p指向s的父母節點p->data = s->data; //被刪除結點的值替換成它的直接前驅的值if(q != *p){q->rchild = s->lchild; //重接q的右子樹}else{q->lchild = s->lchild; //重接q的左子樹}pree(s);}return TRUE; }3、小結(引申出平衡二叉樹)
二叉排序樹的優點明顯,插入刪除的時間性能比較好。而對于二叉排序樹的查找,走的就是從根結點到要查找的結點的路徑,其比較次數等于給定值的結點在二叉排序樹的層數。極端情況,最少為1次,即根結點就是要找的結點,最多也不會超過樹的深度。也就是說,二叉排序樹的查找性能取決于二叉排序樹的形狀。可問題就在于,二叉排序樹的形狀是不確定的。
例如{62,88,58,47,35,73,51,99,37,93}\{62,88,58,47,35,73,51,99,37,93\}{62,88,58,47,35,73,51,99,37,93}這樣的數組,我們可以構建如下左圖的二叉排序樹。但如果數組元素的次序是從小到大有序,如{35,37,47,51,58,62,73,88,93,99},則二叉排序樹就成了極端的右斜樹,如下面右圖的二叉排序樹:
也就是說,我們希望二叉排序樹是比較平衡的,即其深度與完全二叉樹相同,那么查找的時間復雜也就為O(logn)O(logn)O(logn),近似于折半查找。
不平衡的最壞情況就是像上面右圖的斜樹,查找時間復雜度為O(n)O(n)O(n),這等同于順序查找。
因此,如果我們希望對一個集合按二叉排序樹查找,最好是把它構建成一棵平衡的二叉排序樹。
二、平衡二叉樹
1、定義
平衡二叉樹(Self-Balancing Binary Search Tree 或 Height-Balanced Binary Search Tree)是一種二叉排序樹,其中每一個節點的左子樹和右子樹的高度差至多等于1。
它是一種高度平衡的二叉排序樹。它要么是一棵空樹, 要么它的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的深度之差的絕對值不超過1。我們將二叉樹上結點的左子樹深度減去右子樹深度的值稱為平衡因子BF (Balance Factor) , 那么平衡二叉樹上所有結點的平衡因子只可能是-1、0和1。只要二叉樹上有一個結點的平衡因子的絕對值大于1,則該二叉樹就是不平衡的。
2、平衡二叉樹的查找
在平衡二叉樹上進行查找的過程與二叉排序樹的相同。因此,在查找過程中,與給定值進行比較的關鍵字個數不超過樹的深度。假設以nhn_hnh?表示深度為hhh的平衡樹中含有的最少結點數。顯然,有n0=0,n1=1,n2=2n_0=0,n_1=1,n_2=2n0?=0,n1?=1,n2?=2,并且有nh=nh?1+nh?2+1n_h=n_{h-1}+n_{h-2}+1nh?=nh?1?+nh?2?+1。可以證明,含有nnn個結點的平衡二叉樹的最大深度為O(log2n)O(log2n)O(log2n),因此平衡二叉樹的平均查找長度為O(log2n)O(log2n)O(log2n) 如下圖所示。
3、平衡二叉樹的插入
二叉排序樹保證平衡的基本思想如下:每當在二叉排序樹中插入(或刪除)一個結點時,首先檢查其插入路徑上的結點是否因為此次操作而導致了不平衡。若導致了不平衡,則先找到插入路徑上離插入結點最近的平衡因子的絕對值大于1的結點A,再對以A為根的子樹,在保持二叉排序樹特性的前提下,調整各結點的位置關系,使之重新達到平衡。
注意:每次調整的對象都是最小不平衡子樹,即以插入路徑上離插入結點最近的平衡因子的絕對值大于1的結點作為根的子樹。下圖中的虛線框內為最小不平衡子樹。
平衡二叉樹的插入過程的前半部分與二叉排序樹相同,但在新結點插入后,若造成查找路徑上的某個結點不再平衡,則需要做出相應的調整。可將調整的規律歸納為下列4種情況:
如下圖所示,結點旁的數值代表結點的平衡因子,而用方塊表示相應結點的子樹,下方數值代表該子樹的高度。
注意: LR和RL旋轉時,新結點究竟是插入C的左子樹還是插入C的右子樹不影響旋轉過程,而上圖中是以插入C的左子樹中為例。
舉個例子:
假設關鍵字序列為15,3,7,10,9,8{15,3, 7, 10, 9, 8}15,3,7,10,9,8,通過該序列生成平衡二叉樹的過程如下圖所示。
二叉排序樹還有另外的平衡算法,如紅黑樹(Red Black Tree)等,與平衡二叉樹(AVL樹)相比各有優勢。
三、哈夫曼樹和哈夫曼編碼
1、哈夫曼樹的定義和原理
在許多應用中,樹中結點常常被賦予一個表示某種意義的數值,稱為該結點的權。從樹的根到任意結點的路徑長度(經過的邊數)與該結點上權值的乘積,稱為該結點的帶權路徑長度。樹中所有葉結點的帶權路徑長度之和稱為該樹的帶權路徑長度,記為WPL=∑i=1nwiliWPL = \displaystyle\sum_{i=1}^{n} w_il_iWPL=i=1∑n?wi?li?式中,wiw_iwi?是第i個葉結點所帶的權值,lil_ili?是該葉結點到根結點的路徑長度。
在含有n個帶權葉結點的二叉樹中,其中帶權路徑長度(WPL)最小的二叉樹稱為哈夫曼樹,也稱最優二叉樹。例如,下圖中的3棵二叉樹都有4個葉子結點a, b,c,d,分別帶權7,5,2,4,它們的帶權路徑長度分別為
a. WPL = 7x2 + 5x2 + 2x2 + 4x2 = 36。
b. WPL = 4x2 + 7x3 + 5x3 + 2x1 = 46。
c. WPL = 7x1 + 5x2 + 2x3 + 4x3 = 35。
其中,圖c樹的WPL最小。可以驗證,它恰好為哈夫曼樹。
2、哈夫曼樹的構造
步驟:
看圖就清晰了,如下圖所示:
3、哈夫曼編碼
赫夫曼當前研究這種最優樹的目的是為了解決當年遠距離通信(主要是電報)的數據傳輸的最優化問題。
哈夫曼編碼是一種被廣泛應用而且非常有效的數據壓縮編碼。
比如我們有一段文字內容為“ BADCADFEED”要網絡傳輸給別人,顯然用二進制的數字(0和1)來表示是很自然的想法。我們現在這段文字只有六個字母ABCDEF,那么我們可以用相應的二進制數據表示,如下表所示:
這樣按照固定長度編碼編碼后就是“001000011010000011101100100011”,對方接收時可以按照3位一分來譯碼。如果一篇文章很長,這樣的二進制串也將非常的可怕。而且事實上,不管是英文、中文或是其他語言,字母或漢字的出現頻率是不相同的。
假設六個字母的頻率為A 27,B 8,C 15,D 15,E 30,F 5,合起來正好是
100%。那就意味著,我們完全可以重新按照赫夫曼樹來規劃它們。
下圖左圖為構造赫夫曼樹的過程的權值顯示。右圖為將權值左分支改為0,右分支改為1后的赫夫曼樹。
這棵哈夫曼樹的WPL為:
WPL=2?(15+27+30)+3?15+4?(5+8)=241WPL=2*(15+27+30) + 3*15 + 4*(5+8)=241WPL=2?(15+27+30)+3?15+4?(5+8)=241
此時,我們對這六個字母用其從樹根到葉子所經過路徑的0或1來編碼,可以得到如下表所示這樣的定義。
若沒有一個編碼是另一個編碼的前綴,則稱這樣的編碼為前綴編碼。
我們將文字內容為“ BADCADFEED”再次編碼,對比可以看到結果串變小了。
- 原編碼二進制串: 000011000011101100100011 (共 30個字符)
- 新編碼二進制串: 10100101010111100(共25個字符)
也就是說,我們的數據被壓縮了,節約了大約17%的存儲或傳輸成本。
注意:
0和1究竟是表示左子樹還是右子樹沒有明確規定。左、右孩子結點的順序是任意的,所以構造出的哈夫曼樹并不唯一,但各哈夫曼樹的帶權路徑長度WPL相同且為最優。此外,如有若干權值相同的結點,則構造出的哈夫曼樹更可能不同,但WPL必然相同且是最優的。
附錄
上文鏈接
數據結構:串
下文鏈接
數據結構:圖
專欄
數據結構專欄
參考資料
1、嚴蔚敏、吳偉民:《數據結構(C語言版)》
2、程杰:《大話數據結構》
3、王道論壇:《數據結構考研復習指導》
4、托馬斯·科爾曼等人:《算法導論》
總結
以上是生活随笔為你收集整理的数据结构:树(Tree)【详解】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 各式标签二维码明确采用QR码或DM码,其
- 下一篇: 斐讯路由器k2p a1刷官改只能刷入k2