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重构:算法

     实现

    综合评价

  • 相关阅读:
    Note/Solution 转置原理 & 多点求值
    Note/Solution 「洛谷 P5158」「模板」多项式快速插值
    Solution 「CTS 2019」「洛谷 P5404」氪金手游
    Solution 「CEOI 2017」「洛谷 P4654」Mousetrap
    Solution Set Border Theory
    Solution Set Stirling 数相关杂题
    Solution 「CEOI 2006」「洛谷 P5974」ANTENNA
    Solution 「ZJOI 2013」「洛谷 P3337」防守战线
    Solution 「CF 923E」Perpetual Subtraction
    KVM虚拟化
  • 原文地址:https://www.cnblogs.com/aidata/p/11572509.html
Copyright © 2011-2022 走看看