数据结构 | 第7章 - 二叉搜索树

76 阅读6分钟

第7章 搜索树

总之,若既要求对象集合的组成可以高效率地动态调整,同时也要求能够高效率地查找,则以上线性结构(如向量、列表、栈和队列),均难以胜任。

那么,高效率的动态修改和高效率的静态查找,究竟能否同时兼顾?如有可能,又应该采用什么样的数据结构?接下来的两章,将逐步回答这两个层次的问题。

image-20220715213441828.png

本章将首先介绍树式查找的总体构思、基本算法以及数据结构,通过对二分查找策略的抽象与推广,定义并实现二叉搜索树结构。尽管就最坏情况下的渐进时间复杂度而言,这一方法与此前相比并无实质的改进,但这部分内容依然十分重要——基于半线性的树形结构的这一总体构思,正是后续内容的立足点和出发点。

7.1 查找

所谓的查找或搜索,指从一组数据对象中找出符合特定条件者,这是构建算法的一种基本而重要的操作。其中的数据对象,统一地表示和实现为词条(entry)的形式;不同的词条之间,依照各自的关键码(key)彼此区分。

循关键码访问:查找的过程与结果,仅仅取决于目标对象的关键码。其与此前的“循秩访问”和“循位置访问”等完全不同,这一新的访问方式,与数据对象的物理位置和逻辑次序均无关。

词条对象拥有成员变量key和value。

7.2 二叉搜索树

二叉搜索树(Binary Search Tree, BST)

顺序性

任一节点r的左(右)子树中,所有节点(若存在)均不大于(不小于)r

回避边界情况,暂定所有节点互不相等:所有任一节点r的左(右)子树中,所有节点(若存在)均小于(大于)r

中序遍历序列:

image-20220716105847879.png

任何一棵二叉树是二叉搜索树,当且仅当其中序遍历序列单调非降

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 };

查找算法及其实现

  • 算法:二叉搜索树的查找算法,亦采用了减而治之的思想与策略,其执行过程可描述为:

    从树根出发,逐步地缩小查找范围,直到发现目标——成功 / 缩小至空树——失败

    image-20220718103005353.png

  • searchIn()算法与search()接口:searchIn()算法使用递归实现,search()接口通过调用searchIn()算法实现

    image-20220718110604537.png

  • 语义约定:

image-20220718141424866.png

在调用searchIn()算法之前,search()接口首先将内部变量_hot初始化为NULL,然后作为引用型参数hot传递给searchIn()。在整个查找的过程中,__hot变量始终指向当前节点的父亲。

  • 效率:在最好情况下,目标关键码恰好出现在树根处(或其附近),此时只需O(1)时间。然而不幸的是,对于规模为n的二叉搜索树,深度在最坏情况下可达Ω(n)。

插入算法及其实现

  • 算法

    image-20220718143701801.png

  • 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(),判断目标节点是否的确存在于树中。若存在,则需返回其位置,然后方能相应地具体实施删除操作。

image-20220718154537803.png

  • 单分支情况

    (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 天,点击查看活动详情