• 二叉搜索树


    二叉查找树有几个地方没搞明白,特别是删除结点。特写这篇文章,认真研究、记录一下。

    二叉搜索树重要性质

    二叉搜索树,也叫二叉树查找树,英文名Binary Search Tree,是一颗特殊的二叉树,是查找表的一种实现。满足以下性质:

    ∀结点x ∈ U(U是二叉树所有结点集合),

    1. 如果y是x左子树的一个结点,那么y.key <= x.key;
    2. 如果y是x右子树的一个结点,那么y.key>=x.key;

    需要注意:

    1. 这2条性质不仅仅只适用于当前子树根结点x和子结点的关系,而是根节点与子树所有结点的关系。
      也就是说,所有右子树结点关键字不会小于根,所有左子树结点关键字不会大于根。

    2. 二叉树并非满二叉树或完全二叉树,更适合用于查找而非其他目的,如存储;
      如果以严格递增序列插入数据,如{1,2,3,4,5}按序插入,会生成如下树形结构的二叉搜索树。明显不少满二叉树,也不是完全二叉树。

    1
      
       2
         
          3
           
            4
              
               5
    

    数据结构

    结点

    包含属性p - 父节点,key - 关键字,left - 左孩子, right - 右孩子。

    伪代码表示

    Node {// 结点
      p // 父结点
      key // 关键字
      left // 左孩子
      right // 右孩子
    }
    

    二叉搜索树

    以{5,2,9,1,3,7,12,null, null, null, 4, null, 8,11,20}为例,将各结点用连线连接到一起,

    由于p、left、right这些指针大量重复,而且用箭头足以表示结点间关系,故可以省略:

    二叉搜索树的伪代码表示

    Tree {
          root // 根节点, 也是p=NIL的结点
    }
    

    遍历二叉搜索树

    中序遍历 inorder tree walk

    二叉查找树的性质,允许对其进行中序遍历得到递增序列。
    注意:中序遍历(二叉树),指的是每个子树的根节点的访问顺序都是:先访问左子树,再访问根节点,最后访问右子树。

    中序遍历并打印二叉搜索树的打印伪代码(递归)

    INORDER-TREE-WALK(x)
        if x ≠ NIL
            INORDER-TREE-WALK(x.left)
            print x.key 
            INORDER-TREE-WALK(x.rigth)
    

    查找

    在二叉搜索树T中,查找给定关键字k的结点。

    根据二叉查找树特点,从根开始,当前结点x.key > k时,k只可能在右子树,然后继续到x右子树查找;x.key < k时,左子树查找;
    这样循环往下查找,结束条件:
    1)找到x.key == k,说明已经成功找到;
    2)x==NIL,说明树中不存在这样的结点。

    TREE-SEARCH(x, k)
        if x == NIL or x.key == k
            return x
        if k < x.key
            return TREE-SEARCH(x.left, k)
        else return TREE-SEARCH(x.rigth, k)
    

    最大关键字tree maximum

    沿着右子树进行查找,最后一个结点是最大关键字的结点。最大关键字结点,也称为x子树的最右结点

    TREE-MAXIMUM(x)
        while x.right ≠ NIL
            x = x.right
        return x // 最后x.right = NIL,也就是说x没有右子树
    

    最小关键字tree minimum

    类同最大关键字查找方法。最小关键字的结点,也称x子树的最左结点

    TREE-MINIMUM(x)
        while x.left ≠ NIL
            x = x.left
        return x // 最后x.left = NIL, 也就是说x没有左子树
    

    后继与前驱

    中序遍历二叉树得到访问序列中,当前结点的下一个结点,叫该结点的后继结点(简称后继);当前结点的前一个结点,叫该结点的前驱结点(简称前驱)。
    而中序遍历整个二叉搜索树,会得到一个以全部结点关键字的非递减序列。

    后继与前驱性质

    记某结点为x(x∈U),其前驱为PRESUCCESSOR(x),后继为SUCCESSOR(x),有:

    1. PRESUCCESSOR(x).key <= x.key <= SUCCESSOR(x).key;
    2. 前驱是关键字≤x.key的最大结点,后继是关键字≥x.key的最小结点;

    后继 SUCCESSOR

    设某结点为x,后续结点y,y.key是 ≥ x.key的最小值。

    y.key = min {t.key | t.key >= x.key, t∈U}, y = SUCCESSOR(x)

    如何找到后继结点?
    后继主要看结点有没有右子树,前驱主要看有没有左子树。
    具体地,
    后继与右子树有关,主要分为两类情况(前提是对应情形存在后继):

    1. 如果x右子树非空,x后继就是右子树的最左结点;
    2. 如果x右子树为空,x后继就是以x所在子树为左子树最底层祖先。
      也就是说,y的左孩子也是x的祖先,x不可能位于y的右子树。而且最底层意味着,y必须是距离x最近的一个以x所在子树为左子树的祖先结点。

    下图是具体几种典型情况示意图

    示例

    中序遍历:1,2,3,4,5,6,7,8的过程:
    结点1,没有右孩子,后继是最底层以1所在子树为左子树的祖先结点2;
    结点2,有右孩子,后继是右子树的最左结点3;
    结点3,没有右孩子,后继是最底层以3所在子树为左子树的祖先结点4;(2以3所在子树为右子树,固2不是)
    结点4,没有右孩子,后继是最底层以4所在子树为左子树的祖先结点5;
    结点5,有右孩子,后继是右子树的最左节点6;
    结点6,没有右孩子,后继是最底层以6所在子树为左子树的的祖先结点7;
    结点7,有右孩子,后继是右子树最左结点8;
    结点8,没有右孩子,没有以8所在子树为左子树的祖先结点,固8是尾节点;

    求后继伪代码

    TREE-SUCCESSOR(x)
        if x.right ≠ NIL
            return TREE-MINIMUM(x.right) // x右子树最左结点
    
        // 从x开始自底向上, 找到第一个以x所在子树为左子树的祖先结点
        y = x.p
        while y ≠ NIL and x == y.right
            x = y
            y = y.p
        return y
    

    前驱 PRESUCCESSOR

    设当前结点为x,前驱结点pre,pre.key是 ≤ x.key的最大值。

    pre.key = max{t.key | t.key <= x.key, t∈U}, pre = PRESUCCESSOR(x)

    如何找前驱结点?
    前驱分为下面2种主要情形:
    1.如果当前结点x有左子树,那么x的前驱是左子树的最右孩子;
    2.如果x没有左子树,那么x前驱y是x最近祖先,而且x所在子树是y的右子树。
    不过,情形2也可以通过判断父结点P和x的关系:
    1)如果x是P的左孩子,那么应该沿着P往上(根端)找父节点,直到找到结点Q,Q的右孩子即为x所在子树根节点。那么Q为x前驱。
    2)如果x是P的右孩子,那么x前驱是P;

    求前驱伪代码

    TREE-PRESUCCESSOR(x)
        if x.left ≠ NIL 
            return TREE-MAXIMUM(x.left)
        else
            y = x.p
            while y ≠ NIL and y.left == x
                x = y
                y = y.p
        
        return y
    

    插入和删除

    插入和删除操作都会改变二叉搜索树的结构,引起动态集合的变化。需要在修改后,维持二叉搜索树的性质不变。

    插入 INSERT

    插入结点z比较简单,分为两步:

    1. 按关键字搜索到合适位置;
    2. 在待插入位置插入新结点;
      新插入的结点一定是新的叶子节点

    插入结点循环版本伪代码:

    TREE-INSERT(T, z)
          /* 从root开始, 自上而下找到插入x.key的合适位置 */
          x = T.root
          while x ≠ NIL
                y = x
                if z.key < x.key
                      x = x.left
                else
                      x = x.right
    
          // 插入z, 作为x的子节点
          z.p = x;
          if y == NIL
               T.root = z
          if z.key < x.key
                x.left = z
          else
                x.right = z 
    

    插入结点递归版本伪代码:

    TREE-INSERT(T, z)
          x = T.root
          if x ≠ NIL // 非空树,递归查找应该插入z的位置,并且在对应位置插入z
                FIND-INSERT(x, z)
          else  // 空树, 插入结点即为根结点
                T.root = z
                z.p = NIL
    
    FIND-INSERT(x, z)
          if z.key < x.key 
                if x.left ≠ NIL
                      FIND-INSERT(x.left, z) // x左子树继续查找
                else // 已经查找到叶子结点,即当前结点x,直接插入z为x左孩子
                      x.left = z
                      z.p = x
          else
                if x.right ≠ NIL
                      FIND-INSERT(x.right, z)
                else // 已经查找到叶子结点,即当前结点x,直接插入z为x右孩子
                      x.right = z
                      z.p = x
    

    删除 DELETE

    从二叉搜索树删除结点z较为复杂,需要分情况讨论:

    1. 如果z没有孩子,直接删除z,调整z父结点;
    2. 如果z只有一个孩子,那么将z的孩子替换z,调整z父结点;
    3. 如果z有2个孩子,原则是找z的后继y替换z(y一定是在z的右子树),不过因为涉及到后继孩子结点的调整,需要分2种情况:
      1)y是z的右孩子(y没有左子树,如果右左子树必定是情形2)),那么y替换z,并且y的左孩子
      2)y不是z的右孩子,而是z右子树的最左孩子。此时,y替换z,y的右孩子x提升到y原来的位置;

    情形3.1:

    情形3.2

    为了便于编程,稍作调整,改成下面4种情况:

    1. 如果z没有左孩子,那么用z右孩子替换z(不论右孩子是否为空NIL);
    2. 如果z仅有一个左孩子,那么用z左孩子替代换z;
    3. 如果z既有左孩子,又有右孩子,那么在z的右子树找后继y。
      1)y是z右孩子(此时y必定没有左孩子),用y替换z,并且保留y的右孩子;
      2)y是z位于z右子树并且不是z右孩子(此时y同样没有左孩子),必定是z右子树最左结点。

    为方便二叉搜索树内部移动结点,定义子过程TRANSPLANT,表示用一颗子树替换另外一颗子树并称为其双亲的孩子结点。
    用一颗v为根子树替换u为根子树的伪代码

    /**
     * 从二叉搜索树T中,用v子树替换u子树,并且结点u父结点原u的孩子结点替换为v结点
     * 基本思想:
     * 首先判断父结点
     * 1.如果被替换的是根结点,就更新树的根结点信息
     * 2.如果不是,则更新父结点的孩子信息
     * 最后更新被替换结点包含的父结点信息
     */
    TRANSPLANT(T, u, v)
          if u.p == NIL // 如果被替换结点u是根结点
                T.root = v
          elseif u == u.p.left // 如果被替换结点是父结点左孩子
                u.p.left = v
          else u.p.right = v
    
          if v ≠ NIL
                v.p = u.p
    
    /**
     * 从二叉搜索树中删除结点z
     */
    TREE-DELETE(T, z)
          if z.left == NIL
                TRANSPLANT(T, z, z.right)
          elseif z.right == NIL
                TRANSPLANT(T,z,z.left)
          else y = TREE-MINIMUM(z.right) // y 为z右子树最左结点
                if y.p ≠ z // y不是z右孩子
                      // 此时, y是z右子树最左结点, y.left = NIL
                      TRANSPLANT(T, y, y.right) // 将y右子树y.right替换y, 此时y闲置
                      // 将z的右孩子转换成y的右孩子
                      y.right = z.right 
                      y.right.p = y 
                // y是z右孩子, 直接用y替换z
                TRANSPLANT(T,z,y)
                // 将z的左孩子替换成y的左孩子(原先y没有左孩子)
                y.left = z.left
                y.left.p = y
    

    参考

    1. 二叉搜索树的前驱节点和后继节点
  • 相关阅读:
    ubuntu下如何批量修改文件后缀名
    vanilla
    Ubuntu apt-get 彻底卸载软件包
    Kendall Rank(肯德尔等级)相关系数
    图像质量评估(IQA)
    conda常用命令
    在ubuntu中搜索文件或文件夹的方法
    libstdc++.so.6: version `GLIBCXX_3.4.21' not found
    迅雷磁力链接转BT种子工具
    springboot 集成mybatis plus3
  • 原文地址:https://www.cnblogs.com/fortunely/p/14048939.html
Copyright © 2020-2023  润新知