树(二叉树)

前面学过的数据结构,包括向量、链表、栈、队列,从物理上或者逻辑上来说,存在一定的前后次序,并且前驱和后继是唯一的,因此称之为线性结构。然而,向量的插入和删除操作、链表的循秩访问等操作,复杂度都非常高。树的结构,可以把两种结构的优势结合起来。

与前两种结构不同,树不存在天然的直接后继或者直接前驱关系,不过,我们可以通过定义一些约束,在树中确定节点之间的线性次序。树属于半线性结构。从结构来看,树其实是一种特殊的图,等价于连通无环图。与图一样,树也由一组顶点以及之间的联边组成,外加指定一个特定的根节点。

树的几个概念

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

posted @ 2017-07-08 23:45  luStar  阅读(6250)  评论(0编辑  收藏  举报