二叉樹(Binary Tree)是最簡單的樹形數據結構,然而卻十分精妙。其衍生出各類算法,以至於佔據了數據結構的半壁江山。STL中大名頂頂的關聯容器——集合(set)、映射(map)即是使用二叉樹實現。因爲篇幅有限,此處僅做通常介紹(若是想要徹底瞭解二叉樹以及其衍生出的各類算法,恐怕要寫8~10篇)。html
1)二叉樹(Binary Tree)node
顧名思義,就是一個節點分出兩個節點,稱其爲左右子節點;每一個子節點又能夠分出兩個子節點,這樣遞歸分叉,其形狀很像一顆倒着的樹。二叉樹限制了每一個節點最多有兩個子節點,沒有子節點的節點稱爲葉子。二叉樹引導出不少名詞概念,這裏先不作系統介紹,遇到時再結合例子一一說明。以下一個二叉樹:git
/* A simple binary tree * A ---------> A is root node * / \ * / \ * B C * / / \ * / / \ * D E F ---> leaves: D, E, F * * (1) ---> Height: 3 * */
其中節點B只有一個子節點D;D, E, F沒有子節點,被稱爲葉子。對於節點C來講,其分出兩子節點,因此C的出度爲2;同理,C有且只有一個父節點,因此其入度爲1。出度、入度的概念來源於圖(Graph,一種更加高級複雜的數據結構),固然,也能夠應用於二叉樹(二叉樹或者說樹形數據結構也是一類特殊的圖)。顯然,二叉樹的根節點入度爲0,葉子節點出度爲0。算法
如何衡量一顆二叉樹?好比大小、節點稠密等。與樓房同樣,通常會對二叉樹分層,而且一般將根節點視爲第一層。接下來B與C同屬第二層,D, E, F同屬第三層。注意,並非全部的葉子都在同一層。一般將二叉樹節點的最高層數做爲其樹的高度,上例中二叉樹高度爲3。顯然,一個二叉樹的節點總數必然小於2的樹高冪,轉化成公式表示爲:N<2^H,其中N爲節點總數,H爲二叉樹高度;對於第k層,最多有2^(k-1)個節點。更加細化的分類,以下:數組
徹底二叉樹:除了最高層之外,其他層節點個數都達到最大值,而且最高層節點都優先集中在最左邊。數據結構
滿二叉樹:除了最高層有葉子節點,其他層無葉子,而且非葉子節點都有2個子節點。app
以下例:數據結構和算法
/* Complete Binary Tree (CBT) and Full Binary Tree (FBT) * A A A * / \ / \ / \ * / \ / \ / \ * B C B C B C * / \ / \ / \ / \ * / \ / \ / \ / \ * D E D E F G D E * * (2) (3) (4) * CBT FBT not CBT * */
其中(2)就是一個徹底二叉樹;(3)是一個滿二叉樹;而(1)和(4)不屬於這二者,(雖然(4)是(2)的一種鏡像二叉樹)。易知,滿二叉樹必然是一個徹底二叉樹,反之則否則。從節點數量上看,滿二叉樹的第k層有2^(k-1)個節點,因此其總節點數爲2^H - 1;徹底二叉樹除了最後一層外,第k層節點有2^(k-1)個節點,最後一層最多有2^(H-1)個節點。post
其實,關於徹底二叉樹的定義有多種,然而無論怎樣定義,其實質是同樣的,關鍵在於怎樣理解。若是徹底二叉樹除去最後一層,則成爲一個滿二叉樹。所謂的「最後一層節點優先集中在左邊」,用語言很難解釋,可是結合上例的(2)和(4)能夠很好理解。爲何要這樣定義呢?這是由於這種徹底二叉樹的效率很是高,而且徹底二叉樹絕大多數狀況使用數組存儲,即無序堆(Heap)!能夠參見關於堆的博文http://www.cnblogs.com/eudiwffe/p/6202111.html爲了充分利用數組的存儲空間,優先將葉子安排在最左邊,以保證該數組每一個存儲單元都被利用(若是是(4)的狀況,則該數組會有部分空間浪費)。這就是爲何要要求「最後一層優先集中在最左邊」。性能
2)二叉樹的構建和遍歷
數據結構和算法,最終要落實在代碼上,首先給出通常C風格的二叉樹節點定義,其中val在同一顆樹中惟一:
// A simple binary tree node define typedef struct __TreeNode { int val; struct __TreeNode *left, *right; }TreeNode;
很簡單,看着很像雙鏈表節點的定義,若是拋開字段名稱,其實質徹底跟雙鏈表節點結構同樣。事實上,有不少狀況下須要將二叉樹就地轉換成一個雙鏈表,甚至是單鏈表。如何構建一個二叉樹?很抱歉,這個佔據數據結構與算法半壁江山的二叉樹,居然沒有一個標準的構建方法!由於二叉樹使用太過普遍,針對不一樣應用有不一樣的構建方法,若是僅僅將一個節點插入(或刪除)到二叉樹中,這又太過簡單,簡單的與鏈表插入(或刪除)同樣。故本文不提供構建方法。
對於給定的一顆二叉樹,如何遍歷呢?有四種常見方法。
中序遍歷:即左-根-右遍歷,對於給定的二叉樹根,尋找其左子樹;對於其左子樹的根,再去尋找其左子樹;遞歸遍歷,直到尋找最左邊的節點i,其必然爲葉子,而後遍歷i的父節點,再遍歷i的兄弟節點。隨着遞歸的逐漸出棧,最終完成遍歷。例如(1)中的遍歷結果爲:D->B->A->E->C->F
先序遍歷:即根-左-右遍歷,再也不詳述。例如(1)中的遍歷結果:A->B->D->C->E->F
後序遍歷:即左-右-根遍歷,再也不詳述。例如(1)中的遍歷結果:D->B->E->F->C->A
層序遍歷:即從第一層開始,逐層遍歷,每層遍歷按照從左到右遍歷。例如(1)中的遍歷結果:A->B->C->D->E->F
很明顯,先序遍歷的第一個節點必然是樹的根節點;後序遍歷的最後一個節點也必然是樹的根節點。層序遍歷更加符合人對二叉樹的樹形結構的遍歷順序。
下面給出通常的實現代碼供參考:
// root is in middle order travel, (1):D->B->A->E->C->F void inorder(TreeNode *root) { if (root == NULL) return; inorder(root->left); printf("%d ",root->val); // visit inorder(root->right); } // previous visit root order travel, (1):A->B->D->C->E->F void preorder(TreeNode *root) { if (root == NULL) return; printf("%d ",root->val); // visit preorder(root->left); preorder(root) } // post vist root order travel, (1):D->B->E->F->C->A void postorder(TreeNode *root) { if (root == NULL) return; postorder(root->left); postorder(root->right); printf("%d ",root->val); // visit }
看着很簡單感受不太對,毋庸置疑,事實上就是這麼簡單。此處僅給出遞歸版本,雖然遞歸間接用到了棧,可是即使使用循環版本實現,其仍然須要輔助空間存儲。爲何在實現堆的代碼中,用的是循環而不是遞歸?這就是由於堆的形象化是一個徹底二叉樹,而且用數組存儲,可見徹底二叉樹的效率如此之高。對於層序遍歷,就須要使用輔助的存儲空間,通常使用隊列(queue),由於其要求每層的順序要從左到右。下面使用STL中queue進行實現,關於隊列的介紹,請自行補充。
// level order travel, (1):A->B->C->D->E->F void levelorder(TreeNode *root) { if(root==NULL) return; queue<TreeNode*> q; for(q.push(root); q.size(); q.pop()){ TreeNode *r = q.front(); printf("%d ",r->val); // visit if (r->left) q.push(r->left); if (r->right) q.push(r->right); } }
上面是一種層序遍歷,但並無對每層進行分割,換言之,並不知道當前遍歷的節點屬於哪一層。如需實現,只須要兩個隊列交替遍歷,每一個隊列遍歷完就是一層的結束,感興趣的能夠自行寫出。
其中,前面三種遍歷最爲常見,先序遍歷是二叉樹的深度優先遍歷(Depth First Search,DFS),使用最普遍。層序遍歷是二叉樹的廣度優先遍歷(Breadth First Search,BFS)。
3)二叉樹的序列化(serialize)和反序列化(deserialize)
簡單講,序列化就是將結構化數據轉化成可順序傳輸的數據流;反序列化就是將順序數據流還原成原來的數據結構。
前面幾種遍歷方法,雖然均可以將二叉樹轉換成順序的數據流,但還不能稱做序列化,由於沒有辦法還原二叉樹結構。以(1)爲例,其常見四種遍歷方法獲得的數據流爲:
/* A simple binary tree four typical traversals * A * / \ in order : D->B->A->E->C->F * / \ pre order : A->B->D->C->E->F * B C post order : D->B->E->F->C->A * / / \ level order: A->B->C->D->E->F * / / \ * D E F * * (1) * */
單獨使用沒法將其還原成二叉樹。可是,仔細觀察發現,先序遍歷的第一個節點A爲根節點;後序遍歷的最後一個節點A也是根節點。若是同時知道一個二叉樹的先序和後序遍歷順序,是否能夠還原樹呢?很抱歉,雖然兩種遍歷的方法不同,但其只能肯定根節點的位置,其餘節點沒法肯定。那麼,若是使用中序+先序遍歷結果,是否可行呢?讓咱們試試。
根據先序遍歷知道第一個節點A爲根節點,接下來「B->D->C->E->F」是左右節點的順序,雖然目前還沒法判斷到底哪一個是左,哪一個是右;
前面已知,中序遍歷以根節點爲分隔,左邊是左子樹,右邊是右子樹,因而在中序中找到A的位置,以此分隔,左部分「D->B」是左子樹,右部分「E->C->F」是右子樹;
請注意,對於任意一個節點來講,都是某個子樹的根節點,即使是葉子節點,它也是一個空二叉樹的根節點!由此引出,先序遍歷的每一個節點都曾充當父節點(某子樹的根節點)。
因而,對於剩下的先序遍歷數據流「B->D->C->E->F」來講,B也是剩下的某子樹的根節點,到底是哪一個子樹呢?顯然是左子樹,由於先序遍歷的順序就是「根-左-右」。所以,在左子樹「D->B」中找到B,其爲左子樹的根;因而將「D->B」分紅左子樹「D」和右子樹「」(空)。根據遞歸的出棧,接下來處理先序遍歷中的「D->C->E->F」,緊接着是「C->E->F」...最終,完成二叉樹的還原。部分步驟示意圖:
// Using In order and Pre order to deserialize /* * A* A A A * / \ ====> / \ / \ / \ * / \ / \ / \ / \ * D-B E-C-F B* E-C-F B E-C-F B C* * / \ / / / \ * / \ / / / \ * D NULL D* D E F * root root root root * | | | | * IN: D-B-A-E-C-F D-B D E-C-F * PRE:A-B-D-C-E-F B-D-C-E-F D-C-E-F C-E-F * | | | | * root root root root * */
每次根據先序遍歷結果肯定當前的根節點(用*標記),而後在中序遍歷結果中尋找該節點,並以此爲分割點,分紅左右子樹;反覆執行,直到先序遍歷結束,二叉樹還原完畢。下面給出C風格的代碼,僅供參考:
// Using In order and Pre order to deserialize TreeNode *deserialize(int pre[], int in[], int n, int begin, int end) { static int id = 0; // current position in PRE order if (begin==0 && end==n) id=0; // reset id TreeNode *r = (TreeNode*)malloc(sizeof(TreeNode)); int pos; // current root position in IN order for (pos=begin; pos<end && in[pos]!=pre[id]; ++pos); if (in[pos]!=pre[id]) exit(-1); // preorder or inorder is error r->val = pre[id++]; r->left = deserialize(pre,in,n,begin,pos); r->right= deserialize(pre,in,n,pos+1,end); return r; }
其中pre[]爲先序遍歷結果,in[]爲中序遍歷結果,此處假設節點的值(val)爲惟一(對於不惟一的,能夠增長關鍵字字段)。n爲節點總數,也即爲數組的長度;start和end表示尋找中序遍歷的區間範圍[start,end)。若是給定的pre[]和in[]絕對正確,那麼第9行的錯誤處理將不會執行。對於一棵N節點的二叉樹,直接調用deserialize(pre,in,n,0,n)則可還原該二叉樹。整個逆序列化的過程,其實是「先序遍歷」的過程,不妨看看10~12行代碼。
同理,使用中序+後序也可還原二叉樹,這裏再也不詳述。
不妨算法其時間複雜度,對於先序數據流,其使用了靜態的id做爲遍歷下標,故爲O(n);可是對於中序遍歷數據流,其根據[start,end)區間進行遍歷尋找,爲O(nlogn)。感興趣的不妨嘗試改進層序遍歷,使其達到序列化和反序列化的要求(注意分層和空節點)。
4)二叉搜索樹(Binary Search Tree)
之因此稱爲二叉搜索樹,是由於這種二叉樹能大幅度提升搜索效率。若是一個二叉樹知足:對於任意一個節點,其值不小於左子樹的任何節點,且不大於右子樹的任何節點(反之亦可),則爲二叉搜索樹。若是按照中序遍歷,其遍歷結果是一個有序序列。所以,二叉搜索樹又稱爲二叉排序樹。不一樣於最大堆(或最小堆),其只要求當前節點與當前節點的左右子節點知足必定關係。下面以非降序二叉搜索樹爲例。
// Asuming each node value is not equal /* A simple binary search tree * 6 6 * / \ / \ * / \ / \ * 3 8 3 8 * / / \ / / \ * / / \ / / \ * 2 7 9 2 4* 9 * * (A) BST (B) Not BST * */
其中(A)爲二叉搜索樹,(B)不是。由於根節點6小於右子樹中的節點4。
構建二叉搜索樹的過程,與堆的構建相似,即逐漸向二叉搜索樹種添加一個節點。每次新添加一個節點,直接尋找到對應的插入點,使其知足二叉搜索樹的性質。下面是一種簡易的構建過程:
// Initialize a bst TreeNode *bst_init(int arr[], int n) { if (n<1) return NULL; TreeNode *r = (TreeNode*)malloc(sizeof(TreeNode)); r->val = arr[0]; // ensure bst_append will not update root address r->left = r->right = NULL; for (; --n; bst_append(r,arr[n])); return r; }
對於給定的數組數據,若是僅有一個元素,則直接構造一個節點,將其返回;不然,逐漸遍歷該數組,將其元素插入到二叉樹中(不要忘記將無子節點的指針置爲空),其中bst_append將元素插入的二叉查找樹中。爲何對於單獨一個元素要特殊處理,而不是全部節點都經過bst_append插入呢?顯然,當插入第一個元素時,此時二叉樹根節點爲空,直接插入必然修改根節點的地址。固然能夠經過返回值獲取插入後二叉樹的根節點指針,但這樣僅僅針對1/n的狀況,卻每次(共N次)都從新對根節點賦值,犧牲太多性能。固然也能夠將bst_append傳參列表聲明爲二級指針,這裏爲了追求簡潔,故不使用。
當給出插入節點的代碼時,你會發現二叉搜索樹的構建跟堆的構建思路有殊途同歸之妙,而且插入方法與先序遍歷十分類似:
// Append a node to bst, return add count int bst_append(TreeNode *r, int val) { // find insertion position for (; r && r->val!=val;){ if (r->val < val && r->right) r=r->right; else if (r->val > val && r->left) r=r->left; else break; } if (r==NULL || r->val==val) return 0; TreeNode *tn = (TreeNode*)malloc(sizeof(TreeNode)); tn->left = tn->right = NULLL; tn->val = val; if (r->val < val) r->right = tn; else r->left = tn; return 1; }
一般狀況,認爲二叉樹的節點值爲惟一,即不存在新插入的值與已有節點值相同的狀況,正如一個集合中不存在相同的兩個元素。雖然STL也提供multiset與multimap以便容許重複元素,但其增長了新的字段count用於存儲每一個值val所包含的節點個數。易知,對於set而言,其每一個節點的count值均爲1。注意,對於同一個元素集合,其數組中的順序不一樣,生成的二叉查找樹也不一樣。其中,二叉搜索樹的插入時間複雜度爲O(logn),構建二叉搜索樹的總時間複雜度爲O(nlogn)。尋找插入位置的過程,實際上相似於二分查找。
既然叫二叉搜索樹,那麼如何高效的查找一個元素是否在該二叉搜索樹呢?與插入相似,一樣使用先序遍歷的結構:
// Find value in bst, return node address TreeNode *bst_find(TreeNode *r, int val) { for (; r && r->val!=val;){ if (r->val < val) r=r->right; else if (r->val > val) r=r->left; } return r; }
若是找到了,直接返回該節點指針,不然返回空指針。二叉搜索樹對於元素的查找效率與二分查找同樣,都爲O(logn),只不過前者使用二叉樹鏈式存儲,而二分查找使用順序的數組存儲,二者各有優劣。
不少時候,經常須要刪除其中的某些元素,對於二分查找來講,其使用的是有序數組存儲,對於數據的插入和刪除效率較低,均爲O(n);而二叉搜索樹卻有着O(logn)的快速,那麼如何刪除節點?與堆不一樣,二叉搜索樹使用鏈式存儲,須要注意內存釋放,避免其父節點、左右子節點意外分離於原二叉搜索樹。所以須要根據待刪除節點所處位置,進行分類處理。
在這以前,首先引入一個概念——前驅節點(Precursor Node)。所謂前驅,即按照某種遍歷方法,節點前的一個節點爲該節點的前驅節點。以(1)爲例,其中序遍歷爲「D->B->A->E->C->F」,那麼對於節點A來講,其前驅節點爲B;對於節點E來講,A是其前驅節點(下面不做特殊說明,均以中序遍歷順序狀況)。與之相反,後繼節點則爲按照某種遍歷方法該節點的下一個節點。即,A是B的後繼節點。對於二叉搜索樹來說,若是使用中序遍歷,其遍歷結果是有序的,即:任意一個節點的前驅節點是知足不大於該節點的最大節點;任意一個節點的後繼節點是知足不小於該節點的最小節點。以(A)爲例,其中序遍歷爲「2-3-6-7-8-9」。
對於二叉搜索樹的節點刪除,通常可分爲三種狀況:待刪除的節點有兩個子節點,待刪除的節點有一個子節點,待刪除的節點無子節點:
/* Erase node from a bst - sketch, i' is special for erase 6 (i) * 6 d=6,(3) f=6 6 d=6,(5) * / \ / \ / \ / \ / \ * / \ / \ / \ / \ / \ * 3 8 p=3 8 d=3 8 3 f=8 f=3 8 * / / \ / / \ / / \ / / \ / \ / \ * / / \ / / \ / / \ / / \ / \ / \ * 2 7 9 2 7 9 2 7 9 2 d=7 9 2 p=5 7 9 * / * BST (i) (ii) (iii) / (i') * erase 6 erase 3 erase 7 4 * */
(i) 待刪除的節點有兩個子節點:以刪除6爲例,爲了便於說明,這裏將待刪除節點稱爲d=6,其前驅節點爲p=3。按照(i)圖示方法,能夠將其前驅節點p的值替換待刪除節點d,並刪除前驅節點。注意,若是前驅節點p仍有子節點(子樹),則其必然是左節點(左子樹),爲何?請自行思考。這裏將前驅節點p的父節點稱爲f,此時的f正好是d,但不是全部狀況都是。對於(i')圖示,前驅節點p=5的父節點爲f=3,當刪除d=6時,能夠將f的右子節點指向p的左子節點;對於(i),因爲f與d相同,因此能夠直接將d的左子節點指向p的左子節點。
(ii)待刪除的節點有一個子節點:以刪除3爲例,因爲只有一個子節點,因此可將d節點的子節點繼承d,此時須要將d的父節點f=6的子節點指向繼承節點。而且須要區分當前刪除節點d是父節點f的左子節點仍是右子節點,以及d節點的子節點是左子仍是右子。圖示d爲f的左子節點,d有左子節點,因此將f的左子節點指向d的左子節點。
(iii)待刪除的節點無子節點:以刪除7爲例,很簡單,將其直接刪除,而且將其父節點f的子節點指向空。一樣須要判斷d是f的左子仍是右子。
請注意,對於單根二叉樹,即一個二叉搜索樹有且只有一個節點,此時須要刪除該根節點,那麼刪除根節點後,二叉樹爲空。與bst_append相似,若是爲空,須要經過返回值回傳根節點爲空,或者經過傳參列表聲明二級節點指針。爲了簡化代碼,此處不對其進行處理,由調用刪除節點處自行處理。
下面是一種實現代碼,其中返回值表示刪除的節點個數,對於單根二叉樹返回-1,告訴調用者,並由調用者自行處理:
int bst_erase(TreeNode *r, int val) { TreeNode *f, *p, *d; // f is father node // p is precursor node // d is to be deleted node for (f=NULL,d=r; d && d->val!=val;){ f = d; if (d->val < val) d=d->right; else d=d->left; } if (d==NULL) return 0; // cannot find erase node if (d->left && d->right){ // deletion has two children // find deletion node d's precursor for (f=d,p=d->left; p->right; f=p, p=p->right); d->val = p->val; // replace deletion val by precursor if (f==d) d->left = p->left;// case (i) else f->right = p->left; // case (i') } else if (d->left==NULL && d->right==NULL){ if (d==r) return -1; // deletion is single root, this will // replace root address to NULL, please // deal this at calling procedure. // deletion is leaf if (f->left == d) f->left=NULL; else if (f->right == d) f->right=NULL; free(d); } else { // deletion has single child node or branch p = (d->left ? d->left : d->right); d->val = p->val; d->left = p->left; d->right = p->right; free(p); } return 1; // return erase node count }
到此爲止,二叉搜索樹介紹完畢。顯然,二叉搜索樹的刪除要複雜的多。實際上,二叉搜索樹才僅僅是二叉樹的一個衍生樹,後續的平衡二叉搜索樹、AVL樹以及紅黑樹等,纔是實際使用最爲普遍的。因爲篇幅限制,二叉樹及其衍生算法介紹完畢。
注:本文涉及的源碼:binary tree : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/binarytree/binarytree.c
binary tree deserialize : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/binarytree/btdeserialize.c
binary search tree : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/binarytree/bst.c
刪除二叉搜索樹中的節點:LintCode, https://git.oschina.net/eudiwffe/lintcode/blob/master/C++/remove-node-in-binary-search-tree.cpp