数据结构(七)二叉搜索树
一、概述
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重构:算法
实现
综合评价