樹是數據結構中很重要的一環,更是C/C++高手的摯愛。
今天就來討論下數據結構中的樹。
先梳理下關於樹一些基本概念。
樹的基本概念
(1)樹(Tree)的概念:樹是一種遞歸定義的數據結構,是一種重要的非線性數據結構。樹可以是一棵空樹,它沒有任何的結點;也可以是一棵非空樹,至少含有一個結點。
(2)根(Root):有且僅有一個結點的非空樹,那個結點就是根。
(3)子樹(Subtree):在一棵非空樹中,除根外,其余所有結點可以分為m(m≥0)個互不相交的集合。每個集合本身又是一棵樹,稱為根的子樹。
(4)結點(Node):表示樹中的元素及若干指向其子樹的分支。
(5)結點的度(Degree):一個結點擁有的子樹數目稱為該結點的度。
(6)葉子結點(Leaf):度為0的結點。
(7)孩子(Child):結點子樹的根稱為該結點的孩子。
(8)雙親(Parents):孩子結點的上層結點叫該結點的雙親。
(9)兄弟(Sibling):同一雙親的孩子。
(10)樹的度:一棵樹中最大的結點度數。
(11)結點的層次(Level):從根結點開始定義根為第一層,它的孩子為第二層,依此類推。
(12)深度(Depth):樹中結點最大層次的值。
(13)有序樹:樹中的各子樹自左向右有序的稱為有序樹。
(14)無序樹:樹中的各子樹自左向右無序的稱為無序樹。
(15)森林(Forest):是m(m≥0)棵互不相交的樹的集合。
(16)祖先:是指從根結點到該結點之間所有的結點。
圖解說明
樹
如圖所示:
A是根結點,A結點的度是3,D結點的度是3;因為3是結點的度的最大值,所以這棵樹的度是3;E、G、H、I、K、L和M是葉子結點。A在樹的第一層,B、C、D在樹的第二層,E、F、G、H、I、J在樹的第三層,K、L、M在樹的第四層;樹的深度是4。樹從左往右是有序的,這是一棵有序樹;E結點的祖先是A、B。
二叉樹(Binary Tree)
概念:二叉樹又叫二分樹,它的特點是每個結點最多只有二棵子樹,也就是二叉樹中沒有度大於2的結點。二叉樹的子樹有左右之分,嚴格區分左孩子、右孩子,其次序不能顛倒。
二叉樹有5種形態:
(1)空二叉樹。
(2)只有一個根結點。
(3)只有根結點和左子樹。
(4)只有根結點和右子樹。
(5)有根結點和左、右子樹。
滿二叉樹
概念:一棵深度為k且有2k-1個結點的二叉樹稱為滿二叉樹。
完全二叉樹
概念:如果有深度為k的,有n個結點的二叉樹,當且僅當其每一個結點都與深度為k的滿二叉樹中編號從1到n的結點一一對應時,稱為完全二叉樹。
二叉樹的性質
(1)在二叉樹的第i層上至多有2i-1(i ≥1)。
(2)深度為k的二叉樹至多有2k-1個結點(k≥1)。
(3)對任何一棵二叉樹,如果n0、n1、n2分別表示度數為0、1、2的結點樹,則有n0=n2+1。
(4)具有n個結點的完全二叉樹的深度為log2n + 1。
(5)如果對一棵有n個結點的完全二叉樹,則對任一結點i(1≤i≤n)有:
<1> 如果i=1,則結點i是二叉樹的根結點,無雙親;如果i>1,則雙親PARENT(i)是結點i/2。
<2> 如果2i>n,則結點i無左孩子(結點i為葉子結點);否則其左孩子LCHILD(i)結點2i。
<3> 如果2i+1>n,則結點i無右孩子;否則其右孩子RCHILD(i)是結點2i+1。
二叉樹的存儲
(1)順序存儲結構
順序存儲結構僅適用於完全二叉樹的存儲,就是把完全二叉樹從上到下、從左到右的順序存儲到一塊連續的存儲空間中。一般存儲在一維數組中。
上圖完全二叉樹的順序存儲結構如下:
(2)鏈式存儲結構
鏈式存儲結構有二叉樹鏈表結構各三叉鏈表存儲結構。一般都是使用二叉鏈表進行存儲。
二叉鏈表結點結構定義如下:
typedef struct bitnode { int num; struct bitnode *lchild; struct bitnode *rchild; }TREENODE;
鏈式存儲結構
二叉樹的遍歷
(1)先序遍歷(先根遍歷)
1. 訪問根結點
2. 先序遍歷左子樹
3. 先序遍歷右子樹
(2)中序遍歷(中根遍歷)
1. 中序遍歷左子樹
2. 訪問根結點
3. 中序遍歷右子樹
(3)后序遍歷(后根遍歷)
1. 后序遍歷左子樹
2. 后序遍歷右子樹
3. 訪問根結點
對於上圖遍歷結果:
先序遍歷:ABDECFG
中序遍歷:DBEAFCG
后序遍歷:DEBFGCA
線索二叉樹
線索二叉樹的引出:為了解決二叉樹遍歷問題。對於上面的中序遍歷,假如現在已經中序遍歷到B結點,問題是,在編程時,怎么知道從B接下來要往哪個結點遍歷,即它的直接后驅結點是誰?同時B又是從哪個結點遍歷來的,即它的前驅結點是誰?在二叉樹的存儲結構中,知道二叉樹的結點是用二叉鏈表進行存儲的,它有兩個指向左右孩子結點的指針。B結點左指針指向D,右指針指向E。但現在已經中序遍歷到了B,而B的兩個指針已經用了,為了能在B結點指出它的前驅結點和后繼結點,需要增加兩個標志位,一個指出前驅,一個指出后繼結點。而對於E這種擁有空指針域的結點,同時可以用空指針域和標志位來標識前驅后后繼結點。
概念:利用二叉鏈表中的空指針域存放指向結點在某種遍歷次序下的前驅結點和后繼結點的指針,這種附加信息的指針稱為“線索”,如上圖的D、E、F、G有空指針域,可以用來指向前驅結點和后繼結點;而A、B、C已經沒有了空指針域,這時這要靠增加的兩個標志位來說明前驅結點和后繼結點。這種加上了線索的二叉鏈表稱為線索鏈表,相應的二叉樹稱為為線索二叉樹(Threaded Binary Tree)。
typedef struct bitnode { int num; struct bitnode *lchild; struct bitnode *rchild; int ltag; //0表示lchild指向左孩子,1表示lchild指向前驅 int rtag; //0表示rchild指向右孩子,1表示rchild指向后繼}TREENODE;
根據線索性質的不同,線索二叉樹分為先序二叉樹、中序二叉樹和后序二叉樹。
現在解決上面提到中序遍歷到B結點時,此時B->ltag=1,表示左孩子是它的前驅結點,即D結點;B->rtag=1,表示右孩子是它的后繼結點,即E結點。
接着假如此時中序遍歷到了E,則E->lchild=B,E->ltag=1,表示B是E的前驅結點; E->rchild=A,E->rtag=1,表示A是E的后繼結點。
接着假如此時中序遍歷到了A結點,這這里A結點的情況是:它沒有空指針域,它的兩個指針已經用來指向了兩個孩子,而兩個孩子都不是它的前驅和后繼,所以它的兩個標志位都為0。可以看到,對於兩個標志位都為0的情況,無法直接找到它的前驅結點和后繼結點。這時需要利用中序遍歷的規律進行推導:A的前驅結點必定是遍歷它的左孩子時的最后一個終端結點,即從左孩子B一直向右,直到找到最后一個右標志位為“1”的結點,即E結點; 同樣,A的后繼結點必定是遍歷它的右孩子時的第一個終端結點,即從右孩子C一直向左,直到找到第一個標志位為“1”的結點,即F結點。