zoukankan      html  css  js  c++  java
  • 数据结构(七)二叉搜索树

    一、概述

    BST继承了二叉树也就是列表结构的特点,也借鉴了有序向量的特点和优势。

    BBST平衡二叉搜索树这个子集尤其重要

    1.循关键码访问

    数据项之间,依照各自的关键码彼此区分,call-by-key

    条件:关键码之间支持大小比较与相等比对

    数据集合中的数据项统一地表示和实现为词条entry形式

    词条

    template <typename K, typename V> struct Entry
    { // 词条模板类
        K key; V value; // 关键码、数值
        Entry(K k = K(), V v = V()) : key(k), value(v) {}; // 默认构造函数
        Entry(Entry<K, V> const & e) : key(e.key), value(e.value) {}; // 克隆
        // 比较器、判断器(从此,不必严格区分词条及其对应的关键码)
        bool operator< (Entry<K, V> const & e) { return key < e.key; } // 小于
        bool operator> (Entry<K, V> const & e) { return key > e.key; } // 大于
        bool operator== (Entry<K, V> const & e) { return key == e.key; } // 等于
        bool operator!= (Entry<K, V> const & e) { return key != e.key; } // 不等于
    
    };

    Binary Search Tree:节点~词条~关键码

    每个节点存有一个词条,一个词条唯一对应一个关键码,后面一般会直接把节点、词条和关键码等同起来。

     BST特征:处处满足顺序性

    顺序性:任一节点均不小于/不大于左/右后代

    两个反例,左不是二叉树;右,3的右后代有一个2,小于3,违反了顺序性

    把2换为7即可

     单个节点二叉树必然是BST

     只有一个单分支,满足顺序性,依然是BST

     为简化起见,禁止重复词条

    这种简化应用中不自然,算法上无必要

    顺序性虽然只是对局部特征的刻画,但由此却可导出某些全局特征...

    单调性:BST的中序遍历序列,必然单调非降

    单调性是BST的充要条件(对树高做数学归纳即可),即BST都有该特征,有该特征的二叉树必然是BST

     上图中序遍历,以关键码为序,单调排列           二叉树的节点垂直投影实际就是中序遍历序列

    必要性:考察树根,

    BST模板类

    template <typename T> class BST : public BinTree
    {   //  由BinTree派生
    public: // 以virtual修饰,以便派生类重写
        virtual BinNodePosi(T) & search(const T &); // 查找
        virtual BinNodePosi(T) insert(const T &); // 插入
        virtual bool remove(const T &); // 删除
    protected:
        BinNodePosi(T) _hot; // 命中节点的父亲
        BinNodePosi(T) connect34(// 3 + 4 重构
            BinNodePosi(T), BinNodePosi(T), BinNodePosi(T),
            BinNodePosi(T), BinNodePosi(T), BinNodePosi(T), BinNodePosi(T)
        );
        BinNodePosi(T) rotateAt(BinNodePosi(T)); // 旋转调整
    };

    二、算法及实现

    查找

     查找22,从根节点16开始,左后代都小于16,从右后代开始找

    转入右子树,与子树根节点比较,小于根节点

    转入左子树,大于子树根节点

    转入右子树,找到

    背后是减而治之策略:从根节点出发,逐步缩小查找范围,直到发现目标(成功),或查找范围缩小至空树(失败)

    对照中序遍历序列可见,整个过程可视作是在仿效有序向量的二分查找。

    实现

    template<typename T> BinNodePosi(T) & BST<T>::search(const T & e)
    {return searchTn(_root, e, _hot = NULL }; // 从根节点启动查找
    
    static BinNodePosi(T) & searchIn( // 典型的尾递归,可改为迭代版
        BinNodePosi(T) & v, // 当前(子)树根
        const T & e, // 目标关键码
        BinNodePosi(T) & hot) // 记忆热点
    {
        if (!v || (e == v->data))return v; // 足以确定失败、成功,或者
        hot = v; // 先记下当前(非空)节点,然后再...
        return searchIn((e < v->data) ? v->lChild : v->rChild), e, hot);
    } // 运行时间正比于返回节点v的深度,不超过树高O(h)

    接口语义

    返回的引用值:成功时,指向一个关键码为e且真实存在的节点

                             失败时,指向最后一次试图转向的空节点NULL

    _hot

     失败时,不妨假想地将此空节点,转换为一个数值为e的哨兵节点,如此,依然满足BST的充要条件;而且更重要地...无论成功与否,返回值总是等效地指向命中节点,而_hot总指向命中节点的父亲

    插入

    先借助search(e)确定插入位置及方向,再将新节点作为叶子插入

    若e尚不存在,则:_hot为新节点的父亲,v=search(e)为_hot对新孩子的引用

     插入40的位置如下,再插入55

     插入后

     实现

    template <typename T> BinNodePosi(T) BST<T>::insert(const T & e) {
        BinNodePosi(T) & x = search(e); // 查找目标(留意_hot的设置)
        if (!x) { // 禁止雷同元素,故仅在查找失败时才实施插入操作
            x = new BinNode<T>(e, _hot); // 在x处创建新节点,以_hot为父亲
            _size++; updateHeightAbove(x); // 更新全树规模,更新x及其历代祖先的高度
        }
        return x; // 无论e是否存在于原树中,至此总有x->data==e
    } // 验证:对于首个节点插入之类的边界情况,均可正确处置

    删除

    template <typename T> bool BST<T>::remove(const T & e) {
        BinNodePosi(T) & x = search(e); // 定位目标节点
        if (!x) return false; // 确认目标存在(此时_hot为x的父亲)
        removeAt(x, _hot); // 分两大类情况确认实施删除,更新全树规模
        _size--; // 更新全树规模
        updateHeightAbove(_hot); // 更新_hot及其历代祖先的高度
        return true;
    } // 删除成功与否,由返回值指示

    情况一:

    若*x(69)的某一子树为空,则可将其替换为另一子树(64)

    验证:如此操作之后,二叉搜索树的拓扑结构依然完整,顺序性依然满足

    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)) succ = x = x->rChild; // 左子树为空
        else if (!HashRChild(*x)) succ = x = x->lChild; // 右子树为空
        else { /**左右子树并存的情况,即下面的第二种情况**/ }
        hot = w->parent; // 记录实际被删除节点的父亲
        if (succ) succ->parent = hot; // 将被删除节点的接替者于hot相联
        release(w->data); release(w); // 释放被摘除节点
        return succ; // 返回接替者
    }//此类情况仅需O(1)时间

    情况二 两边子树都存在

    化繁为简

    假设删除节点36, 找到直接后继,前面的BinNode::succ()就是找到中序遍历下的直接后继

    也就是全树中不小于该节点的最小的节点

    会先进入右子树,然后沿左侧分支不断下行,直到最终不能继续下行,40就是直接后继节点

    将直接后继节点和要删除的节点互换,右图所示

    然后,按照情况一的方法删除即可

    最终结果,_hot指向被实际删除节点的父亲,并从该变量开始不断向上追溯历代祖先,因为这些祖先的高度可能因为后代的删除而发生变化

     将上面的else补充完整

    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)) succ = x = x->rChild; // 左子树为空
        else if (!HashRChild(*x)) succ = x = x->lChild; // 右子树为空
        else { /**左右子树并存的情况**/ 
            w = w->succ(); swap(x->data, w->data); // 令*x与其后继*w互换数据
            BinNodePosi(T) u = w->parent; // 原问题即转化为,摘除非二度的节点w
            (u == x ? u->rChild : u->lChild) = succ = w->rChild;
        }
        hot = w->parent; // 记录实际被删除节点的父亲
        if (succ) succ->parent = hot; // 将被删除节点的接替者于hot相联
        release(w->data); release(w); // 释放被摘除节点
        return succ; // 返回接替者
    }

    O(h)

    三、平衡与等价

    随机生成

    关键码总数为3时,所有的排列

    n时,有n!棵,可证明n!棵BST平均高度为log(n)

    随机组成

    catalan(n),O(根号n)

    理想平衡

    节点数目固定时,兄弟子树高度越接近(平衡),全树也将倾向于更低

    由n个节点组成的二叉树,高度不低于log2n,恰为log2n时,称作理想平衡

    最理想的是下面的完全二叉树和满二叉树,但在实际中可遇不可求

    适度平衡

    理想平衡出现概率极低、维护成本高,故须适当放松标准:

     高度渐进地不超过O(logn),即可称作适度平衡

    适度平衡的BST,称作平衡二叉搜索树(BBST)

    等价BST

    结构不同的BST,中序遍历序列可能是相同的

    中序遍历序列的歧义性

     这种拓扑结构不同,中序遍历序列却相同的BST,称为等级BST

    规律:

    上下可变:联接关系不尽相同,承袭关系可能颠倒

    左右不乱:中序遍历序列完全一致,全局单调非降

    等价变换+旋转调整

     变换前后中序遍历序列不变

    看成围绕v做顺时针变换

    第二种围绕v逆时针旋转

    在设计所有等价变换的组合时,不能忘了遵循两个重要的准则,一是所谓的局部性,我们执行的每次等价变换都应该局限在某一常数规模的局部,zig和zag都局限在v和c两个节点上,O(1);将刚刚失衡的BST恢复为BBST的过程中,累计需要执行的操作的次数不要过多,比如不能超过O(logn),这样就可有效控制整个操作序列的长度以及总体所需要的时间。

    四、AVL树

    AVL树为代表的BBST,即使在某次操作后不再满足BBST的条件,可以通过等价变换迅速地重新转化为BBST,转化的代价不超过O(longn)

    对包括AVL树在内的各种BBST而言,核心技巧无非两条:第一,界定适度的平衡标准,其次,一整套重平衡的技巧和算法。

    平衡因子

     1的叶子节点,左右子树为空-1减-1等于0

    2的节点,右子树高度为-1,左子树高度为0,平衡因子为1

    6的节点,左子树高度为-1,右子树高度为0,平衡因子为-1

    11的节点,左子树高度为1,右子树高度为0,平衡因子为1

    3的节点,左子树高度为1,右子树高度为2,平衡因子为-1

     上面的树,平衡因子绝对值小于等于1,因此是AVL树

    AVL=适度平衡

     高度为h的AVL树,至少包含S(h)=fib(h+3)-1个节点

    接口

    #define Balanced(x) (stature((x).lc) == stature((x).rc)) // 理想平衡条件
    #define BalFac(x) (stature((x).lc) - stature((x).rc)) // 平衡因子
    #define AvlBalanced(x) ((-2<BalFac(x) && (BalFac(x)<2)) // AVL平衡条件
    template <typename T> class AVL : public BST<T>
    { // 由BST派生
    public: // BST::search()等接口,可直接沿用
        BinNodePosi(T) insert(const T &); // 插入重写
        bool remove(const T &); // 删除重写
    };

    失衡与平衡

    插入:单旋

    同时可有多个失衡节点,最低者g不低于x祖父

    g经单旋调整后复衡,子树高度复原;更高祖父也必平衡,全树复衡

     对失衡的g逆时针做zag旋转

     首先引入临时的引用指向节点p

     p的左子树T1成为p的右子树

     令g成为p的左孩子

     将局部子树的根由g替换为p

    临时引用完成了历史使命,退出,旋转操作完成

     整理

     可以看到复衡后的子树高度又恢复到最初以第二条线为基准的情况

    在局部子树高度复原之后,所有祖先会统一地恢复平衡,全树因此复衡

    O(1)

    zagzag

    双旋

    zigzag

    zagzig

    同时可有多个失衡节点,最低者g不低于x祖父

    g经双旋调整后复衡,子树高度复原;更高祖先也必平衡,全树复衡

     首先,围绕节点p做顺时针的

     

     

     

     

     围绕节点g做逆时针zag旋转

     

     

     整理

    实现

    删除:单旋

    同时至多一个失衡节点g,首个可能就是x的父亲_hot

    g经单旋调整后复衡,子树高度未必复原;更高祖先仍可能失衡

    因有失衡传播现象,可能需要做O(logn)次调整

    删除:双旋

    同时至多一个失衡节点g,首个可能就是x的父亲_hot

     实现

    3+4重构:算法

     实现

    综合评价

  • 相关阅读:
    正方形_自适应_移动端
    meta name="viewport" content="width=device-width,initial-scale=1.0"
    :before/:after与::before/::after的区别 和属性content:值
    布局:flex弹性布局_兼容性写法
    布局:文本多列布局 column-* :
    布局:网格布局
    clear
    布局:盒模型 box-sizing : border-box ;
    object-fit : cover; 对象(图片和视频对象)
    布局:flex弹性布局_实践02
  • 原文地址:https://www.cnblogs.com/aidata/p/11572509.html
Copyright © 2011-2022 走看看