第7章 搜索树
总之,若既要求对象集合的组成可以高效率地动态调整,同时也要求能够高效率地查找,则以上线性结构(如向量、列表、栈和队列),均难以胜任。
那么,高效率的动态修改和高效率的静态查找,究竟能否同时兼顾?如有可能,又应该采用什么样的数据结构?接下来的两章,将逐步回答这两个层次的问题。
本章将首先介绍树式查找的总体构思、基本算法以及数据结构,通过对二分查找策略的抽象与推广,定义并实现二叉搜索树结构。尽管就最坏情况下的渐进时间复杂度而言,这一方法与此前相比并无实质的改进,但这部分内容依然十分重要——基于半线性的树形结构的这一总体构思,正是后续内容的立足点和出发点。
7.1 查找
所谓的查找或搜索,指从一组数据对象中找出符合特定条件者,这是构建算法的一种基本而重要的操作。其中的数据对象,统一地表示和实现为词条(entry)的形式;不同的词条之间,依照各自的关键码(key)彼此区分。
循关键码访问:查找的过程与结果,仅仅取决于目标对象的关键码。其与此前的“循秩访问”和“循位置访问”等完全不同,这一新的访问方式,与数据对象的物理位置和逻辑次序均无关。
词条对象拥有成员变量key和value。
7.2 二叉搜索树
二叉搜索树(Binary Search Tree, BST)
顺序性
任一节点r的左(右)子树中,所有节点(若存在)均不大于(不小于)r
回避边界情况,暂定所有节点互不相等:所有任一节点r的左(右)子树中,所有节点(若存在)均小于(大于)r
中序遍历序列:
任何一棵二叉树是二叉搜索树,当且仅当其中序遍历序列单调非降
BST模板类:
0001 #include "BinTree/BinTree.h" //引入BinTree
0002
0003 template <typename T> class BST : public BinTree<T> { //由BinTree派生BST模板类
0004 protected:
0005 BinNodePosi<T> _hot; //“命中”节点的父亲
0006 BinNodePosi<T> connect34 ( //按照“3 + 4”结构,联接3个节点及四棵子树
0007 BinNodePosi<T>, BinNodePosi<T>, BinNodePosi<T>,
0008 BinNodePosi<T>, BinNodePosi<T>, BinNodePosi<T>, BinNodePosi<T> );
0009 BinNodePosi<T> rotateAt ( BinNodePosi<T> x ); //对x及其父亲、祖父做统一旋转调整
0010 public: //基本接口:以virtual修饰,强制要求所有派生类(BST变种)根据各自的规则对其重写
0011 virtual BinNodePosi<T> & search ( const T& e ); //查找
0012 virtual BinNodePosi<T> insert ( const T& e ); //插入
0013 virtual bool remove ( const T& e ); //删除
0014 };
查找算法及其实现
-
算法:二叉搜索树的查找算法,亦采用了减而治之的思想与策略,其执行过程可描述为:
从树根出发,逐步地缩小查找范围,直到发现目标——成功 / 缩小至空树——失败
-
searchIn()算法与search()接口:searchIn()算法使用递归实现,search()接口通过调用searchIn()算法实现
-
语义约定:
在调用searchIn()算法之前,search()接口首先将内部变量_hot初始化为NULL,然后作为引用型参数hot传递给searchIn()。在整个查找的过程中,__hot变量始终指向当前节点的父亲。
- 效率:在最好情况下,目标关键码恰好出现在树根处(或其附近),此时只需O(1)时间。然而不幸的是,对于规模为n的二叉搜索树,深度在最坏情况下可达Ω(n)。
插入算法及其实现
-
算法
-
insert()接口的实现
0001 template <typename T> BinNodePosi<T> BST<T>::insert ( const T& e ) { //将关键码e插入BST树中 0002 BinNodePosi<T> & x = search ( e ); if ( x ) return x; //确认目标不存在(留意对_hot的设置) 0003 x = new BinNode<T> ( e, _hot ); //创建新节点x:以e为关键码,以_hot为父 0004 _size++; //更新全树规模 0005 updateHeightAbove ( x ); //更新x及其历代祖先的高度 0006 return x; //新插入的节点,必为叶子 0007 } //无论e是否存在于原树中,返回时总有x->data == e -
效率
时间复杂度也同样取决于新节点的深度,在最坏情况下不超过全树的高度。
删除算法及其实现
为从二叉搜索树中删除节点,首先也需要调用算法BST::search(),判断目标节点是否的确存在于树中。若存在,则需返回其位置,然后方能相应地具体实施删除操作。
-
单分支情况
(a):search(69)直接定位,再替换、释放。
-
双分支情况
(b)(c)(d):search(36)定位,BinNode::succ()找到直接后继,交换,转化为单分支情况,再替换、释放
-
remove()
一般地,删除关键码e的过程,可描述为如代码7.6所示的函数remove()。
0001 template <typename T> bool BST<T>::remove ( const T& e ) { //从BST树中删除关键码e 0002 BinNodePosi<T> & x = search ( e ); if ( !x ) return false; //确认目标存在(留意_hot的设置) 0003 removeAt ( x, _hot ); _size--; //实施删除 0004 updateHeightAbove ( _hot ); //更新_hot及其历代祖先的高度 0005 return true; 0006 } //删除成功与否,由返回值指示 -
removeAt()
0001 /****************************************************************************************** 0002 * BST节点删除算法:删除位置x所指的节点(全局静态模板函数,适用于AVL、Splay、RedBlack等各种BST) 0003 * 目标x在此前经查找定位,并确认非NULL,故必删除成功;与searchIn不同,调用之前不必将hot置空 0004 * 返回值指向实际被删除节点的接替者,hot指向实际被删除节点的父亲——二者均有可能是NULL 0005 ******************************************************************************************/ 0006 template <typename T> 0007 static BinNodePosi<T> removeAt ( BinNodePosi<T> & x, BinNodePosi<T> & hot ) { 0008 BinNodePosi<T> w = x; //实际被摘除的节点,初值同x 0009 BinNodePosi<T> succ = NULL; //实际被删除节点的接替者 0010 if ( !HasLChild ( *x ) ) //若*x的左子树为空,则可 0011 succ = x = x->rc; //直接将*x替换为其右子树 0012 else if ( !HasRChild ( *x ) ) //若右子树为空,则可 0013 succ = x = x->lc; //对称地处理——注意:此时succ != NULL 0014 else { //若左右子树均存在,则选择x的直接后继作为实际被摘除节点,为此需要 0015 w = w->succ(); //(在右子树中)找到*x的直接后继*w 0016 swap ( x->data, w->data ); //交换*x和*w的数据元素 0017 BinNodePosi<T> u = w->parent; 0018 ( ( u == x ) ? u->rc : u->lc ) = succ = w->rc; //隔离节点*w 0019 } 0020 hot = w->parent; //记录实际被删除节点的父亲 0021 if ( succ ) succ->parent = hot; //并将被删除节点的接替者与hot相联 0022 release ( w->data ); release ( w ); return succ; //释放被摘除节点,返回接替者 0023 } //release()负责释放复杂结构,与算法无直接关系,具体实现详见代码包 -
效率
删除操作所需的时间,主要消耗于对search()、succ()和updateHeightAbove()的调用。 在树中的任一高度,它们至多消耗O(1)时间。故总体的渐进时间复杂度,亦不超过全树的高度。
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情”