树的一些结论

度: 一个节点的孩子数称为度。

一颗树的边数 = 节点数n - 1 意义在于衡量算法复杂度时使用。

树的特性

路径(通路) + 环路

通路: a, b 之间通过节点连接成的路。

* 长度:所有边的数目(有些文献使用节点定义长度)

环路:通路的俩个节点彼此短路,即重合构成环路。eg. a - b - c - d - b

连通图:节点之间均有路径

无环图:没有环路

重要特性

  1. 所谓的树就是无环连同图。极小连通图,极大无环图。
  2. 任一节点到根的路径唯一。因此path(v,r) = path(r); (v 就是vertex的意思), 因此我们可以通过
  3. 路劲的长度来衡量书中的一个节点。
  4. 等价类:长度相同的节点,我们称之为等价类。

简化

路径,节点,子树均可以相互指代,原因是节点到根路径唯一。所以在后面树的遍历中,要明白访问节点和
访问子树的概念是不同的,虽然都是使用同一个节点进行代表

深度: 一个节点的深度就是节点的长度。就是节点到根的路径的长度

半线性: 前驱唯一性得以满足,但后继唯一性不满足。

叶子: 没有后代的节点称为叶子。

树的高度: 一个深度最大的叶子节点的深度称为这颗树的高度。 这一概念可以推给子树的高度。

根节点的高度就是整树的高度。 某个子节点的高度是这个节点作为子树根节点的高度。

具有一个节点的树的高度为0。 没有节点的树,也就是空树的高度为-1。

注意区分一个节点的高度和深度。

节点的深度是节点到根的路径。节点的高度是节点所代表的子树总,具有最大深度最大的某个节点深度

树为什么要这样表示

  1. 父亲表示法: 向下查询孩子节点的时候,需要遍历整个表,看谁的父亲是这个节点,才能找到孩子节点。
  2. 孩子表示法: 向上查询父亲的时候,同样需要遍历整个表,看谁的孩子是这个节点,才能找到父亲节点。
  3. 长子+兄弟法:最能体现树的本质。

二叉树

重要概念

  1. 真二叉树
    • 所有节点的出度均为偶数。将一颗二叉树转化为真二叉树,有模糊概念,需要到具体应用时才能理解。
  2. 如何使用二叉树来描述多叉树
    • 没有错,真的可以。

遍历

先序遍历

递归版本
void preTraverseRecur( BinNode * x, VST visit)
{
    if(!x)
        return ;
    visit(x->data);
    preTraverseRecur(x->lchild, visit);
    preTraverseRecur(x->rchild, visit);
}

有一个理解错误的概念,就是先序遍历,我们做的是先访问根节点,然后访问左子树,然后访问右子树
注意这里标记出来的点,我之前理解的概念为访问左孩子这个节点。有什么不一样吗?因为一个节点可以表示
一个节点,同时也可以表示一颗树。(将其视为这颗树的根)。所以我们访问节点,就是访问节点,访问子树
则意味着访问子树内的所有节点。

所以先序遍历的理解是:先访问root节点,然后访问左子树,访问右子树。

迭代版本

版本A

这个代码感觉自己已经背会了,不知道怎么写思路了。

void preTraverseIter(BinNode * x, VST visit)
{
    stack<BinNode *> s;
    s.push(x); //根节点入栈
    while(s.empty())
    {
        auto ret = s.top(); s.pop();
        visit(ret->data);
        if(x->rchild)  //注意先将右子树入栈
            s.push(x->rchild);
        if(x->lchild)
            s.push(x->lchild);
    }
}

要点

  1. 需要开始,因此需要将根节点压入栈,然后右子树进栈,然后左子树进栈,然后新一轮判断。
  2. 必须先右再左,因为栈是filo。
  3. 这个感觉其实很奇怪,就是利用栈来模拟整个递归过程。

版本B

void preTraverseIter(BinNode * x, VST visit)
{
    stack<BinNode *>tmp;
    while(true)
    {
        visitAlongLC(x, visit, tmp); //有左树,走左树,同时将每个经历的右树入栈
        if(tmp.empty())
            return ;
        x = tmp.top(); //走向最近的右树
        tmp.pop();
    }
}

void visitAlongLC(BinNode *x, VST visit, stack<BinNode *> &tmp)
{
    while(x)
    {
        visit(x->data);
        tmp.push(x->rc);
        x = x->lc;
    }
}

慢慢有点感觉了,就是有左走左,没左就走一步右,然后继续循环。有点绕大圈的感觉是不是。

中序遍历

递归版本
void inTraverseRecur( BinNode * x, VST visit)
{
    if(!x)
        return ;
    preTraverseRecur(x->lchild, visit);
    visit(x->data);
    preTraverseRecur(x->rchild, visit);
}

迭代版本
void inTraverseIter(BinNode * x, VST visit)
{
    stack<BinNode *> S;
    while(true)
    {
        {//goAlongLc
        while(x)
        {
            S.push(x);
            x = x->lc;
        }
        }
        if(S.empty())
            break;
        x = S.top();
        S.pop();
        visit(x);
        x = x->rc;
    }
}

还是无法具体描述这种感觉,需要仔细和递归版本比较。递归版本来看。就是有左边就一直走左边。
走到左边不能走为止,然后回朔,访问最近的左节点(也就是不能左边空节点的根),然后走向这个节点
的右子树。

后序遍历

递归版本
void postTraverseRecur( BinNode * x, VST visit)
{
    if(!x)
        return ;
    preTraverseRecur(x->lchild, visit);
    preTraverseRecur(x->rchild, visit);
    visit(x->data);
}

迭代版本

最喜欢的就是迭代版本的后序遍历,因为真的很巧妙。它恰好是先根,再右子树,再左树的倒序输出。
明显就是使用俩个栈就可以实现了。

void postTraverseIter(BinNode * x, VST visit)
{
    stack<BinNode * > helper;
    stack<BinNode * > output;
    helper.push(x);
    while(!helper.empty())
    {
        x = helper.top();
        helper.pop();
        output.push(x);
        if(HasLc(x))
            helper.push(x->lc);
        if(HasRc(c))
            helper.push(x->rc);
    }
    while(!output.empty())
    {
        visit(output.top());
        output.pop();
    }
}

注意这里是先压左树再压右树,和先序遍历的方式刚好相反。

层次遍历

层次遍历,自上而下
void levelTraverse(BinNode * x, VST visit)
{
    queue<BinNode *> Q;
    Q.push(x);
    while(!Q.empty())
    {
        x = Q.front();
        Q.pop();
        visit(x);
        if(HasLc(x))
            Q.push(x->lc);
        if(HasRc(x))
            Q.push(x->rc);
    }
}
层次遍历,自下而上

哇!这个实现也非常巧妙

void levelReverseTraverse(BinNode * x, VST visit)
{
    queue<BinNode *> Q;
    stack<BinNode *> S;
    Q.push(x);
    while(!Q.empty())
    {
        x = Q.front();
        Q.pop();
        S.push(x);
        if(HasRc(x))
            Q.push(x->rc);
        if(HasLc(x))
            Q.push(x->lc);
    }
    while(!S.empty())
    {
        visit(S.top());
        S.pop();
    }
}

根据前序或者后序配合中序还原树

这个代码自己花了3个小时才搞出来。

BinNode * rebuild(vector<int> preorder, vector<int> inorder)
{
    return buildHelper(preorder.begin(), preorder.end(), inorder.begin(), inorder.end());
}
using iter = vector<int>::iterator;
BinNode * buildHelper(iter p1, iter p2, iter i1, iter i2)
{
    if(p1 >= p2 || i1 >= i2)
        return nullptr;
    BinNode * root = new BinNode();
    root->val = *p1;
    auto ret = find(i1, i2, root->val);
    int len = ret - i1;
    p1++;
    root->lc = buildHelper(p1, p1 + len, i1, ret);
    root->rc = buildHelper(p1+len, p2, ret + 1, i2); 
    return root;
}

时刻要记得自己是[low, high) 的方式,还是[low, high]的方式在操纵。大部分情况下是[low, high)的方式
因为在c++中的迭代器就是给你这样的方式。

这里可以优化的一个点就是find这里。在leetcode上看到的做法是直接使用散列表。因为preorder和inorder
中的那个数都是相同的,所以讲preorder中的数字映射到inorder中的位置。这样以O(1)的时间得到

同样,使用后序遍历结合中序遍历的情况下也能建树

BinNode * rebuild(vector<int> postorder, vector<int> inorder)
{
    return buildHelper(postorder.begin(), postorder.end(), inorder.begin(), inorder.end());
}
using iter = vector<int>::iterator;
BinNode * buildHelper(iter p1, iter p2, iter i1, iter i2)
{
    if(p1 >= p2 || i1 >= i2)
        return nullptr;
    BinNode * root = new BinNode();
    root->val = *(p2 - 1);
    auto ret = find(i1, i2, root->val);
    int len = ret - i1;
    root->lc = buildHelper(p1, p1 + len, i1, ret);
    root->rc = buildHelper(p1+len, p2 - 1, ret + 1, i2); 
    return root;
}

插入和删除

插入节点

这个其实就是基本功夫的考验。这里主要注意的是我们这次有parent节点。

succ,计算后继节点

如何计算后继节点,首先必须要深深理解中序遍历。所谓后继节点,就是当前节点中序遍历的下一个节点。

BinNode * succ()
{
    BinNode * s = this;
    if(this->rc) // 如果有右树,就一定在右树之中
    {
        s = rc;
        while(s) s = s->lc; 
    }
    else{ // 如果该节点没有右树
        while(s == s->parent->rc)  s = s->parent; // 那么该节点一定是最右边的孩子。因此首先需要找到
                                                  // 其右树开始的分叉
        s = s->parent;                            // 此时的s一定是在上一节点的左树之中,它的parent
                                                  // 就是后继 
    } 
}

不是特别特别的明白。但是这里其实也不是那么重要。

 posted on 2018-09-28 18:57  patientcat  阅读(446)  评论(0编辑  收藏  举报