数据结构(七)二叉搜索树

一、概述

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

 实现

综合评价

原文地址:https://www.cnblogs.com/aidata/p/11572509.html