数据结构——树与二叉树
樹與二叉樹
一、樹的定義:
1、定義:樹(Tree)是n(n>=0)個節點的有限集,n=0時稱為“空樹”。在任意一棵非空樹中:
⒈有且僅有一個特定的稱為根(root)的節點。
⒉當n>1時,其余節點可分為m(m>0)個互不相交的有限集T1、T2……Tm,其中每一個集合本身又是一棵樹,并且稱之為根的子樹(SubTree)。
注意:
⒈n>0時根節點是唯一的,不可能存在多個根節點。
⒉m>0時,子樹個數沒有限制,但一定互不相交。
2、樹的節點的分類:
樹的節點包含一個數據元素以及若干個指向其子樹的分支。節點擁有的子樹個數稱為節點的度(Degree)。度為0的節點稱為葉節點(Leaf);度不為0且不為根節點的節點稱為“內部節點”。
3、節點間關系:
節點的子樹的根稱為該節點的孩子(Child),相應地,該節點稱為孩子的父(Parent,或翻譯成“雙親”)。同一個父節點的孩子之間互稱兄弟(Sibling)。
4、層次:
節點的層次(Level)從根開始定義起,根為第一層,根的孩子為第二層,依次類推。樹中節點的最大層次稱為樹的深度(Depth)或高度。
如果該樹中節點的子樹看成是從左到右有次序的,不能互換的,則稱該樹為有序樹,否則稱為無序樹。
?
二、二叉樹
1、定義:二叉樹(Binary Tree)是n(n>=0)個節點的有限集合,該集合或者為空集(稱為“空二叉樹”),或者由一個根節點和兩棵互不相交的、分別稱為根節點的左子樹和右子樹的二叉樹組成。
定理:任意一棵樹都可以完全等價于一棵二叉樹。
2、二叉樹的特點:
⒈每個節點最多有兩棵子樹,所以二叉樹中不存在度大于2的節點。注意不是“只有”,而是“最多有”,即可以沒有子樹或只有一棵子樹。
⒉左子樹和右子樹有序,不能隨意顛倒。
⒊即使樹中某節點只有一棵子樹,也要區分它是左子樹還是右子樹,不能混淆。
3、二叉樹的基本形態:
二叉樹具有五種基本形態
⒈空二叉樹
⒉只有一個根節點
⒊根節點只有左子樹
⒋根節點只有右子樹
⒌根節點既有左子樹又有右子樹
4、滿二叉樹:
在一棵二叉樹中,如果所有分支節點都存在左子樹和右子樹,并且所有葉子節點都在同一層上,這樣的二叉樹稱為滿二叉樹。
滿二叉樹的特點有:
⒈葉子節點只能出現在最下面一層。出現在其他層就不可能達到平衡。
⒉非葉子節點的度一定是2。
⒊在同樣深度的二叉樹中,滿二叉樹的節點個數最多,葉子節點個數最多。
5、完全二叉樹:
對一棵具有n個節點的二叉樹按層序編號,如果編號為i(1<=i<=n)的節點與同樣深度的滿二叉樹中編號為i的節點在二叉樹中的位置完全相同,則這棵二叉樹稱為完全二叉樹。
需要注意區分滿二叉樹與完全二叉樹,滿二叉樹一定是一棵完全二叉樹,而完全二叉樹不一定是滿的。
判定一棵二叉樹是否是完全二叉樹需要對該二叉樹進行層序編號。
層序編號:以根節點作為起點,從左至右對每一層的節點進行編號。對該層的所有節點編號完畢后再對下一層的節點進行編號。
從這里我們也可以得到一些完全二叉樹的特點:
⒈葉子節點只能出現在最下兩層
⒉最下層的葉子節點一定集中在左部的連續位置
⒊倒數二層,若有葉子節點,一定都在右部連續位置
⒋如果節點度為1,則該節點只有左孩子,不存在只有右子樹的情況
⒌同樣節點數的二叉樹,完全二叉樹的深度最小
注意:同樣節點數的二叉樹,深度最小的二叉樹不一定是完全二叉樹
?
三、二叉樹的性質
關于二叉樹,有一些需要理解并記憶的特性,以便我們更好使用它
1、性質1:在二叉樹的第i層上至多有2^(i-1)個節點(i>=1)。
2、性質2:深度為k的二叉樹至多有2^k-1個節點(k>=1)。注意不是2^(k-1)。
3、性質3:對于任何一棵二叉樹T,如果其葉子節點數為n0,度為2的節點數為n2,則n0=n2+1。
性質3的推導:
二叉樹的節點的度只有3種:0、1、2。我們設其對應的節點數分別為n0、n1、n2,則二叉樹的節點總數為:
Tn=n0+n1+n2
二叉樹的連線數應等于節點個數減1,即
Sn=Tn-1
另一方面,二叉樹的連線數應等于2*n2+n1,即
Sn=2*n2+n1
我們可得到:
n0+n1+n2-1=2*n2+n1
整理可得
n0=n2+1
若事先已知葉子節點個數,使用性質3可以快速計算出一個二叉樹的度為2的節點個數,進而推出度為1的節點個數。
四、二叉樹的存儲
二叉樹在內存中是無法形象存儲的,通常情況下我們可以使用順序存儲和鏈式存儲兩種結構來模擬雙親與孩子的邏輯關系從而存儲二叉樹。
1、順序存儲結構
順序存儲結構就是利用一維數組存儲二叉樹中的節點。將一個完全二叉樹按層序編號的方式,可以得到節點編號與該節點的數據的一一對應關系,這樣我們就可以使用數組來存儲該完全二叉樹了。
對于非完全二叉樹,我們需要先將其補全為完全二叉樹,再進行存儲。后補的節點存儲時數據為空(NULL)。考慮到一種極端情況,若有一棵深度為k的右斜樹,只有k個節點,但根據存儲方式我們需要建立一個2^k-1個存儲單元。因此順序存儲一般不常用,僅用于存儲完全二叉樹。
2、鏈式存儲結構
既然順序存儲適用性不強,我們就要考慮鏈式存儲結構。二叉樹每個節點最多有兩個孩子,所以為它涉及一個數據域和兩個指針域。
lchild︱data︱rchild
其中data是數據域,lchild和rchild都是指針域,分別存放指向左孩子和右孩子的指針。
?
typedef struct BiTNode
{
data_t data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
?
如果需要,我們還可以再增加一個指向其父節點的指針域,這里不再贅述。
?
五、二叉樹的遍歷
二叉樹的遍歷(traversing)是指從根節點出發,按照某種次序依次訪問二叉樹中所有節點,使得每個節點被且僅被訪問一次。
二叉樹的遍歷方式有很多,常用的有前序遍歷、中序遍歷、后序遍歷3種。
1、前序遍歷(PreOrderTraverse)
規則:
⒈若二叉樹為空,則不進行操作,返回
⒉訪問根節點
⒊前序遍歷左子樹
⒋前序遍歷右子樹
2、中序遍歷(InOrderTraverse)
規則:
⒈若二叉樹為空,則不進行操作,返回
⒉中序遍歷左子樹
⒊訪問根節點
⒋中序遍歷右子樹
3、后序遍歷(PostOrderTraverse)
規則:
⒈若二叉樹為空,則不進行操作,返回
⒉后序遍歷左子樹
⒊后序遍歷右子樹
⒋訪問根節點
無論是前序遍歷、中序遍歷還是后序遍歷,都使用到了遞歸的定義方法。
//代碼見附錄
練習:寫出該二叉樹的前序遍歷、中序遍歷、后序遍歷的結果。
?
思考:我們為什么要研究這么多種不同的遍歷方法呢?
答:我們用圖形來表現樹的結構十分直觀和容易理解,但是對于計算機來說,它只能處理循環、分支等問題,也就是說,它只能處理線性問題。以上3種遍歷方法都是人為地將二叉樹變成某個有意義的線性序列,方便計算機處理。
?
練習1:有一棵二叉樹,其前序遍歷結果為ABDC,中序遍歷結果為BDAC,畫出該二叉樹。
練習2:有一棵二叉樹,其中序遍歷結果為ABCDEFG,后序遍歷結果為BDCAFGE,畫出該二叉樹并求其前序遍歷的結果。
練習3:已知一棵二叉樹的前序遍歷結果為ABC,后序遍歷的結果為CBA,畫出該二叉樹。
?
由以上三個練習我們可以得出:
⒈已知前序遍歷和中序遍歷,可以唯一確定一棵二叉樹。
⒉已知中序遍歷和后序遍歷,可以唯一確定一棵二叉樹。
⒊但已知前序遍歷和后序遍歷,無法唯一確定一棵二叉樹。
?
六、二叉樹的建立
有了遍歷方法,我們就可以在遍歷過程中來創建一棵二叉樹。
若我們要創建一棵二叉樹,首先我們要對這棵二叉樹進行擴展,將每個節點都補上一個虛擬的孩子節點,數據為空。這樣我們通過遍歷這個擴展二叉樹得到遍歷序列就可以創建這棵二叉樹了。
示例:創建練習1中的二叉樹
⒈首先對這棵二叉樹進行擴展,補全其所有節點的孩子節點,數據為空(以#代替)
⒉得到該擴展二叉樹的前序遍歷為AB#D##C##
⒊通過前序遍歷的方法,邊遍歷邊創建這棵二叉樹
其實創建二叉樹,也是利用了遞歸的原理,只不過在原來應該打印節點的操作替換為生成節點的操作而已。
當然我們也可以使用中序遍歷和后序遍歷來創建二叉樹。若這棵二叉樹使用中序遍歷來創建,則需要的序列為#B#D#A#C#;若使用后序遍歷來創建,則需要的序列為###DB##CA。
//代碼見附錄
七、二叉樹的應用——赫夫曼樹與赫夫曼編碼
1、赫夫曼樹
赫夫曼(David Huffman,也譯為“哈夫曼”),美國數學家,他在1952年發明了赫夫曼樹與赫夫曼編碼。赫夫曼編碼是當代壓縮和解壓縮技術的基礎。
我們通過一個具體的示例來體會一下什么叫赫夫曼樹(Huffman Tree)。
給定一個成績(0~100),輸出所對應的分數段。我們可以通過以下代碼來實現:
if(a<60)
printf("不及格\n");
else if(a<70)
printf("及格\n");
else if(a<80)
printf("中等\n");
else if(a<90)
printf("良\n");
else
printf("優\n");
粗略來看沒什么問題,但在通常情況下,學生成績大致都分布在“良”和“中等”的分數段,處于兩端的“優”和“不及格”反而很少:
不及格:5%
及格:15%
中等:40%
良:30%
優:10%
在這種情況下,若采用上面的程序,則判斷數量最大的“中等”和“良”分數段時,都需要至少3次的比較才能得出結果,大大影響了程序的執行效率。
如果我們改進算法,將比利最高的“中等”或“良”作為首個判斷條件,則可大大加快程序的執行效率。
if(a<80)
{
if(a<70)
{
if(a<60)
printf("不及格\n");
else
printf("及格\n");
}
else
printf("中等\n");
}
else
{
if(a<90)
printf("良\n");
else
printf("優\n");
}
2、赫夫曼樹的定義
我們可以將上文中的程序畫成二叉樹的形式,每個if分支都作為二叉樹的兩個子樹,將概率標記到父節點到子節點的路徑上。這樣的樹叫做帶權(Weight)樹。
從樹中一個節點到另一個節點之間的邊構成了兩點之間的路徑,路徑上的權值之和叫做路徑長度。樹的路徑長度就是從根節點出發到每一個節點的路徑長度之和。如果考慮帶權的二叉樹,節點的帶權路徑長度等于從該點到根節點的路徑長度乘以該點的權值的乘積。帶權二叉樹的路徑長度為所有葉子節點的帶權路徑長度之和。
針對同一問題,不同的二叉樹畫法路徑長度也不同,其中最小路徑長度的樹就叫做赫夫曼樹。
3、構造一棵赫夫曼樹
構造一棵赫夫曼樹的流程如下:
⒈將所有葉子節點按權值大小有序排列。例如上文的成績占比,我們可以排列成:
A5 E10 B15 D30 C40
⒉取前面兩個最小的葉子節點作為兄弟節點,權值小的做左孩子,權值大的做右孩子,它們兩個共用一個父節點N1,其父節點的權值為兩個葉子節點的權值的和。得到:
N115 B15 D30 C40
⒊重復步驟2,將N1與B作為新的兩個子節點,其父節點為N2,權值為30
N230 D30 C40
⒋重復步驟2,將N2與D作為新的兩個子節點,其父節點為N3,權值為60
C40 N360
⒌重復步驟2,將C與N3作為新的兩個子節點,由于這時所有節點都已構造完畢,因此其父節點就是該二叉樹的根節點。
最后得到構造完成的赫夫曼樹如圖:
?
此時該二叉樹的帶權路徑長度為WPL=40*1+30*2+15*3+10*4+5*4=205。
4、赫夫曼編碼
其實構造赫夫曼樹可不是為了解決程序效率低的問題,赫夫曼樹的主要作用是用來構造赫夫曼編碼,而赫夫曼編碼則是為了解決當年遠距離通信(主要是電報)的數據傳輸最優化問題。
例如:若有一個編碼規則:
A 000
B 001
C 010
D 011
E 100
F 101
可以看出是簡單的以二進制的方式進行編碼。
若有以下報文:BADCADFEED
則按照以上編碼規則編碼后,獲得編碼為:001000011010000011101100100011
對方接收到報文編碼后,按編碼規則3位一分來解碼即可。
事實上,英語中字母的使用頻率是不同的,常用字母(如a e i o u等)使用頻率特別高,而不常用字母(如j v z等)使用頻率就低得多。如果我們能人為減少常見字母的編碼長度,則整個報文長度都會縮短,這樣既方便傳輸又節省了存儲空間。
假設上文的編碼規則中,各字母出現的額頻率如下:
可以看到,如果我們能縮短字母A和E的編碼長度,則我們就可以大大縮短報文長度。這種壓縮效果在文本長度很長時效果會更加明顯。
但是我們看到,所有編碼都是以二進制(0/1)進行編碼的,貿然縮短字母A和E的編碼長度很容易引起混淆,若要設計這種長短不等的編碼,必須使得任意字符的編碼不能是其他字符編碼的前綴。
我們可以使用上文構造赫夫曼樹的方法來構造一個赫夫曼樹:
假設在一個報文中,6個字母的出現頻率如下:
A 27%
B 8%
C 15%
D 15%
E 30%
F 5%
則我們可以構造出如下的赫夫曼樹:
?
構造完畢后,若從根節點出發,規定走向左子樹的邊編碼為0,走向右子樹的邊編碼為1,則我們就可以得到每個字母新的編碼:
A 01
B 1001
C 101
D 00
E 11
F 1000
則使用新的編碼規則后,對報文BADCADFEED獲得新的報文編碼如下:
1001010010101001000111100(新)
001000011010000011101100100011(舊)
大概縮短了17%。
而且我們發現,通過赫夫曼樹構造出的赫夫曼編碼,不會存在因編碼長短不一而混淆的情況。
現代計算機程序中大部分的壓縮文件算法都是基于赫夫曼編碼改進而來的。1977年,Lempel-Ziv在對赫夫曼編碼進行深度研究后,發表了“順序數據壓縮的一個通用算法(A Universal Algorithm for Sequential Data Compression )”的論文,論文中描述的算法被后人稱為LZ77算法。LZ77算法以及后續的衍生算法是現在一些通行的壓縮包模式(ARJ,PKZip,WinZip,LHArc,RAR,GZip,ACE,ZOO,TurboZip,Compress,JAR,7z等)的編碼基礎算法。
總結
以上是生活随笔為你收集整理的数据结构——树与二叉树的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ubuntu下vim的命令及使用方法
- 下一篇: 数据挖掘150道笔试题