数据结构_二叉搜索树

二叉搜索树
所谓的查找,指从一组数据对象中找出符合特定条件者。其中的数据对象,统一的表示和实现为词条(entry)的形式;不同的数据项之间,依照各自的关键码(key)彼此区分。
一般而言词条应以如下形式:

 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顺序性:处处满足顺序性,任一节点均不小于/不大于其左后代/右后代。
    二叉搜索树反例:
    这里写图片描述

    BST单调性:顺序性虽然只是对局部特征的刻画,但是由此却可以导出全局特征,即BST的中序遍历序列,必然单调非降。并且,任何一颗二叉树是二叉搜索树,当且仅当其中序遍历序列单调非降。
    这里写图片描述

  • BST接口定义
    二叉搜索树属于二叉树的特例,可以基于BinTree模板类(参考)派生出BST模板类。这里新增加了三个标准的对外接口search()、insert()和remove(),对应于基本的查找、插入和删除操作。


 #include "BinTree/BinTree.h" //引入BinTree

 template <typename T> class BST : public BinTree<T> { //由BinTree派生BST模板类
 protected:
    BinNodePosi(T) _hot; //“命中”节点的父亲
    BinNodePosi(T) connect34 ( //按照“3 + 4”结构,联接3个节点及四棵子树
       BinNodePosi(T), BinNodePosi(T), BinNodePosi(T),
       BinNodePosi(T), BinNodePosi(T), BinNodePosi(T), BinNodePosi(T) );
    BinNodePosi(T) rotateAt ( BinNodePosi(T) x ); //对x及其父亲、祖父做统一旋转调整
 public: //基本接口:以virtual修饰,强制要求所有派生类(BST变种)根据各自的规则对其重写
    virtual BinNodePosi(T) & search ( const T& e ); //查找
    virtual BinNodePosi(T) insert ( const T& e ); //插入
    virtual bool remove ( const T& e ); //删除
 };
  • 查找算法及其实现
    二叉搜索树的查找算法,执行过程可以描述为:
    从树根出发,逐步的缩小查找范围,直到发现目标(成功)或缩小至空树(失败)
    例如:
    这里写图片描述
    由上可以得到:每次查找对应于从根到某一节点的路径,最大比较次数为树高+1。

    查找算法的实现以及search接口实现如下:

查找算法的实现:


 template <typename T> //在以v为根的(AVL、SPLAY、rbTree等)BST子树中查找关键码e
 static BinNodePosi(T) & searchIn ( BinNodePosi(T) & v, const T& e, BinNodePosi(T) & hot ) {
    if ( !v || ( e == v->data ) ) return v; //递归基:在节点v(或假想的通配节点)处命中
    hot = v; //一般情况:先记下当前节点,然后再
    return searchIn ( ( ( e < v->data ) ? v->lc : v->rc ), e, hot ); //深入一层,递归查找
 } //返回时,返回值指向命中节点(或假想的通配哨兵),hot指向其父亲(退化时为初始值NULL)

search接口实现:

template <typname T>BinNodePosi(T)&BST(T)::search(const T&e)//在BST中查找关键码e
{
    return searchIn (_root,e,_hot=NULL);
}

节点的插入和删除操作,都需要首先调用查找算法。查找算法之所以如此实现,是为了统一并且简化不同搜索树的各种操作接口的实现,这种技巧主要体现在返回值和hot变量。下面讲讨论一下这两个的语义。
借助下图解释:
这里写图片描述
查找时无非两种情况,查找成功(作图)与查找失败(右图)。
返回的引用值与hot:
1、查找成功时,指向一个关键码为e且真实存在的节点。
2、查找失败时,返回的数值虽然为NULL,但是如上图,它指向最后一次试图转向的空节点(可以想象为一个哨兵节点,无论成功与否,查找的返回值总是等效的指向“命中的节点”)。
3、对于hot统一指向“命中节点”的父节点。(这里的命中节点可能是真实命中的节点,也可能是假想的哨兵节点。)

效率分析:在二叉树的每一层,查找算法至多访问一个节点,并且只需要常数的时间,总体所需要时间线性正比于查找路径的长度。在最好的情况下,在树根处命中即O(1);在最后的情况下,为O(n),此时退化为一条单链。所以,如果要控制单词查找在最坏情况下的时间,要从控制二叉搜索树的高度入手(平衡二叉搜索树)。

  • 插入算法及其实现
    首先需要利用查找算法search()确定插入的位置及方向,然后才能将新节点作为叶子插入。
    实例:
    这里写图片描述
    我们这里假定没有雷同元素,此时_hot为新节点的父亲。
    插入算法实现:
//对于首个节点插入之类的边界情况,都可以处理
 template <typename T> BinNodePosi(T) BST<T>::insert ( const T& e ) { //将关键码e插入BST树中
    BinNodePosi(T) & x = search ( e ); if ( x ) return x; //确认目标不存在(留意对_hot的设置)
    x = new BinNode<T> ( e, _hot ); //创建新节点x:以e为关键码,以_hot为父
    _size++; //更新全树规模
    updateHeightAbove ( x ); //更新x及其历代祖先的高度
    return x; //新插入的节点,必为叶子
 } //无论e是否存在于原树中,返回时总有x->data == e

首先调用search()查找e。若返回的位置非空,说明已有雷同节点,插入操作失败。否则x必然是_hot节点的某一空孩子,于是创建这个孩子并存入e。此后,更新全树的规模,并且更新x以及历代祖先的高度。这里无论插入操作成功与否,都会返回一个非空位置。
效率:节点插入操作所需要的时间,主要消耗在search()及updateHeightAbove()的调用。两者在每层最多涉及一个节点,仅消耗O(1)时间,因此,其时间复杂度取决于新节点的深度,在最坏的情况下不超过全树的高度。

  • 删除算法及其实现
    为了在二叉搜索树中删除节点,首先也需要调用算法search(),判断目标节点是否的确存在于树中。若存在,则需要返回其位置,然后才能实施相应的删除操作。删除操作具体分为两种情况:
    1、单分支情况
    这种情况指的是,我们确定所要删除的节点,至多只有一个孩子,为了方便说明,看如下实例:
    这里写图片描述
    若我们要想删除的节点为(69),我们只需要将其替换为左孩子(64),即拓扑意义上的节点删除即完成。最后为了保持二叉搜索树作为数据结构的完整性和一致性,我们还需要更新全树的规模,释放被摘除的节点(69),并且自下而上的更新替代节点(64)历代祖先的高度。
    2、双分支情况
    双分支情况即当一个节点的左、右孩子均存在。如下实例:
    这里写图片描述
    在这种情况下,我们使用二叉树中下一个节点
    使用的方法,首先找到待删除节点的直接后继,然后交换两个数据项即可将其转换为单分支情况。
    删除操作的具体实现:

 /******************************************************************************************
  * BST节点删除算法:删除位置x所指的节点(全局静态模板函数,适用于AVL、Splay、RedBlack等各种BST)
  * 目标x在此前经查找定位,并确认非NULL,故必删除成功;与searchIn不同,调用之前不必将hot置空
  * 返回值指向实际被删除节点的接替者,hot指向实际被删除节点的父亲——二者均有可能是NULL
  ******************************************************************************************/
 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 ) ) //若*x的左子树为空,则可
       succ = x = x->rc; //直接将*x替换为其右子树
    else if ( !HasRChild ( *x ) ) //若右子树为空,则可
       succ = x = x->lc; //对称地处理——注意:此时succ != NULL
    else { //若左右子树均存在,则选择x的直接后继作为实际被摘除节点,为此需要
       w = w->succ(); //(在右子树中)找到*x的直接后继*w
       swap ( x->data, w->data ); //交换*x和*w的数据元素
       BinNodePosi(T) u = w->parent;
       ( ( u == x ) ? u->rc : u->lc ) = succ = w->rc; //隔离节点*w
    }
    hot = w->parent; //记录实际被删除节点的父亲
    if ( succ ) succ->parent = hot; //并将被删除节点的接替者与hot相联
    release ( w->data ); release ( w ); return succ; //释放被摘除节点,返回接替者
 } 

//remove接口实现

 template <typename T> bool BST<T>::remove ( const T& e ) { //从BST树中删除关键码e
    BinNodePosi(T) & x = search ( e ); if ( !x ) return false; //确认目标存在(留意_hot的设置)
    removeAt ( x, _hot ); _size--; //实施删除
    updateHeightAbove ( _hot ); //更新_hot及其历代祖先的高度
    return true;
 } //删除成功与否,由返回值指示

效率分析:删除操作所需要的时间。主要消耗在search()、succ()和updateHeightAbove ()的调用。树中任一高度,它们至多消耗O(1)时间。总体时间复杂度不超过全树的高度。

猜你喜欢

转载自blog.csdn.net/xc13212777631/article/details/80752323
今日推荐