二叉搜索树及其操作详解


参考《算法导论(第三版)》第 12 章。

搜索树数据结构支持许多动态及和操作,包括 SEARCH、MINIMUM、MAXIMUM、PREDECESSOR、SUCCESSOR、INSERTDELETE 等。因此,我们使用一棵搜索树既可以作为一个字典,又可以作为一个优先队列。

二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势,所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。

二叉搜索树的定义

二叉搜索树(Binary Search Tree),也叫二叉查找树和二叉排序树(Binary Sort Tree)。

定义一

二叉搜索树或者是空树,或者是满足下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的关键字的值都小于根结点关键字的值;
  • 若它的右子树不空,则右子树上所有结点的关键字的值都大于根结点关键字的值;
  • 它的左、右子树本身又是一个二叉搜索树;
  • 没有键值相等的节点。

定义二

二叉搜索树或者是空树,或者是满足下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的关键字的值都小于根结点关键字的值;
  • 若它的右子树不空,则右子树上所有结点的关键字的值都大于或等于根结点关键字的值;
  • 它的左、右子树本身又是一个二叉搜索树;

定义三

二叉搜索树或者是空树,或者是满足下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的关键字的值都小于或等于根结点关键字的值;
  • 若它的右子树不空,则右子树上所有结点的关键字的值都大于根结点关键字的值;
  • 它的左、右子树本身又是一个二叉搜索树;

以上的三种定义在不同的数据结构教材中均有不同的定义方式,但是都是正确的,在开发时需要根据不同的需求进行选择。

下面使用定义一来对二叉搜索树进行介绍。

二叉搜索树的结构特点

(1)按中序遍历二叉搜索树所得的中序序列是一个递增的有序序列,因此,二叉查找树可以把无序序列变为有序序列。

如果 x x x 是一棵有 n n n 个结点子树的根,那么对其进行中序遍历得到递增的有序序列的时间复杂度为 O ( n ) O(n) O(n)。也就是说可以在 O ( n ) O(n) O(n) 的时间内按序输出树中的所有关键字。

Q: 二叉搜索树与最小堆有什么区别?能使用最小堆在 O ( n ) O(n) O(n) 时间内按序输出一棵有 n n n 个结点树的关键字吗?

二叉搜索树:根的左子树的所有节点的值都小于根,右子树的所有节点的值都大于根;最小堆:根的左右子树均大于根。

不能,最小堆所有根的左右子树根节点的值是无序的,所以不能按照树的前中后序遍历在 O ( n ) O(n) O(n) 时间内来有序地输出;而二叉查找树,按照中序遍历正好是 左 < 根 < 右,是有序的。

(2)同一个数据集合,可按关键字表示成不同的二叉搜索树,即同一数据集合的二叉搜索树不唯一,但中序序列相同。

如关键字集合 1,4,5,10,16,17,21,可以构建出多种二叉搜索树,其中两种如下所示:

在这里插入图片描述

二叉搜索树查询

我们经常需要查找一个存储在二叉搜索树中的关键字。除了 SEARCH 操作之外,二叉搜索树还能支持诸如 MINIMUM, MAXIMUM, SUCCESSOR, PREDECESSOR 查询操作。

二叉搜索树的高度为 h h h,这里介绍如何在 O ( h ) O(h) O(h) 的时间内执行完每个操作。这里按照《算法导论》中的介绍,每个树结点包含属性 l e f t , r i g h t , p left,right,p left,right,p,分别指向结点的左孩子、右孩子和双亲。

查找

在一棵二叉搜索树中查找一个具有给定关键字的节点。这个过程从树根开始查找,并沿着这棵树中的一条简单路径向下进行,对于遇到的每个结点 x x x,比较关键字 k k k x . k e y x.key x.key

  • 如果两个关键字相等,查找就终止;
  • 如果 k k k 小于 x . k e y x.key x.key,查找在 x x x 的左子树中继续;
  • 如果 k k k 大于 x . k e y x.key x.key,查找在 x x x 的右子树中继续。

从树根开始递归期间遇到的结点就形成了一条向下的简单路径,所以该算法的时间复杂度为 O ( h ) O(h) O(h),其中 h h h 是这棵树的树高。

// 递归
TREE_SEARCH(x, k)
    if x == NULL or k == x.key
        return x
    if k < x.key
        return TREE_SEARCH(x.left, k)
    else return TREE_SEARCH(x.right, k)

// 迭代
ITERATIVE_TREE_SEARCH(x, k)
    while x != NULL and k != x.key
        if k < x.key
            x = x.left
        else x = x.right
    return x

最大关键字元素和最小关键字元素

通过从树根开始沿着 l e f t left left 孩子指针直到遇到一个 N U L L NULL NULL,我们总能在一棵二叉搜索树中找到一个最小元素

// 迭代
TREE_MINIMUM(x)
    while x.left != NULL
        x = x.left
    return x

// 递归
RECURSIVE_TREE_MINIMUM(x)
    if x.left == NULL
        return x
    return RECURSIVE_TREE_MINIMUM(x.left)

二叉搜索树的性质保证了 T R E E _ M I N I M U M TREE\_MINIMUM TREE_MINIMUM 是正确的。如果结点 x x x 没有左子树,那么由于 x x x 右子树的每个关键字都至少大于或等于 x . k e y x.key x.key,则以 x x x 为根的子树中的最小关键字是 x . k e y x.key x.key;如果结点 x x x 有左子树,那么由于其右子树中没有关键字小于 x . k e y x.key x.key,且在左子树中的每个关键字不大于 x . k e y x.key x.key,则以 x x x 为根的子树中的最小关键字一定在以 x . l e f t x.left x.left 为根的子树中。

T R E E M A X I M U M TREE_MAXIMUM TREEMAXIMUM 的伪代码是对称的:

// 迭代
TREE_MAXIMUM(x)
    while x.right != NULL
        x = x.right
    return x

// 递归
RECURSIVE_TREE_MAXIMUM(x)
    if x.right == NULL
        return x
    return RECURSIVE_TREE_MINIMUM(x.right)

这两个过程在一棵高度为 h h h 的二叉搜索树上均能在 O ( h ) O(h) O(h) 的时间内执行完,因为它们所遇到的节点构成了一条从树根向下的简单路径。

后继和前驱

这里就需要用到结点中存储的其父节点的指针。

假定树中的所有关键字都不同,而且树中节点中包括一个指向其父节点的指针,其树中某节点的后继结点的查询过程:

  • 如果结点 x x x 的右子树非空,那么 x x x 的后继恰是 x x x 右子树中的最左节点,也就是以 x . r i g h t x.right x.right 为根的子树中的最小元素;
  • 如果结点 x x x 的右子树为空,那么简单地从 x x x 沿树向上搜,直到搜到当前根节点是其父节点的左子树的根为止,返回该父节点;
TREE_SUCCESSOR(x)
    if x.right != NULL
        return TREE_MINIMUM(x.right)
    y = x.p
    while y != NULL and x = y.right
        x = y
        y = y.p
    return y

在这里插入图片描述

如上图,关键字为 15 的结点的后继是关键字为 17 的结点,也就是 15 右子树的最左结点;关键字为 13 的节点的后继是关键字为 15 的结点。

在一棵高度为 h h h 的树上, T R E E − S U C C E S S O R TREE-SUCCESSOR TREESUCCESSOR的运行时间为 O ( h ) O(h) O(h),因为该过程或者遵从一条简单路径沿树向上或者遵从简单路径沿树向下。

过程 T R E E − P R E D E C E S S O R TREE-PREDECESSOR TREEPREDECESSOR T R E E − S U C C E S S O R TREE-SUCCESSOR TREESUCCESSOR 是对称点,运行时间也是 O ( h ) O(h) O(h)

TREE_PREDECESSOR(x)
    if x.left != NULL
        return TREE_MAXIMUM(x.left)
    y = x.p
    while y != NULL and x == y.left
        x = y
        y = y.p
    return y

再如上图,关键字为 15 的结点的前驱是关键字为 13 的结点,也就是 15 左子树的最右结点;关键字为 9 的结点的前驱是关键字为 7 的结点。

结论:在一棵高度为 h h h 的二叉搜索树上,动态集合上的查找、最小(大)元素、前驱和后继操作可以在 O ( h ) O(h) O(h) 时间内完成。

二叉搜索树插入和删除

插入

二叉查找树的插入操作:

  • 若二叉排序树为空树,则新插入的结点为根结点;
  • 否则,新插入的结点必为一个新的叶结点;
  • 新插入的结点一定是查找不成功时,查找路径上最后一个结点的左儿子或右儿子。
TREE_INSERT(T,z)        // 将结点 z 插入到二叉搜索树 T 中, z.left = z.right = NULL
    y = NULL
    x = T.root
    while x != NULL
        y = x
        if z.key < x.key
            x = x.left
        else x = x.right
    z.p = y
    if y == NULL        // tree T was empty
        T.root = z
    elseif z.key < y.key
        y.left = z
    elseif z.key > y.key
        y.right = z

递归实现

TREE_INSERT(T,z)        // 将结点 z 插入到以 x 为根的子树上
    x = T.root
    if x == NULL
        T.root = z
    elseif z.key < x.key
        TREE_INSERT(x.left, z)
    elseif z.key > x.key
        TREE_INSERT(x.right, z)

与其他搜索树上的原始操作一样,过程 T R E E − I N S E R T TREE-INSERT TREEINSERT 在一棵高度为 h 的树上的运行时间为 O ( h ) O(h) O(h),只需要从根开始沿树向下搜索,直到找到新节点要插入的位置(某一个 N U L L NULL NULL 的位置)。

删除

删除某结点,并保持二叉排序树特性,分三种情况处理

1)如果删除的是叶结点,则直接删除;

2)如果删除的结点只有一株左子树或右子树,则直接继承:将该子树移到被删结点位置;

在这里插入图片描述在这里插入图片描述

3)如果删除的结点有两株子树,则用继承结点(后继)代替被删结点,这相当于删除继承结点——按 1) 或 2) 处理继承结点(因为继承节点肯定没有左子树,不然它就不会是继承节点)。

二叉查找树的删除操作的实现步骤(按《算法导论》中每个节点存储了父节点指针来实现的)

  • 若结点 z z z 是叶子,则直接删除结点 z z z;
  • 若结点 z z z 只有左子树,则只需重接 z z z 的左子树;若结点 z z z 只有右子树,则只需重接 z z z 的右子树;
  • 若结点 z z z 的左右子树均不空,则
    • 查找结点 z z z 的右子树上的最左下结点 y y y y y y 肯定没有左孩子);

    • 将结点 y y y 数据域替换到被删结点 z z z 的数据域;

    • 如果 y y y z z z 的右孩子,那么用 y y y 替换 z z z 成为 z z z 的双亲的一个孩子,然后用 z z z 的左孩子替换 y y y 的左孩子;
      在这里插入图片描述

      如果 y y y 不是 z z z 的左孩子,那么用 y y y 的右孩子替换 y y y 并成为 y y y 的双亲的一个孩子,然后再将 z z z 的右孩子转变为 y y y 的右孩子,然后执行和上面一样的操作,用 y y y 替换 z z z 成为 z z z 的双亲的一个孩子,然后用 z z z 的左孩子替换 y y y 的左孩子;

      在这里插入图片描述

为了在二叉搜索树内移动子树,定义一个子过程 T R A N S P L A N T TRANSPLANT TRANSPLANT,它是用一棵子树替换一棵子树并成为其双亲的孩子节点。当 T R A N S P L A N T TRANSPLANT TRANSPLANT 用一棵以 v v v 为根的子树来替换一棵以 u u u 为根的子树时,结点 u u u 的双亲就变为了节点 v v v 的双亲,并且最后 v v v 成为 u u u 的双亲的相应孩子。

TRANSPLANT(T,u,v)
    if u.p == NULL
        T.root = v
    elseif u == u.p.left
        u.p.left = v
    elseif u == u.p.right
        u.p.right = v
    if v != NULL
        v.p = u.p

利用现成的 T R A N S P L A N T TRANSPLANT TRANSPLANT 过程,下面是从二叉搜索树 T T T 中删除节点 z z z 的过程:

TREE_DELETE(T,z)
    if z.left == NULL
        TRANSPLANT(T,z,z.left)
    elseif z.right == NULL
        TRANSPLANT(T,z,z.right)
    else
        y = TREE_MINIMUM(z.right)
        if y.p != z
            TRANSPLANT(T, y, y.right)
            y.right = z.right
            y.right.p = y
        TRANSPLANT(T, z, y)
        y.left = z.left
        y.left.p = y

除了调用 T R E E − M I N I M U M TREE-MINIMUM TREEMINIMUM 之外, T R E E − D E L E T E TREE-DELETE TREEDELETE 的每一行,包括调用 T R A N S P L A N T TRANSPLANT TRANSPLANT 都只花费常数时间,因此在一棵高度为 h h h 的二叉搜索树上, T R E E − D E L E T E TREE-DELETE TREEDELETE 的运行时间为 O ( h ) O(h) O(h)

结论:在一棵高度为 h h h 的二叉搜索树上,动态集合上的查找、最小(大)元素、前驱和后继、插入和删除操作,均可以在 O ( h ) O(h) O(h) 时间内完成。

我们已经证明了高度为 h h h 的二叉搜索树上的每个基本操作都可以在 O ( h ) O(h) O(h) 时间内完成,然而随着元素的插入和删除,二叉搜索树的高度是变化的。例如,如果 n n n 个关键字按照严格递增的次序被插入,则这棵树一定是高度为 n n n 的一条链,在这样的树上进行操作的时间性能是比较低的。也就是说,如果搜索树的高度较低时,这些集合操作会执行得较快;然而如果树的高度较高时,这些集合操作可能并不比在链表上执行的快。也就有了各种各样的 “平衡”搜索树

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值