一、 樹的定義
- 1. 為什么學習樹?
樹是一對多的邏輯結構,在人機對弈、家族族譜、樹形信息等應用非常廣泛。學習它有很重要的意義。
- 2. 樹的定義
由n(n>=0)個結點的有限集。n=0表示空樹。
n>1 滿足:
(1) 有且只有一個根結點。
(2) 其余結點分成互不相交的m個子集T1、T2、...、Tm,每個集合又都是一顆樹。
注意:1)樹可以是空樹。
2)樹的定義具有遞歸性 (樹中有樹)。
- 3. 結點分類
樹的結點包含一個數據元素及若干指向其子樹的分支。結點擁有的子樹稱為結點的度(Regree)。度為0的結點稱為葉結點(Leaf)或終端結點;度不為0的結點稱為非終端結點或分支節點。除根結點之外,分支節點也將稱為內部結點。樹的度是樹內各結點的度的最大值。如圖所示,因為這棵樹結點的度的最大值是結點D的度,為3,所以樹的度也為3。
問上圖中,樹的度是幾?葉子結點是幾?內部結點為幾?
- 1. 結點間關系
結點的子樹的根稱為該結點的孩子(Child),相應地,該結點稱為孩子的雙父親(Parent)。嗯,為什么不是父或母,叫雙親呢?呵呵,對於結點來說其父母同體,惟一的一個,所以只能把它稱為雙親了。同一個雙親的孩子之間互稱兄弟(Sibling)。結點的祖先是從根到該結點所經分支上的所有結點。所以對於H來說,D、B、A都是它的祖先。反之,以某結點為根的子樹中的任一結點都稱為該結點的子孫。B的子孫有D、G、H、I。如圖所示
五個概念:雙親、兄弟、孩子、祖先、子孫、堂兄弟
- 1. 樹的其他相關概念
結點的層次(Level)從根開始定義起,根為第一層,根的孩子為第二層,若某結點在第一層,則其子樹的根就在第l+1層。其雙親在同一層的終點互為堂兄弟。顯然圖中的D、E、是堂兄弟,而G、H、I、J也是。樹中結點的最大層次稱為樹的深度(Deoth)或高度,當前樹的深度為4。
如果將樹中結點的各子樹看成從左至右是有次序的,不能互換的,則稱該樹為有序樹,否則稱為無序樹。森林(Forest)是m>(m≥0)顆互不相交的樹的集合。對於樹中每個結點而言,其子樹的集合即為森林。對比線性表與樹的結構,它們有很大的不同,如圖所示。
一、 樹的抽象數據類型
相對於線性結構,樹的操作就完全不同了,這里我們給出一些基本和常用操作。
一、 樹的存儲結構
說到存儲結構,就會想到我們前面章節講過的順序存儲和鏈式存儲兩種結構。先來看看順序存儲結構,用一段地址鏈接的存儲單位以此存儲線性的數據元素,這對於線性表來說是很自然的,對於樹這樣一多對這樣的結構呢?、樹中某個結點的孩子可以有多個,這就意味着無論按何種順序將樹中所有的結點存儲到數組中,結點的存儲位置都無法直接反應邏輯關系,你想想看,數據元素挨個的存儲,誰是誰的雙親,誰是誰的孩子呢?簡單的順序存儲結構是不能滿足樹的實現要求的。
不過充分利用順序存儲和鏈式存儲結構的特點,完全可以實現對樹的存儲結構的表示。我們這里要介紹三種不同的表示方法:雙親表示法、孩子表示法、孩子兄弟表示法。
- 1. 雙親表示法
我們人可能因為種種原因沒有孩子,但無論是誰都不可能從石頭里蹦出了的,孫悟空顯然不能算是人,所以人一定會有父母。樹這種結構也不例外,除了根結點處,其余每個結點,他不一定有孩子,但一定有且僅有一個雙親。我們假設以一組連續空間存儲樹的結點,同時在每個結點中附設一個指示器指它的雙親在哪里。它的節點結構如圖所示。
有了這樣的結構定義,我們就可以來實現雙親表示法了。由於根結點是沒有雙親的,所以我們約定根結點的位置域設置為—1,這也意味着,我們所有的結點都存有它雙親的位置。如圖中的樹結構和下表中樹雙親表示所示。
這樣的存儲結構,我們可以根據結點的parent指針很容易找到它的雙親結點,所用的時間復雜度為0(1),直到parent為—1時,表示找到了樹結點的根,可如果我們要知道結點的孩子是什么,對不起,請遍歷整個結構才行。
這真是麻煩,能不能改進一下呢?
當然可以,我們增加一個結點最左邊的孩子的域,不妨叫他長子域,這樣就可以很容易得到結點的孩子。如果沒有孩子的結點,這個長子域就設置為—1,如圖所示
對於0個域和1個孩子結點來說,這樣的結構是解決了要找結點孩子的問題了。設置是有2個孩子,知道了長子是誰,另一個當然就是次子了。
另外一個問題場景我們很關注各兄弟之間的關系,雙親表示法無法體現這樣的關系,那我們怎么辦?恩,可以增加一個右兄弟域來體現兄弟關系,也就是說,每一個結點如果它存在有兄弟,則記錄下右兄弟的下標,同樣的,如果右兄弟不存在,則賦值為—1,如表6-4-4所示。
但如果結點的孩子很多,超過了2個,我們又關注結點的雙親,又關注結點的孩子,還關注結點的兄弟,而且對時間遍歷要求還比較高,那么我們還可以把此結構擴展為有雙親域、長子域、再有右兄弟域。存儲結構的設計是一個非常靈活的過程。一個存儲結構設計得是否合理,取決於基於該存儲結構的運算是否適合,是否方便,時間復雜度好不好等。注意也不是越多越好,有需要時間再設計相應的結構就像在好聽的音樂,不停反復聽上千遍也會膩味,再好看的電影一段時間反復看上百遍,也會無趣,你們說是嗎?
- 1. 孩子表示法
換一種完全不同的考慮方法。由於樹中每個結點可能有多顆子樹,可以考慮用多重鏈表,即每個結點有多個指針域,其中每個指針指向一棵樹的根結點,我們把這種方法叫做多重鏈表表示法。不過,樹的每個結點的度,也就是它的孩子個數是不同的。所以,可以設計兩種方案來解決。
方案一
一種指針域的個數就等於樹的度。復習一下,樹的度是樹各個結點度的最大值,其結構如圖所示。
其中data是數據域。child1到childd 是指針域,用來指向該結點的孩子結點。
對於圖中的樹來說,樹的度是3,所以我們的指針域的個數是3,這種方法實現如圖中所示。
這種方法對於樹中各結點的度相差很大時,顯然是很浪費空間的,因為有很多的結點,它的指針域都是空的。不過如果樹的各結點度相差很小時,那就意味着開辟的空間被充分利用了,這時存儲結構的缺點反而變成了優點。
既然很多指針域都可能為空,為什么不按需分配空間呢。於是我們有了第二種方案。
方案二
第二種方案每個結點指針域的個數等於該結點的度,我們專門取一個位置來存儲結點指針的個數,其結構如表所示。
其中data為數據域,degree為度域,也就是存儲該結點的孩子結點的個數。child1到childd為指針域,指向該結點的各個孩子的結點。對於上面圖的樹來說,這種方法實現如圖所示。
這種方法克服了浪費空間的缺點,對空間利用率是很高了,但由於各個節點的鏈表是不相同的結構,加上要維護結點的度的數值,在運算上就會帶來時間上的損耗。能否有更好的方法既可以減少空指針的浪費又能使結點結構相同。仔細觀察,我們為了要遍歷整棵樹,把每個結點放到一個順序存儲結構的數組中是合理的,但每個結點的孩子有多少是不確定的,所以我們在對每個結點的孩子建立一個單鏈表體現它們的關系。這就是我們要講的孩子表示法,具體辦法是,把每個結點的孩子結點排列起來,以單鏈表做存儲結構,則n個結點有n個孩子鏈表,如果是葉子結點則此單鏈表為空。然后n個頭指針又組成一個線性表,采用順序存儲結構,存放進一個一維數組中,如圖所示。
為此,設置兩種結點結構,一個是孩子鏈表的孩子節點,如表所示
其中child是數據域,用來存儲某個結點在表頭數組中的下方。next是指針域,用來存儲指向某結點的下一個孩子結點指針。另一個表頭數組的表頭結點,如表所示。
其中data是數據域,存儲某結點的數據信息,firstchild是頭指針域,存儲該結點的孩子鏈表的頭指針。
一下是我們的孩子表示法的結構定義代碼。
這樣的結構對於我們要查找某個結點的某個孩子或者找某個結點的兄弟,,只需要查找這個結點的孩子單鏈表即可,對於遍歷整棵樹也是很方便的,堆頭結點的數組循環即可
但是,這也存在着問題,我如何知道某個結點的雙親是誰呢?比較麻煩,需要整個樹的遍歷才行,難道就不可以把雙親表示法和孩子表示法綜合一下嗎?當然是可以的。如圖所示
我們把這種方法稱為雙親孩子表示法,應該算是孩子表示法的改進。至於這個表示法的具體結構定義,這里就略過,留給同學們自己去設計了。
- 1. 孩子兄弟表示法
剛才我們分別從雙親的角度和從孩子的角度研究樹的存儲結構,如果我們從樹結點的兄弟的角度又如何呢?當然,對於樹這樣的層級結構來說,只研究結點的兄弟是不行的,我們觀察后發現,任意一棵樹,他的結點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我們設置兩個指針,分別指向該結點的第一個孩子和此結點的右兄弟。
結點結構如圖所示
其中data是數據域,firstchild 為指針域,存儲該結點的第一個孩子結點的存儲地址,rightsib 是指針域,存儲該結點的右兄弟結點的存儲地址。結構定義代碼如下
對於下圖的樹來說,這種方法實現的示意圖。
這種表示法,給查找某個結點的某個孩子帶來了方便,只需要通過fistchild 查找此結點的長子,然后在通過長子的結點的rightsib找到它的二弟,接着一直下去,直到找到具體的孩子。當然如果想找到某個結點的雙親,這個表示法是有缺陷的,那咋么辦呢?呵呵,對如果真的有必要,完全可以在增加一個parent指針域來解決快速查找雙親問題,這里就不再細談了。其實這個表示法的最大好處是把一顆復雜的樹變成一顆二叉樹。我們把圖變變形就成了下圖這個樣子。
這樣就可以從分利用二叉樹的特性和算法來處理這棵樹了。嗯?有人問,二叉樹是什么?哈哈,別急,這正是下一個單元重點講的內容。
代碼
#include "stdio.h" #define MAX 20 struct slist{ int arr[MAX]; int len; }s; void fun1() { int g,sh,b;//代表個位,十位,百味 int count=0,i;//用於順序表的數組計數 for(i=100;i<1000;i++) { g=i%10; sh=(i-g)%100/10; b=i/100; if((g*g*g+sh*sh*sh+b*b*b)==i) { s.arr[count++]=i; s.len++; } } } void fun2() { int i,j,sum=0; for(i=1;i<=10000;i++) { sum=0; for(j=i-1;j>0;j--) { if(i%j==0) { sum+=j; } } if(sum==i) { s.arr[s.len]=i; s.len++; } } } void print() { int i; printf("線性表的元素有:\n"); for(i=0;i<s.len;i++) { printf("%d ",s.arr[i]); } } void maoplist() { int tem,i,j,flag; for(i=1;i<s.len;i++) { flag=-1; for(j=0;j<s.len-i;j++) { if(s.arr[j]>s.arr[j+1]) { tem=s.arr[j]; s.arr[j]=s.arr[j+1]; s.arr[j+1]=tem; flag=0; } } if(flag==-1) { break;//提前結束 } } } void zhebanlist() { int tem,mid,low,high; low=0; high=s.len-1; printf("請輸入要查找的數:"); scanf("%d",&tem); while(low<=high) { mid=(low+high)/2; if(s.arr[mid]>tem) { high=mid-1; }else if(s.arr[mid]<tem) { low=mid+1; }else{ printf("查找成功,該元素在第%d個位置\n",mid); return ; } } printf("查找失敗!\n"); } void main() { int bh; s.len=0; for(;;) { printf("\n請輸入您的選擇:"); scanf("%d",&bh); switch(bh) { case 1: fun1(); print(); break; case 2: fun2(); print(); break; case 3: maoplist(); print(); break; case 4: zhebanlist(); break; default : printf("您輸入的功能編號有誤!\n"); break; } } }