前言:常見的數據結構都有指針和數組兩種實現方式,這篇先介紹指針實現,而數組實現在后續文章里會講到。
(長文預警!)
說完了一般的樹,我們再來看看二叉樹,這是一種很典型的樹,它的所有節點度數都不超過2,最多只有兩個孩子。這是一種特例,但是后面我們會看到在保證有序性和有根性之后,它卻足以描述所有的樹。每個節點的出度最多為2,在之前對所有節點按照深度划分的等價類,從規模上看就構成了一個公比為2的等比數列,相應地,深度為k(第k層)的節點,最多有2k個。那么對於含n個節點、高度為h的二叉樹中(這里再多說一句,高度指的是:除了根節點,下面有幾層高度就是幾,回想一下,空樹高度是-1,一個根節點高度為0。),滿足這樣一個條件:
h<n<2^h+1。
這個性質很好證明。對於上界的情況,樹的每一層都是滿的,所以從根到第n層累加,總數=1+2+4+…+2n=2n+1 - 1,也就是右半部分。而下界情況,根據定義每層至少有一個節點,所以一共h+1個,這種情況下,樹就退化為一條單鏈。
具體來說一下上界的情況,在這個時候節點個數n=2h-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.只有一個兒子的話,就可以在它的父節點調整指針時繞過該節點后被刪除。
這棵樹中,刪除4
從父節點直接繞過去,bypass
而有兩個兒子的話情況就復雜了,一般來說是用它右子樹下最小的數據來代替該節點數據並遞歸刪除。因為右子樹下面最小的節點不可能有左兒子,所以第二次delete就更容易了。
//好像插入不了視頻……所以請點擊 這里查看刪除演示。如果你們知道怎么弄,請在評論里告訴我🙏,我試過插入源代碼,還是不行Orz
總結起來就是:
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 order和Level order,前者對應着深搜,后者對應廣搜。層序遍歷是按照離根節點的距離由遠及近地訪問。與層序不同,其他三種都是根據對根節點的訪問次序來划分的。如果是先於左右子樹,那就是Preorder,如果是介於左右子樹之間,就是Inorder,如果是位於左右子樹遍歷之后,就是Postorder。
通過這張圖我們來對比記憶,下面詳細說明每種方法。
這是層序遍歷,結果是ADBFHCEG。它的思想是用一個隊列來維護,對於每個節點進行如下操作:
1.將這個節點入隊
2.打印后出隊
3.接着把該節點所有孩子按順序入隊
然后對所有孩子重復第2,3步,很顯然,這是用遞歸輕松解決的。
這是先序遍歷,而這條紅色的線有一個名字叫Euler tour,preorder的結果就是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 }