1. 程式人生 > >二叉樹及其實現(基礎版)

二叉樹及其實現(基礎版)

除了 一覽 display ros return 路徑 動態操作 spl signature

前言常見的數據結構都有指針和數組兩種實現方式這篇先介紹指針實現數組實現在後續文章裏會講到

(長文預警!)

說完了一般的樹,我們再來看看二叉樹,這是一種很典型的樹,它的所有節點度數都不超過2,最多只有兩個孩子。這是一種特例,但是後面我們會看到在保證有序性和有根性之後,它卻足以描述所有的樹。每個節點的出度最多為2,在之前對所有節點按照深度劃分的等價類,從規模上看就構成了一個公比為2的等比數列,相應地,深度為k第k層)的節點,最多有2^k個。那麽對於含n個節點、高度為h的二叉樹中(這裏再多說一句,高度指的是:除了根節點,下面有幾層高度就是幾,回想一下,空樹高度是-1,一個根節點高度為0。),滿足這樣一個條件:

h<n<2^h+1

這個性質很好證明。對於上界的情況,樹的每一層都是滿的,所以從根到第n層累加,總數=1+2+4+…+2^n=2^(n+1)-1,也就是右半部分。而下界情況,根據定義每層至少有一個節點,所以一共h+1個,這種情況下,樹就退化為一條單鏈。

具體來說一下上界的情況,在這個時候節點個數n=2^h-1是一顆滿樹,那每一層(每一個等價類)都會到達飽和狀態。它的橫向上的寬度與在縱向上的高度是呈指數關系的——h=log(W),高度會增長的很慢。

技術分享圖片

相關實現

討論了二叉樹的基本信息後,就該進入正題了,Talk is cheap,show me the code。現在來談談怎麽在計算機中實現一棵二叉樹。談一個東西的實現不能脫離實際背景,不然就成了空中樓閣,二叉樹也有很多種,比如最大堆,最小堆,優先隊列,搜索樹,這裏給出一個其中一個二叉樹的具體例子。首先需要知道的是,二叉樹的一個重要應用是他們在查找(搜索)中的使用,那為什麽查找往往要依靠樹形結構呢?這是很自然的一個問題,前面的文章中我們說過,樹結構對靜態和動態操作的支持都是十分迅速的,因此是這種高效性使得樹結構成為了搜索的“天選之人”。這也告訴我們,如果自身很強,那麽遇到機會時才有可能把握住,機會是留給有準備的人(突然雞湯2333)。

假設樹中的每個節點內部存了一個值,任何復雜的值都可以,不過這裏為了簡單起見,讓它們都是int型,同時假設他們是互異的,以後我們再處理有重復值的情形。我們要進行一些操作的前提是:知道規則,有了規則,操作的步驟就一目了然了,這是老師在課上反復強調的。那搜索樹的規則就是——對於每個節點X(作為根),左子樹中存的所有值都小於X存的值;右子樹的所有值都大於X的值。也就是從小到大依次是左-根-右,就像這樣:

技術分享圖片

這個性質要引起註意,這意味著樹中的所有元素可以用統一的方式排序。為什麽要這樣說?因為樹是遞歸構造的,其中每一棵子樹中都滿足這個性質,那我們的排序過程就可以逐步分解到最小的子樹中,每個被分解的部分和原來的總體都是“性質相同的子問題”這樣一種關系,所以可以用統一的方法,從最小的子問題推而廣之,直至解決整個問題。

現在具體說說怎麽實現他們,由於樹是遞歸定義的,所以通常遞歸地編寫操作函數,前面說了,二叉樹的平均深度是多少來著?忘的話往上翻翻。由於平均深度增長地很慢,所以我們不必擔心棧空間被用盡(emmm這怎麽突然提到棧了,忘了的話去前面講棧的文章裏翻翻)。先給出一般性的結點聲明

1 struct BinNode;
2 typedef struct BinNode *Position;         //只需要拿到某個單結點時用它表示
3 typedef struct BinNode *SearchTree;       //對整棵樹操作時,用它表示返回類型
4 
5 struct BinNode{
6     int Value;
7     SearchTree Left,Right;
8 };

這裏又遇到和鏈表那裏一樣,用兩個typedef替換同一個類型的情況了,稍後我們會看到如此邏輯分層的優勢,它便於我們在大腦中構建一個清晰的搜索樹ADT模型。現在只要知道他們表示的都是一個指向二叉樹節點的指針就好了,只是所指示的側重有細微的不同。按照慣例,先說初始化的操作,然後說查找元素,最後就是重頭戲——插入和刪除了。每種結構都按這個順序來講解看似單調乏味,但這的確是我們從0搭建一個結構的必由之路,從簡到繁,自下而上這也符合人類的認知規律。

1 SearchTree MakeEmpty(SearchTree T){
2     if (T){
3         MakeEmpty(T->Left);
4         MakeEmpty(T->Right);
5         free(T);
6     }
7     return NULL;
8 }

給這個函數輸入一個節點作為根,然後在它不為空的情況下,逐層遞歸地銷毀它以下的所有子樹。註意到了吧,這裏用的是“SearchTree”來標識,因為置空後要返回的是一個根節點,實際上從整體理解,是以返回值為根的一整棵樹(當然這裏是空樹)。

再說查找,它要返回的是一個指針,指向我們所查的值所在的結點(既然只返回一個單一結點指針,自然用Position做返回類型更清晰),沒有的話就NULL。樹的結構使得這種操作很簡單,我們先分析一下大體策略:如果T是NULL,也就是走到某一個葉子結點仍然沒有找到,那就返回NULL;如果T中存的值是要查找的X,那麽返回T的地址;如果既沒找到,但也沒走到末尾(葉結點)時,就按照X和根節點的大小關系來逐層遞歸左或右子樹:如果比根小,左邊,否則右邊,這就很類似二分查找的思想。

1 Position Find(int X,SearchTree T){
2     if(T == NULL) return NULL;      //如果走到葉子還沒找到,返回空
3     if (X < T->Value)  return Find(X, T->Left); //如果給定值比根小,往左邊找
4     else if(X > T->Value) return Find(X , T->Right);//比根大就往右找
5     else return T;  //這種情況就是某時X==T->Value,正好命中的情況
6 }

接著來說兩種Find的具體情況,分別是找最小最大值,這種遞歸寫法是很自然的,以至於深受喜愛,不過遞歸調用過多的話會占用大量資源,這是一個弊端,所以叠代和遞歸兩種方法都要熟稔於心。因此,我們用兩種方法編寫,FindMin(遞歸)和FindMax(叠代)。

1 Position FindMin(SearchTree T){
2     if(T == NULL) return NULL;         //同上
3     else if (T->Left==NULL) return  T;   //左子樹空,意味著沒有比它更小的值了,直接返回地址
4     else return FindMin(T->Left); //如果上面兩個情況都不符合,接著往左找
5 }

1 Position FindMax(SearchTree T) {
2     if (T!=NULL)         //沒有走到葉結點時尋找
3         while (T->Right!=NULL)    //右邊還有子樹時一直往右走
4             T=T->Right;
5     return T;             //這個return包含了兩種情形,如果傳入的是葉子,自動返回NULL,如果找到最右邊了,返回對應地址
6 }

同理,查找最小值的如果用叠代來寫,就是完全對稱的。

1 SearchTree FindMinByLoop(SearchTree T) {
2     if(T)
3         while (T->Left)
4             T=T->Left;
5     return  T;
6 }

這裏要註意時如何處理空樹這種退化情形的,一定要小心。

接著就是兩大重頭戲——插入和刪除,我們慢慢討論,對於插入一個數X來講,從概念上很好理解:先用find查一下,看是不是已經存在,有的話就不用做什麽了(或者做一些修改)。如果沒有,就把X插到遍歷路徑的末尾。

比如對於這樣一棵樹

技術分享圖片

我們要插入66這個數,那麽就應該按下面這個路徑放置。

技術分享圖片

 1 SearchTree Insert(SearchTree T,int X) {
 2     if(!T){             //這是應對初始情況,空樹
 3         T=(SearchTree)malloc(sizeof(struct BinNode));
 4         T->Value=X;
 5         T->Left=T->Right=NULL;  //底部封口
 6     }
 7     //在一棵現成的樹裏插入,二分查找
 8     else if (X < T->Value) T->Left=Insert(T->Left, X);
 9     else if (X > T->Value) T->Right=Insert(T->Right, X);
10     //X==T->Value的情況什麽也不用做
11     return T;
12 }

而正如許多數據結構一樣,最困難的是刪除,因為這會涉及到好多種情況,我們都需要將其考慮在內。

  1. 節點是一片葉子
  2. 節點有一個兒子
  3. 節點有兩個兒子

分類討論,1.葉子的話就直接刪除。

2.只有一個兒子的話,就可以在它的父節點調整指針時繞過該節點後被刪除。

技術分享圖片

這棵樹中,刪除4

技術分享圖片

從父節點直接繞過去,bypass

技術分享圖片

而有兩個兒子的話情況就復雜了,一般來說是用它右子樹下最小的數據來代替該節點數據並遞歸刪除。因為右子樹下面最小的節點不可能有左兒子,所以第二次delete就更容易了。

總結起來就是:

search for v

if v is a leaf

delete leaf v

else if v has 1 child

bypass v

else replace v with successor

代碼如下

 1 SearchTree Delete(int X,SearchTree T) {
 2     Position TempCell;
 3     if (T==NULL)
 4          printf("Element not found\n");
 5     //search for Value
 6     
 7     else if(X<T->Value) T->Left=Delete(X, T->Left);
 8     else if(X<T->Value) T->Right=Delete(X, T->Right);
 9     //找到給定的X了,開始分類討論
10     else if(T->Left && T->Right){   //有兩個兒子的情況
11         TempCell=FindMin(T->Right);     //找到右子樹下最小的數據
12         T->Value=TempCell->Value;   //Replace
13         T->Right=Delete(T->Value, T->Right);  //遞歸刪除
14     }
15     else{       //1個兒子or葉子的情況,可以統一起來,操作邏輯是一致的
16         TempCell=T;
17         if (T->Left==NULL)  T=T->Right;       //只有右孩子,就把父節點直接連到右邊
18         else if (T->Right==NULL) T=T->Left;  //只有左孩子,就把父節點直接連到左邊
19     free(TempCell);
20     }
21     return T;
22 }

這裏0 or 1 children的情況在實現的時候統一寫了不用再討論他們的差別了因為即使是葉子進入分支後也就相當於原來的T=T->Right效果變成了T=NULL,同樣達到了目的。如果我們一開始來寫,可能會多寫一條分支判斷是否為葉子,這樣代碼就顯得冗余了,也正因此我們需要慢慢品味上面這種寫法的精妙之處。

小零件我們都寫好了,下面就需要把他們粘合起來,形成一個有機的系統了。怎麽粘合才能讓他們各個部分有序而協調地運轉呢?先走誰後走誰的問題就涉及到了遍歷規則了,所以下面我們來討論樹的遍歷。

遍歷

樹的主要遍歷方式有四種:Pre/In/Post orderLevel order,前者對應著深搜,後者對應廣搜。層序遍歷是按照離根節點的距離由遠及近地訪問。與層序不同,其他三種都是根據對根節點的訪問次序來劃分的。如果是先於左右子樹,那就是Preorder,如果是介於左右子樹之間,就是Inorder,如果是位於左右子樹遍歷之後,就是Postorder

技術分享圖片

通過這張圖我們來對比記憶,下面詳細說明每種方法。

技術分享圖片

這是層序遍歷,結果是ADBFHCEG。它的思想是用一個隊列來維護,對於每個節點進行如下操作:

1.將這個節點入隊

2.打印後出隊

3.接著把該節點所有孩子按順序入隊

然後對所有孩子重復第23步,很顯然,這是用遞歸輕松解決的。

技術分享圖片

這是先序遍歷,而這條紅色的線有一個名字叫Euler tourpreorder的結果就是GDBFAHEIC。

postorder的話,就是FBDEIHCAG,inorder的結果是:DFBGEHIAC

1 void PreOrder(SearchTree T) {
2     if(T){                  //如果這顆子樹非空,就打印,否則把控制權還給上級
3     printf("%d ",T->Value);
4     PreOrder(T->Left);
5     PreOrder(T->Right);
6     }
7 }

中序的情況類似

1 void InOrder(SearchTree T){
2     if(T){
3         InOrder(T->Left);
4         printf("%d ",T->Value);
5         InOrder(T->Right);
6     }
7 }

後序同理,只是打印順序略有區別,這裏不再贅述。不過我要說的是,後序有一個妙用,就是計算樹的高度,當然其他方式也可以,不過後序最符合人的思維習慣。

1 int Height(SearchTree T){
2     //下面這兩句都是根據定義得出的
3     if(!T) return -1;
4     else return 1+max(Height(T->Left), Height(T->Right));
5 }

而層序遍歷就稍微復雜一些了,因為它涉及到如何判斷“某個節點是否被訪問”以及“如何按照遠近關系來行進”,這就需要我們為其指定一個優先級,故需要隊列。

 1 void LevelOrder(SearchTree r) {
 2     SearchTree current=r;     //為了不修改根節點,新建一個指針作為光標
 3     queue<SearchTree> q;
 4     q.push(current);        //把當前(根)節點入隊
 5     //以下是廣搜的核心
 6     while (!q.empty()) {        //隊列非空時進行遍歷
 7         current=q.front();
 8         printf("%d ",current->Value);
 9         q.pop();            //打印完則出隊
10         if (current->Left)            //依次查看當前節點是否有後繼,有的話重復上述入隊過程,left,right or both
11             q.push(current->Left);
12         if (current->Right)
13             q.push(current->Right);
14     }
15 }

最後看一個具體的總實現,給出演示程序。

以這個為例,插入17,刪除72

技術分享圖片

就分別變成

技術分享圖片

技術分享圖片

  1 //出於布局合理的考慮,把主函數放在中間。
  2 #include <cstdio>
  3 #include <cstdlib>
  4 #include <ctime>
  5 #include <queue>
  6 using namespace std;
  7 
  8 struct BinNode;
  9 typedef struct BinNode *SearchTree;
 10 typedef struct BinNode *Position;
 11 struct BinNode{
 12     int Value;
 13     SearchTree Left,Right;
 14 };
 15 
 16 SearchTree root=NULL;
 17 
 18 // Function signature
 19 SearchTree Insert(SearchTree T,int X);
 20 SearchTree Delete(int X,SearchTree T);
 21 int Height(SearchTree T);
 22 void PreOrder(SearchTree T);
 23 void InOrder(SearchTree T);
 24 void LevelOrder(SearchTree T);
 25 void DisplayInfo(SearchTree t);
 26 Position FindMax(SearchTree T);
 27 Position FindMix(SearchTree T);
 28 //Entrance
 29 int main(){
 30     int n;
 31     printf("Could you tell me what the tree looks like?(0 to complete)\n");
 32     while (scanf("%d",&n) && n)
 33         root=Insert(root, n);
 34     printf("\n");
 35     DisplayInfo(root);
 36     printf("Which guys will be pushed?\n"); scanf("%d",&n);
 37     root=Insert(root, n); DisplayInfo(root);
 38     printf("Which value do you desire to remove?\n");  scanf("%d",&n);
 39     root=Delete(n, root);
 40     DisplayInfo(root);  printf("\n");
 41 }
 42 
 43 //接口內部一覽
 44 SearchTree MakeEmpty(SearchTree T){
 45     if (T){
 46         MakeEmpty(T->Left);
 47         MakeEmpty(T->Right);
 48         free(T);
 49     }
 50     return NULL;
 51 }
 52 
 53 Position Find(int X,SearchTree T){
 54     if(T == NULL) return NULL;      //如果走到葉子還沒找到,返回空
 55     if (X < T->Value)  return Find(X, T->Left); //如果給定值比根小,往左邊找
 56     else if(X > T->Value) return Find(X , T->Right);//比根大就往右找
 57     else return T;  //這種情況就是某時X==T->Value,正好命中的情況
 58 }
 59 
 60 Position FindMin(SearchTree T){
 61     if(T == NULL) return NULL;         //同上
 62     else if (T->Left==NULL) return  T;   //左子樹空,意味著沒有比它更小的值了,直接返回地址
 63     else return FindMin(T->Left); //如果上面兩個情況都不符合,接著往左找
 64 }
 65 
 66 void DisplayInfo(SearchTree t){
 67     printf("\nCurrently\nPre-order is :");
 68     PreOrder(t);   printf("\n");
 69     printf("In-order is :");
 70     InOrder(t);    printf("\n");
 71     printf("Level-order is :");
 72     LevelOrder(t); printf("\n");
 73     printf("Height is %d\n",Height(root));
 74     printf("The min is: %d\n",FindMin(root)->Value);
 75     printf("The max is: %d\n",FindMax(root)->Value);
 76 }
 77 
 78 int Height(SearchTree T){
 79     //這兩句都是根據定義得出的
 80     if(!T) return -1;
 81     else return 1+max(Height(T->Left), Height(T->Right));
 82 }
 83 SearchTree FindMinByLoop(SearchTree T) {
 84     if(T)
 85         while (T->Left)
 86             T=T->Left;
 87     return  T;
 88 }
 89 
 90 Position FindMax(SearchTree T) {
 91     if (T!=NULL)         //沒有走到葉結點時尋找
 92         while (T->Right!=NULL)    //右邊還有子樹時一直往右走
 93             T=T->Right;
 94     return T;             //這個return包含了兩種情形,如果傳入的是葉子,自動返回NULL,如果找到最右邊了,返回對應地址
 95 }
 96 
 97 SearchTree Insert(SearchTree T,int X) {
 98     if(!T){             //這是應對初始情況,空樹
 99         T=(SearchTree)malloc(sizeof(struct BinNode));
100         T->Value=X;
101         T->Left=T->Right=NULL;  //底部封口
102     }
103     //在一棵現成的樹裏插入,二分查找
104     else if (X < T->Value) T->Left=Insert(T->Left, X);
105     else if (X > T->Value) T->Right=Insert(T->Right, X);
106     //X==T->Value的情況什麽也不用做
107     return T;
108 }
109 SearchTree Delete(int X,SearchTree T) {
110     Position TempCell;
111     if (T==NULL)
112             printf("Element not found\n");
113     //search for Value
114     
115     else if(X<T->Value) T->Left=Delete(X, T->Left);
116     else if(X>T->Value) T->Right=Delete(X, T->Right);
117     //找到給定的X了,開始分類討論
118     else if(T->Left && T->Right){   //有兩個兒子的情況
119             TempCell=FindMin(T->Right);     //找到右子樹下最小的數據
120             T->Value=TempCell->Value;   //Replace
121             T->Right=Delete(T->Value, T->Right);  //遞歸刪除
122         }
123     else{       //1個兒子or葉子的情況,可以統一起來,操作邏輯是一致的
124         TempCell=T;
125         if (T->Left==NULL)       //只有右孩子,就把父節點直接連到右邊
126                 T=T->Right;
127         else if (T->Right==NULL){   //只有左孩子,就把父節點直接連到左邊
128                 T=T->Left;
129             }
130         free(TempCell);
131         }
132     return T;
133 }
134 
135 
136 void PreOrder(SearchTree T) {
137     if(T){                  //如果這顆子樹非空,就打印,否則把控制權還給上級
138     printf("%d ",T->Value);
139     PreOrder(T->Left);
140     PreOrder(T->Right);
141     }
142 }
143 void InOrder(SearchTree T){
144     if(T){
145         InOrder(T->Left);
146         printf("%d ",T->Value);
147         InOrder(T->Right);
148     }
149 }
150 
151 void LevelOrder(SearchTree r) {
152     SearchTree current=r;     //為了不修改根節點,新建一個指針作為光標
153     queue<SearchTree> q;
154     q.push(current);        //把當前(根)節點入隊
155     //以下是廣搜的核心
156     while (!q.empty()) {        //隊列非空時進行遍歷
157         current=q.front();
158         printf("%d ",current->Value);
159         q.pop();            //打印完則出隊
160         if (current->Left)            //依次查看當前節點是否有後繼,有的話重復上述入隊過程,left,right or both
161             q.push(current->Left);
162         if (current->Right)
163             q.push(current->Right);
164     }
165 }

二叉樹及其實現(基礎版)