• 数据结构与算法(七)——二叉搜索树


    iwehdio的博客园:https://www.cnblogs.com/iwehdio/

    1、二叉搜索树

    • 二叉搜索树BST,在外形上借鉴了列表,可以看作是二维的列表。同时也借鉴了有序向量的特点。

    • 循关键码访问:

      • 数据项之间,依照各自的关键码彼此区分,类似车与车牌之间的关系。
      • 关键码之间需要支持大小比较和相等比对。可以统一的表示和实现为词条形式。
    • 词条:

      • 包括关键码和数据,提供了比较器和判等器。
      • 不同的词条之间可以进行比较和判等。
    • 二叉搜索树:

      • 节点、词条、关键码之间可以认为是等价的。
      • 顺序性:任意节点均不小于 / 不大于其 左 / 右后代。
      • 顺序性虽然只是局部特征,但是可以导出全局特征:单调性。BST的中序遍历序列,必然是单调非降的。(BST的充要条件)
    • BST接口:

      • 派生自二叉树。
      • 对于不同的BST实现,有不同的查找、插入和删除操作,在最坏情况下时间复杂度都是O(h),即树的高度。
      • 可以认为BST中没有重复节点。
    • 查找:

      • 从根节点开始,比较关键码的大小。根据结果,转入左子树或右子树。

      • 其实就是对二叉搜索树的中序遍历序列的结果,对该有序向量进行的二分查找。(减而治之)

      • 实现:

        template <typename T> BinNodePosi(T) & BST<T>::search ( const T & e ) { //在BST中查找关键码e
           if ( !_root || e == _root->data ) { _hot = NULL; return _root; } //在树根v处命中
           for ( _hot = _root; ; ) { //自顶而下
              BinNodePosi(T) & c = ( e < _hot->data ) ? _hot->lc : _hot->rc; //确定方向
              if ( !c || e == c->data ) return c; _hot = c; //命中返回,或者深入一层
           } //无论命中或失败,hot均指向v之父亲(或为NULL)
        } //返回目标节点位置的引用,以便后续插入、删除操作
        
      • 接口语义:

        • 返回值:成功时指向一个存在的关键码为目标值e的节点;失败时指向一个不存在的空节点,这个位置是指如果该树中存在目标值e则应在的位置。
        • 返回值总是指向命中节点,_hot指向的是指向命中节点的父亲。
    • 插入:

      • 先借助search确定插入位置及方向,再将新节点作为叶子插入。(默认插入值e尚不存在,查找返回的是对新孩子的引用)

      • 插入后更新全树规模和插入点及其祖先的高度。

      • 实现:

        template <typename T> BinNodePosi(T) BST<T>::insert ( const T& e ) { //将关键码e插入BST树中
           BinNodePosi(T) & x = search ( e ); if ( x ) return x; //确认目标不存在(留意对_hot的设置)
           x = new BinNode<T> ( e, _hot ); //创建新节点x:以e为关键码,以_hot为父
           _size++; //更新全树规模
           updateHeightAbove ( x ); //更新x及其历代祖先的高度
           return x; //新插入的节点,必为叶子
        } //无论e是否存在于原树中,返回时总有x->data == e
        
    • 删除:

      • 先借助search找到所要删除的节点e,然后进行具体的删除操作。

      • 情况1:如果节点e有一颗子树为空,可将其替换为其另一颗一棵子树。

      • 情况2:左右子树都不为空。

        • 首先,找出所要删除节点e的直接后继s。(即中序遍历下的后继,在右子树中不大于节点e的最小值)
        • 然后,将s与e的数据互换。因为s至多只有右子树,左子树必为空,所以在交换位置后再删除e就回到了第一种情况。
      • 新接替的节点需要记录其新的父亲,即原来被删除节点e的父亲。

      • 删除后更新全树规模和原删除点直接后继的父亲及其祖先的高度。

      • 实现:

        template <typename T> bool BST<T>::remove ( const T& e ) { //从BST树中删除关键码e
           BinNodePosi(T) & x = search ( e ); if ( !x ) return false; //确认目标存在(留意_hot的设置)
           removeAt ( x, _hot ); _size--; //实施删除
           updateHeightAbove ( _hot ); //更新_hot及其历代祖先的高度
           return true;
        } //删除成功与否,由返回值指示
        
        template <typename T>
        static BinNodePosi(T) removeAt ( BinNodePosi(T) & x, BinNodePosi(T) & hot ) {
           BinNodePosi(T) w = x; //实际被摘除的节点,初值同x
           BinNodePosi(T) succ = NULL; //实际被删除节点的接替者
           if ( !HasLChild ( *x ) ) //若*x的左子树为空,则可
              succ = x = x->rc; //直接将*x替换为其右子树
           else if ( !HasRChild ( *x ) ) //若右子树为空,则可
              succ = x = x->lc; //对称地处理——注意:此时succ != NULL
           else { //若左右子树均存在,则选择x的直接后继作为实际被摘除节点,为此需要
              w = w->succ(); //(在右子树中)找到*x的直接后继*w
              swap ( x->data, w->data ); //交换*x和*w的数据元素
              BinNodePosi(T) u = w->parent;
              ( ( u == x ) ? u->rc : u->lc ) = succ = w->rc; //隔离节点*w
           }
           hot = w->parent; //记录实际被删除节点的父亲
           if ( succ ) succ->parent = hot; //并将被删除节点的接替者与hot相联
           release ( w->data ); release ( w ); return succ; //释放被摘除节点,返回接替者
        } //release()负责释放复杂结构
        

    2、平衡二叉搜索树

    • 因为之前二叉搜索树的操作的时间复杂度都与树高成正比,所以希望评估树高:

      • 随机生成:一个长为n的关键码序列,随机顺序插入一棵二叉搜索树的平均高度是log n。
      • 随机组成:一棵有n个关键码的节点的二叉搜索树,进行随机组合,生成的平均高度是n^0.5。
      • 一般认为随机组成的结果更为可信。
    • 理想平衡:

      • 理想平衡:节点数目固定时,兄弟子树高度越接近(平衡),全树也倾向于更低。
      • 由n个节点组成的二叉树,高度不低于 log2 n。当达到这个值时成为理想平衡。
      • 但是理想平衡出现概率极低、维护成本过高,。
    • 适度平衡:

      • 高度渐进的不超过log n,即可称为适度平衡。
      • 适度平衡的BST,称为平衡二叉搜索树BBST。
      • 在操作过程中从BBST变为不平衡时,需要将其变换回BBST。
    • 等价二叉搜索树:

      • 结构不相同的BST,它们的中序遍历序列也可能是相同的。
      • 如果中序遍历序列相同但拓扑结构不同的,称为等价二叉搜索树。
      • 等价BST:上下可变(父子关系可能颠倒),但左右不乱。
    • 等价变换,旋转跳转:

      • zig:右旋。新的根节点是原来根节点的左子树,原来的根节点变成新的根节点的右子树,新的根节点原来的右子树变为的原来的根节点的左子树。

      • zag:左旋。

      • 等价变换的计算时间在O(1)内。且各节点的高度变化不超过1。

      • 将失衡的BBST恢复平衡的操作次数不应该超过O(log n)。

    3、AVL树

    • AVL树:一种适度平衡的二叉搜索树。

    • 平衡因子:

      • 用于评价AVL意义下的适度平衡。
      • 定义为该节点左子树高度减去右子树高度。(空树的高度为-1,只有根节点的为0)
      • AVL要求各个节点的平衡因子在-1到1之间。
    • AVL树是适度平衡的:

      • 高度为h的AVL树,至少包含S(h)=fib(h+3)-1个节点。(可由S(h)=1+S(h-1)+S(h-2)递推求得)
    • 失衡与重平衡:

      • 插入节点可能会使得多个节点失衡。这是因为插入操作只会使其祖先节点的高度变化。
      • 删除节点最多只会让该节点的父亲失衡。这是因为删除操作如果导致失衡,那么这个节点的祖先节点的高度都不会变化。
    • 插入操作:

      • 如果插入节点使得多个祖先节点失衡,将对发生失衡的最低的祖先记为g,至少为插入节点的祖父。

      • 单旋(插入T2/T3):如果插入位置在g的单调位置(左子树的左子树、右子树的右子树),则只需单旋(zag-zag)。

      • 双旋(插入T1/T2):如果插入位置在g的非单调位置(左子树的右子树、右子树的左子树),则需要双旋(zig-zag)。

      • 如果插入节点使得多个祖先节点失衡,对其最低的失衡祖先e进行旋转复衡即可。

      • e经过旋转调整后复衡,e为根的子树高度复原,使得更高的祖先也必平衡,全树复衡。

      • 实现:

        template <typename T> BinNodePosi(T) AVL<T>::insert ( const T& e ) { //将关键码e插入AVL树中
           BinNodePosi(T) & x = search ( e ); if ( x ) return x; //确认目标节点不存在
           BinNodePosi(T) xx = x = new BinNode<T> ( e, _hot ); _size++; //创建新节点x
        // 此时,x的父亲_hot若增高,则其祖父有可能失衡
           for ( BinNodePosi(T) g = _hot; g; g = g->parent ) { //从x之父出发向上,逐层检查各代祖先g
              if ( !AvlBalanced ( *g ) ) { //一旦发现g失衡,则(采用“3 + 4”算法)使之复衡,并将子树
                 FromParentTo ( *g ) = rotateAt ( tallerChild ( tallerChild ( g ) ) ); //重新接入原树
                 break; //g复衡后,局部子树高度必然复原;其祖先亦必如此,故调整随即结束
              } else //否则(g依然平衡),只需简单地
                 updateHeight ( g ); //更新其高度(注意:即便g未失衡,高度亦可能增加)
           } //至多只需一次调整;若果真做过调整,则全树高度必然复原
           return xx; //返回新节点位置
        } //无论e是否存在于原树中,总有AVL::insert(e)->data == e
        
    • 删除操作:

      • 如果删除节点使得多个祖先节点失衡,将对发生失衡的最低的祖先记为g,可能是删除节点从父亲开始的所有祖先。

      • 插入会导致该子树高度增加而失衡,删除会导致该子树高度降低而失衡。将失衡的节点中高度较高的子树位置称为失衡位置v。

      • 单旋(删除T3):如果失衡位置在g的单调位置(左子树的左子树、右子树的右子树),则单旋(zig-zig)后使得局部复衡。

      • 双旋(删除T3):如果失衡位置在g的非单调位置(左子树的右子树、右子树的左子树),则双旋(zag-zig)后使得局部复衡。

      • 因为复衡后,子树的高度未必复原(如果单旋中T2的最后一个节点不存在;而双旋中高度必减一),所以更高层的祖先仍然可能失衡。

      • 因为有失衡传播现象,可能需要做O(log n)次调整。

      • 实现:

        template <typename T> bool AVL<T>::remove ( const T& e ) { //从AVL树中删除关键码e
           BinNodePosi(T) & x = search ( e ); if ( !x ) return false; //确认目标存在(留意_hot的设置)
           removeAt ( x, _hot ); _size--; //先按BST规则删除之(此后,原节点之父_hot及其祖先均可能失衡)
           for ( BinNodePosi(T) g = _hot; g; g = g->parent ) { //从_hot出发向上,逐层检查各代祖先g
              if ( !AvlBalanced ( *g ) ) //一旦发现g失衡,则(采用“3 + 4”算法)使之复衡,并将该子树联至
                 g = FromParentTo ( *g ) = rotateAt ( tallerChild ( tallerChild ( g ) ) ); //原父亲
              updateHeight ( g ); //并更新其高度(注意:即便g未失衡,高度亦可能降低)
           } //可能需做Omega(logn)次调整——无论是否做过调整,全树高度均可能降低
           return true; //删除成功
        } //若目标节点存在且被删除,返回true;否则返回false
        
    • 3+4重构:

      • 在实际的算法实现中,不必要使用多次左旋和右旋,而是使用3+4重构。

      • 算法思路:按其中序遍历次序重构。

      • 实现:

      • 根据何种规则重命名:

        • 具体来说应该是先找到发生失衡的最低的祖先g,再依此寻找较高的子树部分的p和v。
        • 传入v的理由是可以直接唯一的找出p和g。

    • AVL树:

      • 优点:无论查找、插入或删除,最坏情况下复杂度均为O(log n)。存储空间仍为O(n)。
      • 缺点:
        • 人为引入了平衡因子,需要改造元素结构。
        • 实测复杂度与理论值尚有差距。
        • 单次动态调整中,全局拓扑结构的变化量可能高达Ω(log n)。(对于插入是O(1),对于删除是O(log n))。

    iwehdio的博客园:https://www.cnblogs.com/iwehdio/
  • 相关阅读:
    postgresql 的统计信息
    postgresql 查看表、列的备注信息
    redis 4.0.9 cluster + startup stop
    redis 4.0.9 cluster + failover
    oracle ebs r12 打补丁的步骤
    centos 7.4 + redis 4.0.9 cluster + make
    pgpool running mode
    pgpool + streaming replication mode + slave down up
    pgpool 的安装之一
    postgresql 函数的三个状态
  • 原文地址:https://www.cnblogs.com/iwehdio/p/14152307.html
Copyright © 2020-2023  润新知