前面學過的數據結構,包括向量、鏈表、棧、隊列,從物理上或者邏輯上來說,存在一定的前后次序,並且前驅和后繼是唯一的,因此稱之為線性結構。然而,向量的插入和刪除操作、鏈表的循秩訪問等操作,復雜度都非常高。樹的結構,可以把兩種結構的優勢結合起來。
與前兩種結構不同,樹不存在天然的直接后繼或者直接前驅關系,不過,我們可以通過定義一些約束,在樹中確定節點之間的線性次序。樹屬於半線性結構。從結構來看,樹其實是一種特殊的圖,等價於連通無環圖。與圖一樣,樹也由一組頂點以及之間的聯邊組成,外加指定一個特定的根節點。
樹的幾個概念
深度(depth):如圖所示,根節點為r,v是一個樹中間的節點。v的深度,即為v到r的唯一通路經過的邊的個數,記作depth(v)。
祖先(ancestor)、后代(descendant):任一節點v在通往樹根沿途所經過的每個節點都是其祖先,v是他們的后代。特別地,如果u恰好比v高一層,則u是v的父親(parent),v是u的孩子(child)。
度數(degree):v孩子的個數,稱為v的度數,記作deg(v)。
葉節點(leaf):如果節點v沒有后代,那么v稱為葉節點。
子樹(subtree):v及其后代,以及他們直接的聯邊,稱為一顆子樹,記作subtree(v)。
高度(height):樹T中所有節點深度的最大值,稱作該樹的高度,記作height(T),推廣這一定義,節點v對應子樹的高度,記作height(v)。
二叉樹
如果每個節點最多有兩個孩子,即每個節點的度數均不超過2,稱為二叉樹(binary tree)。
二叉樹中,如果同一節點的孩子以左右區分,稱為有序二叉樹(ordered binary tree)。特別地,不含一度節點的二叉樹稱作真二叉樹(proper binary tree)。二叉樹是不失一般性地,比如一個多叉樹,如果可以定義兄弟節點的次序,那么可以轉換為一顆二叉樹。比如,假設每個節點有兩個指針,一個指向“長子”,一個指向下一個兄弟,那么這顆多叉樹就轉化為了一顆二叉樹,如下如所示:
二叉樹的實現與遍歷
下面,簡單定義一個二叉樹節點的模板類:
1 template<typename T> class BinNode { 2 public: 3 T data; 4 BinNodePosi(T) parent; 5 BinNodePosi(T) lc; BinNodePosi(T) rc; 6 int height; 7 // 構造函數 8 BinNode() :parent(nullptr), lc(nullptr), rc(nullptr), height(0) {} 9 BinNode(T e, BinNodePosi(T) p = nullptr, BinNodePosi(T) lc = nullptr, BinNodePosi(T) rc = nullptr,int h = 0) : 10 data(e), parent(p), lc(lc), rc(rc), height(h){} 11 // 操作接口 12 int size();//返回以該節點作為根節點的子樹規模 13 BinNodePosi(T) insertAsLC(T const&); 14 BinNodePosi(T) insertAsRC(T const&); 15 BinNodePosi(T) succ();//直接后繼 16 17 }; 18 template<typename T> BinNodePosi(T) BinNode<T>::insertAsLC(T const& e) 19 { 20 return lc = new BinNode(e, this); 21 } 22 template<typename T> BinNodePosi(T) BinNode<T>::insertAsRC(T const& e) 23 { 24 return rc = new BinNode(e, this); 25 }
二叉樹的模板類:
1 template<typename T> int stature(BinNodePosi(T) p) 2 { 3 return (p) ? (p)->height : -1; 4 } 5 template<typename T> class BinTree { 6 protected: 7 int _size; BinNodePosi(T) _root; 8 virtual int updateHeight(BinNodePosi(T) x);//更新節點x的高度 9 void updateHeightAbove(BinNodePosi(T) x);//更新節點及其祖先的高度 10 public: 11 BinTree() :_size(0), _root(NULL) {} 12 ~BinTree() { if (_size > 0) remove(_root); } 13 int size() const { return _size; } 14 bool empty()const { return !_root; } 15 BinNodePosi(T) root() const { return _root; } 16 BinNodePosi(T) insertAsRoot(T const& e); 17 BinNodePosi(T) insertAsLC(BinNodePosi(T) x,T const& e); 18 BinNodePosi(T) insertAsRC(BinNodePosi(T) x, T const& e);//x原來沒有LC||RC 19 BinNodePosi(T) attachAsLC(BinNodePosi(T) x, BinTree<T>*& T);//T作為x的子樹接入 20 BinNodePosi(T) attachAsRC(BinNodePosi(T) x, BinTree<T>*& T); 21 int remove(BinNodePosi(T) x);//刪除x為根的子樹 22 BinTree<T>* secede(BinNodePosi(T) x);//刪除子樹,並作為新樹返回根節點 23 }; 24 int max(int a, int b) { return a > b ? a : b; } 25 template<typename T> int BinTree<T>::updateHeight(BinNodePosi(T) x) 26 { 27 return x->height = 1 + max(stature(x->lc), stature(x->rc)); 28 } 29 template<typename T> void BinTree<T>::updateHeightAbove(BinNodePosi(T) x) 30 { 31 while (x) 32 { 33 updateHeight(x); x = x->parent; 34 } 35 } 36 template<typename T> BinNodePosi(T) BinTree<T>::insertAsRoot(T const& e) 37 { 38 _size = 1; 39 return _root = new BinNode<T>(e); 40 } 41 template<typename T> BinNodePosi(T) BinTree<T>::insertAsLC(BinNodePosi(T) x, T const& e) 42 { 43 _size++; x->insertAsLC(e); updateHeightAbove(x); return x->lc; 44 } 45 template<typename T> BinNodePosi(T) BinTree<T>::insertAsRC(BinNodePosi(T) x, T const& e) 46 { 47 _size++; x->insertAsRC(e); updateHeightAbove(x); return x->rc; 48 } 49 template<typename T> BinNodePosi(T) BinTree<T>::attachAsLC(BinNodePosi(T) x, BinTree<T>*& S) 50 { 51 x->lc = S->_root; S._root->parent = x; 52 _size += S->_size; updateHeightAbove(x); 53 S->_root = NULL; S->_size = 0; release(S); S = NULL; return x; 54 } 55 template<typename T> BinNodePosi(T) BinTree<T>::attachAsRC(BinNodePosi(T) x, BinTree<T>*& S) 56 { 57 x->rc = S->_root; S._root->parent = x; 58 _size += S->_size; updateHeightAbove(x); 59 S->_root = NULL; S->_size = 0; release(S); S = NULL; return x; 60 } 61 template<typename T> int BinTree<T>::remove(BinNodePosi(T) x) 62 { 63 FromParentTo(*(x)) = NULL;//切斷parent->x 64 updateHeightAbove(x->parent); 65 int n = removeAt(x); _size -= n; return n; 66 } 67 template<typename T> static int removeAt(BinNodePosi(T) x)//刪除位置x處的節點及其后代,返回被刪除節點的值 68 { 69 if (!x) return 0; 70 int n = 1 + removeAt(x->lc) + removeAt(x->rc); 71 //release(x->data); release(x); 72 return n; 73 } 74 template<typename T> BinTree<T>* BinTree<T>::secede(BinNodePosi(T) x) 75 { 76 FromParentTo(*x) = NULL; updateHeightAbove(x->parent); 77 BinTree<T>* S = new BinTree<T>; S->_root = x; x->parent = NULL;//以x為根節點新建樹 78 S->_size = x->size(); _size -= S->_size; return S; 79 }
只包含了一些簡單的功能,比如插入節點,返回樹的根節點,刪除節點以及以該節點為根節點的子樹、接入一顆子樹、子樹分離等操作。需要考慮的地方都比較類似,一定不要忘記更新操作后祖先的高度,以及樹的規模。
樹的遍歷
樹的遍歷是非常重要的部分,如果能確定一個先后次序,就可以像訪問鏈表一樣,方便地對存儲的數據進行操作。常見的遍歷,包括先序遍歷、中序遍歷、后續遍歷以及層次遍歷,他們的訪問次序如下圖所示:
幾種遍歷的遞歸方法
遞歸方法是比較簡單的,根據幾種遍歷的規則,可以簡單地得出,以先序遍歷為例:
1 template<typename T> void travPre(BinNodePosi(T) x) 2 { 3 if (!x) return; 4 visit(x); 5 travPre(x->lc); 6 travPre(x->rc); 7 }
先訪問父節點,再遞歸地訪問左孩子和右孩子,另外兩種遍歷也類似。
迭代方法
1 template<typename T> void travPre(BinNodePosi(T) x)//子樹中序遍歷 2 { 3 stack<BinNodePosi(T)> s; 4 while (1) 5 { 6 while (x) 7 { 8 visit(x); 9 if (x->rc) s.push(x->rc); 10 x = x->lc; 11 } 12 if (s.empty()) break; 13 x = s.top(); 14 s.pop(); 15 } 16 } 17 template<typename T> void travIn_v1(BinNodePosi(T) x) 18 { 19 stack<BinNodePosi(T)> s; 20 while (1) 21 { 22 while (x) 23 { 24 s.push(x); 25 x = x->lc; 26 } 27 if (s.empty()) break; 28 x = s.top(); 29 visit(x); 30 s.pop(); 31 x = x->rc; 32 } 33 } 34 template<typename T> BinNodePosi(T) BinNode<T>::succ() 35 { 36 BinNodePosi(T) s = this; 37 if (rc) 38 { 39 s = rc; 40 while (s->lc) s = s->lc;//若有右孩子,后繼為右子樹最深處最靠左的節點 41 } 42 else//否則,直接后繼應當為將當前節點包含於其左子樹中的最低祖先 43 { 44 while ((s->parent)&&(s->parent->rc==s)) s = s->parent;//若s為右孩子, 45 s = s->parent; 46 } 47 return s; 48 } 49 template<typename T> void travIn_v2(BinNodePosi(T) x)//不需要輔助棧的中序遍歷 50 { 51 bool backtrack = false; 52 while (true) 53 if (!backtrack && (x->lc))//當不是剛剛回溯的並且有左子樹 54 x = x->lc; 55 else //剛剛回溯或者沒有子樹 56 { 57 visit(x); 58 if (x->rc)//如果右子樹非空 59 { 60 x = x->rc; 61 backtrack = false; 62 } 63 else 64 { 65 if (!(x = x->succ())) break;//回溯(包括了抵達末節點時的退出) 66 backtrack = true; 67 } 68 } 69 } 70 template<typename T> void travPost(BinNodePosi(T) x) 71 { 72 stack<BinNodePosi(T)> S; 73 if (x) S.push(x);//根節點入棧 74 while (!S.empty()) 75 { 76 if (S.top() != x->parent)//棧頂若不是當前節點的父親,則必定是其右兄弟(右孩子先入棧,右為空時,棧頂為其父親) 77 {//若棧頂為當前訪問元素的父親,則直接訪問不需要繼續搜索 78 while (BinNodePosi(T) x = S.top())//從棧頂出發,尋找棧頂節點的左右孩子 79 if (x->lc)//當有左孩子的時候,盡量向左 80 { 81 if (x->rc) S.push(x->rc);//若有右孩子,先入棧 82 S.push(x->lc);//然后把左孩子也入棧 83 } 84 else//若當前節點只有右孩子(可能不存在為空) 85 S.push(x -> rc); 86 S.pop();//退出循環時,棧頂為空,彈出這個空節點 87 } 88 x = S.top(); 89 S.pop(); 90 visit(x);//棧頂節點出棧並訪問 91 } 92 } 93 template<typename T> void travLevel(BinNodePosi(T) x) 94 { 95 queue<BinNodePosi(T)> q; 96 q.push(x); 97 while (!q.empty()) 98 { 99 BinNodePosi(T) n = q.front(); q.pop(); visit(n); 100 if (n->lc) q.push(n->lc); 101 if (n->rc) q.push(n->rc); 102 } 103 }
對於先序遍歷,在遞歸中我們也可以看到,屬於尾遞歸,故一定可以修改得到迭代方法。在這里,我們采用了一個輔助棧的方法,借助棧來記錄下一個要訪問的節點。
對於中序遍歷,延續先序遍歷的方法,也給出了一種不需要輔助棧的方法,代價是我們需要定義一個函數,每次尋找后繼節點。事實上,通過使用直接后繼的方法,中序遍歷可以無需借助輔助棧以及標志位來實現。
后序遍歷的復雜程度要超過前兩種,並且一定無法從遞歸得來,也通過使用輔助棧解決。
對於層次遍歷,方法最為簡單明了。借助隊列,每次訪問節點,將其孩子入隊,下次循環出隊列,即可按照層次訪問。
最后,區分一下幾種二叉樹。
完全二叉樹:在對某棵二叉樹層次遍歷的過程中,如果前[n/2](向下取整)次迭代中都有左孩子入隊,且前[n/2](向上取整)-1次迭代中都有右孩子入隊,則稱為完全二叉樹(complete binary tree)。完全二叉樹簡單來說,就是最后一層左側必須有節點,而右側可以有空缺,有如下宏觀特征:葉節點只能出現在最底部的兩層,並且最底層葉節點均處於次底層葉節點的左側。因此,高度為h的完全二叉樹,節點數應在2^h到2^(h+1)-1之間,反之,規模為n的完全二叉樹,高度h=log[log2n](向下取整)。葉節點雖不至於少於內部節點,但最多多出一個。
滿二叉樹:完全二叉樹的一種特殊情況,所有葉節點同處於最底層,每一層的節點數都達到飽和,稱為滿二叉樹。
二叉樹的向量實現
有了完全二叉樹的概念,我們可以在二叉樹的層次遍歷中,得出一種二叉樹節點的新組成形式。因為完全二叉樹的特性,內部節點均為二度節點,因此,父節點與子節點間存在一定的數量關系,可以在層次遍歷時,把二叉樹組織為一個向量結構,從而便於進行訪問和修改。具體關系如下:
假設一個節點的序號(秩)為r(x),則他的左孩子序號為2*r(x)+1,右孩子序號為2*r(x)+2。通過二進制展開也可以判斷父子關系,一個節點是另外一個節點的祖先,當且僅當二進制展開是它的前綴。特別地,|S(A)|+1=|S(D)|時,為父子關系。
以向量實現的二叉樹,層次遍歷只需要從前到后進行。其它幾種遍歷方式與前面的實現原理上相同,不同之處僅在於,節點是普通的向量元素,不再存在父子指針。因此,可以通過將父子指針修改為上面提到的秩的關系,即可實現各種遍歷。
ps:文中的圖片選自鄧俊輝老師《數據結構(C++語言版)》。