樹(二叉樹)


前面學過的數據結構,包括向量、鏈表、棧、隊列,從物理上或者邏輯上來說,存在一定的前后次序,並且前驅和后繼是唯一的,因此稱之為線性結構。然而,向量的插入和刪除操作、鏈表的循秩訪問等操作,復雜度都非常高。樹的結構,可以把兩種結構的優勢結合起來。

與前兩種結構不同,樹不存在天然的直接后繼或者直接前驅關系,不過,我們可以通過定義一些約束,在樹中確定節點之間的線性次序。樹屬於半線性結構。從結構來看,樹其實是一種特殊的圖,等價於連通無環圖。與圖一樣,樹也由一組頂點以及之間的聯邊組成,外加指定一個特定的根節點。

樹的幾個概念

深度(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++語言版)》。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM