数据结构——二叉搜索树

顺序性

在所谓二叉树搜索树(binary search tree)中,处处都满足顺序性:

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

数据结构——二叉搜索树

上述顺序性可简化表述为:任一节点的左右子树中,所有节点(若存在)均小于(大于) r

 

数据结构——二叉搜索树

二叉搜索树的三个实例(左),三个反例(右)

中序遍历序列

二叉搜索树的中序遍历序列必然单调非降

数据结构——二叉搜索树

 

 

BST模板类

二叉搜索树属于二叉树的特列,所以基于二叉树模板类BinTree可以派生出BST模板类

 1 template<typename T>
 2 class BST :public BinTree<T>
 3 {
 4 protected:
 5     TreeNode<T>* _hot;                                //“命中”节点的父亲
 6     void insertAsLc(TreeNode<T>* p, T const& e) = delete;
 7     void insertAsRc(TreeNode<T>* p, T const& e) = delete;
 8     void insertAsRoot(T const& e) = delete;
 9     //在以p为根节点的BST中查找关键码e
10     TreeNode<T>*& _search(TreeNode<T>*& p, const T& e, TreeNode<T>*& hot);
11     //删除x所指示的节点
12     TreeNode<T>* _remove(TreeNode<T>*& x, TreeNode<T>*& hot);
13     //“3+4”重构
14     TreeNode<T>* _connect34(TreeNode<T>* a, TreeNode<T>* b, TreeNode<T>* c,
15         TreeNode<T>* T0, TreeNode<T>* T1, TreeNode<T>* T2, TreeNode<T>* T3);
16     //AVL树重平衡
17     TreeNode<T>* _rotateAt(TreeNode<T>* v);
18 public:
19     BST() :_hot(nullptr) {}
20     //在树中查找关键码e
21     virtual TreeNode<T>*& search(const T& e)
22     {
23         return _search(BinTree<T>::_root, e, _hot);
24     }
25     //将关键码插入BST中
26     virtual TreeNode<T>* insert(const T& e);
27     //删除关键码e
28     virtual bool remove(const T& e);
29 };

 查找算法

二叉搜索树的查找算法基于减而治之的思路与策略,其执行过程可以描述为:

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

二叉搜索树的查找类似如下过程:

数据结构——二叉搜索树

一般地,在查找过程中,一旦发现当前节点为空,即说明查找范围已缩小至空,查找失败

对照中序遍历序列可见,整个过程与有序向量的二分查找过程等效,故可视作后者的推广。

查找算法的实现 

template<typename T>
TreeNode<T>*& BST<T>::_search(TreeNode<T>*& p, const T& e, TreeNode<T>*& hot)
{    //递归基:命中节点或查找失败
    if (!p || p->_val == e)return p;
    hot = p;        //一般情况,记下当前节点,深入一层,递归查找
    return _search(((e < p->_val) ? p->_left : p->_right), e, hot);
    //返回值指向命中节点(或是假想的哨兵),hot指向其父节点(退化时为初始值nullptr)
}

节点的插入和删除操作,都需要先调用查找算法,依据查找结果决定后续的处理方式,所以此处以引用方式传递(子)树根节点,以为后续操作提供必要信息。

语义约定

以上查找算法之所以如此实现,是为了统一并简化后续不同搜索树的各种操作接口的实现。其中的技巧,主要体现于返回值和 hot 变量。

若查找成功,则search()的返回值指向一个关键码为e且真实存在的节点;若查找失败,则返回值的数值虽然为nullptr,但可以假象地将此空节点转换为一个数值为e的哨兵节点,如此,无论成功与否,查找的返回值总是等效地指向“命中节点”。

在整个查找过程中,hot变量始终指向当前节点的父亲。因此在算法返回时,按照如上定义,_hot将指向“命中节点”的父亲。_hot节点是否拥有另一个孩子,与查找成功与否无关,查找成功时,节点e可能是叶子,也可能是内部节点;查找失败时,假想的哨兵节点e等效于叶节点,但可能有兄弟。

在删除操作中,也要首先进行查找,按照以上语义,命中节点必然是待摘除节点:该节点与其父亲节点 _hot 联合指示了删除操作的位置。

在插入操作中,“命中节点”就是待插入的新节点,而 _hot 所指向的,就是该节点可行的插入位置

效率 

在二叉搜索树的每一层,查找算法至多访问一个节点,且只需常数时间,故总体所需时间应线性正比于查找路径的长度,或最终返回节点的深度。在最好情况下,目标关键码恰好出现在树根处(或其附近),此时只需O(1)时间。然而对于规模为 n 的二叉搜索树,深度在最坏情况下可达Ω(n)。比如当该树退化为(接近于)一条单链时,发生此类情况的概率将很高。

由此我们可以得到启示:若要控制单次查找在最坏情况下的运行时间,须从控制二叉搜索树的高度入手。

插入算法

为了在二叉搜索树中插入一个新节点,需要首先利用查找算法确定插入的位置及方向,然后将新节点作为叶子插入。

数据结构——二叉搜索树

在插入一个节点之后,还需从节点 _hot 出发,自下而上地逐个更新其历代祖先的高度。

插入算法的实现

 1 template<typename T>
 2 TreeNode<T>* BST<T>::insert(const T& e)
 3 {
 4     TreeNode<T>*& p = search(e);//定位插入位置
 5     if (!p)                        //确定目标不存在
 6     {
 7         //以_hot为父亲节点创建新节点
 8         p = new TreeNode<T>(e, _hot);
 9         ++BinTree<T>::_size; //更新树的规模
10         this->_updateHeightAbove(p);
11         //最后更新其历代祖先的高度
12     }
13     return p;
14     //无论e是否存在于树中,返回时总有p->_val == e
15 }

删除算法

为从二叉树中删除节点,首先也需要查找以确定目标接待节点的确存在于树中。

数据结构——二叉搜索树

单分支情况 

以上图为例,为了删除节点 69 ,只需将该节点替换为其左子节点 64 , 拓扑意义上的节点删除即告完成。当然,还需自下而上地更新节点 64 历代祖先的高度。首个需要更新高度的节点 58 ,恰好由 _hot 指示。对于没有左孩子的目标节点,也可以对称处理。

双分支情况

还是以上图为例,若要删除节点 36 ,首先调用 succ() 算法, 找到该节点的直接后继 40 ,然后交换二者的数据项,则可将后继节点等效为待删除的目标节点。不难验证,该后继节点必然无左孩子,从而相当于将双分支情况转化为单分支的情况。同样,此时也要更新一系列祖先节点的高度,此时首个需要更新高度的节点也恰好由_hot 指示。

删除算法的实现

 1 template<typename T>
 2 bool BST<T>::remove(const T& e)
 3 {
 4     TreeNode<T>*& x = search(e);
 5     //确认目标存在
 6     if (!x)return false;
 7     //实施删除
 8     _remove(x, _hot); --BinTree<T>::_size;
 9     //删除成功与否由返回值指示
10     return true;
11 }
12 template<typename T>
13 TreeNode<T>* BST<T>::_remove(TreeNode<T>*& x, TreeNode<T>*& hot)
14 {
15     TreeNode<T>* w = x;
16     //实际被删除的节点
17     TreeNode<T>* succ = nullptr;
18     //实际被删除节点的接替者
19     if (!x->_left)succ = x = x->_right;
20     //若左子树不存在,则直接将其替换为右子树
21     else if (!x->_right)succ = x = x->_left;
22     //对称处理
23     else
24     {    //否则
25         w = w->succ();
26         //找到待删除节点在中序遍历下的后继节点
27         swap(x->_val, w->_val);//交换其数据项
28         TreeNode<T>* u = w->_parent;
29         ((u == x) ? u->_right : u->_left) = succ = w->_right;
30     }
31     hot = w->_parent;    //实际被删除节点的父亲
32     if (succ)succ->_parent = hot;    //被删除节点的接替者与hot相联
33     delete w;
34     return succ;            //返回接替者
35 }

效率

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

上一篇:8、Cesium学习之配置视窗


下一篇:P1020 导弹拦截(LIS)