二叉搜索树
概念
循关键码访问
数据各项之间,依照各自的关键码彼此区分。其中关键码需要满足以下条件
数据集合中的数据项统一表示和实现为词条(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} }
|
BST
二叉搜索树(Binary Search Tree)首先是一颗二叉树,其次处处满足顺序性,即它的任一节点不小于其左后代或任一节点不大于其右后代。
- 顺序性:为局部的特征,但考察BST的中序遍历时可以发现它必然是单调非降的。
- 这一性质是BST的充要条件
例如下面的一颗BST
根据中序遍历的顺序,列出访问的节点,可以看出其值为单调递增的。
BST模板
template <typename T> class BST:public BinTree<T>{ public: virtual BinNodePosi(T) & search(const T &); virtual BinNodePosi(T) insert(const T &); virtual bool remove(const T &); protected: BinNodePosi(T) _hot; BinNodePosi(T) connect34( BinNodePosi(T), BinNodePosi(T), BinNodePosi(T), BinNodePosi(T), BinNodePosi(T), BinNodePosi(T), BinNodePosi(T)); BinNodePosi(T) rotateAt(BinNodePosi(T)); }
|
算法实现
查找
使用减而治之的方法,从根节点出发,逐步缩小查找范围,直到发现目标,或查找到空树,查找失败。
上面的例子需要查找到23,箭头方向表明了查找的路径,最后查找到了叶节点22,因此查找失败。整个过程可以视为在仿效有序向量的二分查找。
实现方法一:
template <typename T> BinNodePosi(T) & BST<T>::search(const T & e){ return searchIn(_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且真实存在的节点,
_hot指向返回命中节点的父亲
- 失败时:指向最后一次试图转向空节点NULL,
_hot指向最后一次转向的真实节点
空节点意思是其数值为NULL
- 在失败时,可以假象那个空节点为哨兵节点,并将其关键值假象为目标关键码
- 引入假象哨兵后,相当于返回值总是等效于命中节点,
_hot总是指向命中节点的父亲
插入
过程:
- 借助
search(e)确定插入位置和方向,再将新节点作为叶子插入
- 若e不存在
_hot为新节点的父亲
v = search(e)为_hot对新孩子的引用
- 令
_hot通过v指向新节点
实现
template <typename T> BinNodePosi(T) BST<T>::insert(const T & e){ BinNodePosi(T) & x = search(e); if(!x){ x = new BinNode<T>(e, _hot); _size++; updateHeightAbouve(x); } return x; }
|
此算法主要的时间消耗在于search(e)和updateHeightAbove(x)两个函数都线性正比于返回节点x的深度,不超过树高O(h)。
删除
template <typename T> bool BST<T>::remove(const T & e){ BinNodePosi(T) & x = search(e); if(!x) return false; removeAt(x, _hot); _size--; updateHeightAbove(_hot); return true; }
|
此算法在不考虑removeAt(x, _hot)时,时间主要消耗仍然在于search(e)和updateHeightAbove(x),累计的时间消耗也是O(h)。接下来考虑removeAt(x, _hot)的情况
情况一
如果删除的节点还有左孩子或者右孩子,只需要将对象删除,并且以它的子节点作为新进节点替代被删除的节点。这样可以保持BST的拓扑结构,也满足顺序性。
template <typename T> static BinNodePosi(T) removeAt(BinNodePosi(T) & x, BinNodePosi(T) & hot){ BinNodePosi(T) w = x; BinNodePosi(T) succ = NULL: if(!HasLChild(*x)) succ = x = x->rChild; else if(!HasRChild(*x)) succ = x = x->lChild; else{ } hot = w->parent; if(succ) succ->parent = hot; release(w->data); release(w); return succ; }
|
在这种情况下,只需要O(1)的时间。当左右孩子都为空时,succ指向NULL,上面的代码仍然正确。
情况二
当左右孩子并存的时候,需要化繁为简。此处需要用到在二叉树中实现的一个接口BinNode::succ()该接口的作用是返回当前节点在中序遍历下的直接后继。找到之后将当前需要删除的节点和其直接后继调换位置,这时是一个中间状态,已经不再是一颗BST了。最后删除在直接后继位置处的目标节点,完成节点删除,重新变成一颗BST。
template <typename T> static BinNodePosi(T) removeAt(BinNodePosi(T) & x, BinNodePosi(T) & hot){ else{ w = w->succ(); swap(x->data, w->data); BinNodePosi(T) u = w->parent; (u == x ? u->rChild : u->lChild) = succ = w->rChild; } }
|
其直接后继至多只有一个右孩子,因为作为直接后继,它一定是某条分支的左侧末端 。此时的时间消耗主要在于succ()其正比于x的高度,因此search()和succ()共不超过O(h) 。
平衡等价
两种口径
- 随机生成
对于n个互异的词条{e1,e2,.....,en}对任一排列σ={ei1,ei2,.....,ein},从空树开始,反复调用insert()接口将各词条依次插入,得到T(σ),与σ对应的T(σ)称为由σ随机生成的BST。
- 任一排列作为输入的概率均等,为n!1
- 由n个互异词条随机生成的BST平均高度为Θ(logn)。
- 随机组成
对于n个互异的词条,在遵循顺序性的前提下,可随机确定拓扑连接关系。由此所得的BST称为这组词条的随机组成。
- 由n个互异词条随机组成的BST,若共计T(n)棵,T(n)=catalan(n)=∑k=1nSP(k−1)⋅SP(n−k)。
- 所有BST等概论出现,其平均高度为Θ(n)。
按照两种口径的平均性能,随机组成更为可信。因为在随机生成的过程中,不同的随机序列可能生成同一棵BST
两种平衡
- 理想平衡
- 节点数目相对固定时,兄弟子树高度越接近平衡,全树也将倾向于更低。
- 由n个节点组成的二叉树,高度不低于log2n,当正好等于log2n时为理想平衡
- 理想平衡时相当于完全树甚至满树,此时条件太苛刻
- 适度平衡
- 理想平衡出现概率极低,且维护成本过高,需要适当放松标准
- 高度渐进,不超过O(logn),称为适度平衡
- 适度平衡BST称为平衡二叉搜索树(BBST)
等价BST
- 上下可变:连接关系不尽相同,承袭关系可能颠倒
- 左右不乱:中序遍历序列完全一致,全局单调非降
如图这个例子可以看出其中序遍历序列完全相同,但其部分子序列拓扑结构并不相同。这样一对BST称为等价的BST。
对于各种BBST,将BST转换为BBST时,需要限制1. 单次动态修改操作后,至多O(1)处局部不再满足限制条件,2. 可以在O(logn)时间内,使得这些局部满足更新。
AVL树
- AVL树是一种BBST,在AVL的标准下的平衡因子为
balFac(v) = height(lc(v)) - height(rc(v))
- AVL树即是对任意的节点,其平衡因子都不超过1也不小于-1的BST。
- AVL树是适度平衡的,其高度不超过O(logn)
接口
# define Balanced(x) (stature((x).lChild) == stature((x).rChild)) // 理想平衡 # define BalFac(x) (stature((x).lChild) - stature((x).rChild)) // 平衡因子 # define AvlBalabced(x) ((-2 < BalFac(x)) && (BalFac(x) < 2)) // AVL平衡条件 template <typename T> class AVL:public BST<T> { // 继承自BST public: // 沿用BSF::search()接口 BinNodePosi(T) insert(const T &); // 插入重写 bool remove(const T &); // 删除重写 };
|
在按照BST规则插入或删除节点后,AVL的平衡性会被破坏,因此需要借助等价变换
- 局部性:所有旋转都在局部进行 (每次仅需要O(1)时间)
- 快速性:在每一深度只需检查并旋转至多一次 (共O(logn)次)
插入操作
- 单旋插入
如上图所示,如果需要在v下面插入左孩子或者右孩子,则需要让g单旋调整,具体过程为:
- 引入临时引用,指向节点p
- 令p的左子树T1变为g的右子树
- 令g为p的左孩子
- 局部子树的根g替换为p
操作完成之后,局部子树的高度恢复,其更高的祖先也必然是平衡的,使得全树复衡。
- 双旋插入
同时可有多个失衡及诶单,最低者g不低于x的祖父,g经过双旋调整后复衡,子树高度复原,其更高的祖先也必然是平衡的,使得全树复衡。
其过程为:
- 围绕p顺时针旋转,
zig(p)
- 引入临时变量,指向节点v
- 令v的右子树变为p的左子树
- 令p为v的右孩子
- 令g的右孩子为v
- 围绕节点g做一次逆时针旋转,
zag(g)
- 将临时变量指向节点v
- 令v的左子树变为g的右子树
- 令g为v的左孩子
- 局部子树的根由g替换为v
实现
template <typename T> BinNodePosi(T) AVL<T>::insert(const T & e){ BinNodePosi(T) & x = search(e); if (x) return x; BinNodePosi(T) xx = x = new BinNode<T>(e, _hot); _size++; for (BinNodePosi(T) g = _hot; g; g = g->parent){ if (! AvlBalanced(*g)){ FromParentTo(*g) = rotateAt(tallerChild(tallerChild(g))); break; }else updateHeight(g); } return xx; }
|
删除操作
-
单旋删除
在图中这种情况下,g、p、v是朝同一个方向排列。将T3的一个叶节点删除,会引起g点失衡。因此需要围绕点g进行一次zig操作。
当T2最后的那个节点不存在,即调整后的子树高缩减了1,因此有可能引起更上一层的失衡,称为失衡传播现象,可能需要做O(logn)次调整。
-
双旋删除
在此图中,g、p、v并不是朝着同一个方向排列,此时删除T3的一个节点。
这种情况下首先要围绕p做一次zag旋转,再围绕g做一次zig旋转。
旋转完成后,情况和单旋类似,T1和T2这两棵子树可能其中一个会存在一个叶节点,那么旋转之后子树的高度缩减1,仍可能引起失衡,可能需要做O(logn)次调整。
实现
template<typename T> bool AVL<T>::remove(const T & e){ BinNodePosi(T) & x = search(e); if(!x) return false; removeAt(x, _hot); size--; for (BinNodePosi(T) g = _hot; g; g = g->parent){ if (!AvlBalanced(*g)) g = FromParentTo(*g) = rotateAt(tallerChild(tallerChild(g))); updateHeight(g); } return true; }
|
其中for循环可能要做Ω(logn)次调整
3+4重构
设g(x)为最低的失衡节点,考察祖孙三代:g、p、v,按照中序遍历次序,将其重命名为a<b<c。则他们共拥有互不相交的四棵(可能为空)的子树,按照中序遍历次序命名为T0<T1<T2<T3。将原来以g为根的子树替换为一颗新的子树S′。
实现
template <typename T> BinNodePosi(T) BST<T>::connect34( BinNodePosi(T) a, BinNodePosi(T) b, BinNodePosi(T) c, BinNodePosi(T) T0, BinNodePosi(T) T1, BinNodePosi(T) T2, BinNodePosi(T) T3,){ a->lChild = T0; if (T0) T0->parent = a; a->rChild = T1; if (T1){ T1->parent = a; updateHeight(a); } c->lChild = T2; if (T2) T2->parent = c; c->rChild = T3; if (T3){ T3->parent = c; updateHeight(c); } b->lChild = a; a->parent = b; b->rChild = c; c->parent = b; updateHeight(b); return b; }
|
将rotateAt()完整化
template<typenmae T> BinNodePosi(T) BST<T>::rotateAt(BinNodePosi(T) v){ BinNodePosi(T) p = v->parent; g = p->parent; if(IsLChild(*p)){ if(IsLChild(*v)){ p->parent = g->parent; return connect34(v, p, g, v->lChild, v->rChild, p->rChild, g->rChild); }else{ v->parent = g->parent; return connect34(p, v, g, p->lChild, v->lChild, v->rChild, g->rChild); } }else{ if(IsRChild(*v)){ p->parent = g->parent; return connect34(g, p, v, g->lChild, p->lChild, v->lChild, v->rChild); }else{ v->parent = g->parent; return connect34(g, v, p, g->lChild, v->lChild, v->rChild, p->rChild); } } }
|
zig-zig 和zig-zag分别对应于:
剩下两种情况恰好与之相反。
AVL树性能
优点:
- 无论查找、插入或删除,最坏情况下的复杂度均为O(logn),存储空间为O(n)
缺点: