二叉搜索树的基本操作及其变体

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jingangxin666/article/details/79976980

1. 什么是二叉搜索树

二叉搜索树可以使用一个链表数据结构来表示, 每个结点就是一个对象. 除了key和卫星数据(satellite data, 该对象附带的其它数据)之外, 每个结点还包含着属性left, rightp, 它们分别指向结点的左孩子, 右孩子和双亲. 如果某个孩子结点或父结点不存在, 则该属性的值为NIL. 根结点是树中唯一父结点为NIL的结点.

1.1 二叉搜索树结构图

二叉搜索树结构图

1.2 二叉搜索树性质

  • x是一个二叉搜索树中的一个结点. 如果yx左子树中的一个结点, 那么y.keyx.key.如果yx右子树中的一个结点, 那么y.keyx.key.

根据性质, 可以得到:

  • 可以通过递归算法来按序输出二叉搜索树中的所有关键字(称为中序遍历)
  • 如何中序遍历二叉树?
  • 遍历一棵有n个结点的二叉搜索树需要耗费O(n)的时间. (每个结点需要遍历两次, 左孩子和右孩子)

2. 查询二叉搜索树

2.1 查找

如在一棵二叉搜索树中查找一个具有给定关键字的结点(结点可以不存在). 输入一个根结点一个关键字k, 如果这个结点存在, 就返回一个关键字为k的结点, 否则返回NIL

伪代码实现为:

//递归实现为:
TREE-SEARCH(x, k)
    if x == NIL 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 ≠ NIL and k ≠ x.key
        if k < x.key
            x = x.left
        else x = x.right
    return x

为什么迭代方法的效率比递归高得多?

递归效率低是函数调用的开销导致的。在一个函数调用之前需要做许多工作,比如准备函数内局部变量使用的空间、搞定函数的参数等等,这些事情每次调用函数都需要做,因此会产生额外开销导致递归效率偏低,所以逻辑上开销一致时递归的额外开销会多一些当然了.

通过有意识的组织代码的写法可以把某些递归写成尾递归尾递归可以进行特殊的优化所以效率会比普通的递归高一些,也不会因为递归太多导致栈溢出.

遍历树还不用递归的话,那么人肉写一个栈+深度优先遍历或者人肉队列+广度优先遍历,再辅以黑魔法给栈或者队列提速,应该会比递归快一些,加速幅度和语言和写法相关,但在大多数情况下我觉得是得不偿失的,花了很大精力很可能效率提升不明显.

—-来自知乎为什么说递归效率低?Yul8ulY的回答

尾调用(Tail Call)的优化:

2.2 最大关键字和最小关键字

通过树根沿着left孩子直到遇到一个NIL, 则当前结点为最小关键字结点

伪代码实现为:

TREE-MINIMUM(x)
    while x.left ≠ NIL
        x = x.left
    return x

同理, 可以得出最大关键字结点的求法为:

TREE-MAXIMUM(x)
    while x.right ≠ NIL
        x = x.right
    return x

2.3 后继和前驱

  • 一个结点的后继是二叉搜索树中序遍历结果中该结点的后一个结点(也就是key值比当前key值大的第一个结点)
  • 如果结点x的右孩子非空并有一个后继y, 那么y就是x有左节点的最底层祖先, 并且它也是x的一个祖先.

如下图, 关键字为13的结点的后继是关键词为15的结点.

这里写图片描述

伪代码实现为:

//寻找后继结点
TREE-SUCCESSOR(x)
    if x.right ≠ NIL
        return TREE-MINIMUM(x.right)
    y = x.p
    while y ≠ NIL and x == y.right
        x = y
        y = y.p
    return y

//寻找前驱结点
TREE-PREDECESSOR(x)
    if x.left ≠ NIL
        return TREE-MAXIMUM(x.left)
    y = x.p
    while y ≠ NIL and x == y.left
        x = y
        y = y.p
    return y

总结

  • 在一棵高度为h的二叉搜索树上, 动态集合上的操作SEARCH, MINIMUM, MAXIMUM, SUCCESSOR, PREDECESSOR均能在O(h)时间内执行完.

3. 插入和删除

3.1 插入

将一个新值v插入到一棵二叉搜索树T中, 需要调用过程TREE-INSERT. 该过程以结点z作为插入, 其中z.key = v, z.left = NIL, z.right = NIL. 这个过程要修改Tz的某些属性, 来把z插入到树中的相应位置上.

将关键字为13的数据项插入二叉搜索树的过程图解为:

这里写图片描述

伪代码实现为:

TREE-INSERT(T, z)
    y = NIL
    x = T.root
    while x ≠ NIL
        y = x
        if z.key < x.key
            x = x.left
        else x = x.right
    z.p = y
    if y == NIL
        T.root = z  // tree T was empty
    elseif z.key < y.key
        y.left = z
    else y.right = z

3.2 删除

删除结点z的整个策略分为3个基本情况:

  • 如果z没有孩子结点, 则简单删除它, 并修改父结点, 用NIL代替z
  • 如果z只有一个孩子结点, 将z的孩子提升到z的位置, 并修改z的父结点, 用z的孩子代替z. (如下图(a), (b)过程)
  • 如果z有两个孩子结点, 需要寻找z的后继y:
    • 如果z的右孩子没有左孩子, 则z的右孩子为后继y, 用y代替z, 保持y的右子树不变. (如下图中(c)过程)
    • 如果z的右孩子(r)有左孩子, 则寻找r子树中最小的结点, 即为y, 用y的右子树(如果它不是叶子结点)代替y的父结点的左子树, 用y代替z, 将r的父结点设置为y, 将z的父结点的右子树设置为y. (如下图中(d)过程)

这里写图片描述

为了在二叉搜索树中移动子树, 定义一个子过程TRANSPLANT, 它是用另一棵子树代替一棵子树.

TRANSPLANT用一棵以v为子树的根来代替一棵以u为根的子树时, 伪代码实现如下:

TRANSPLANT(T, u, v)
    if u.p == NIL
        T.root = v
    elseif u == u.p.left
        u.p.left = v
    else 
        u.p.right = v
    if v ≠ NIL
        v.p = u.p

注意:

  • 以上TRANSPLANT并没有处理v.leftv.right的更新, 这些更新都有TRANSPLANT的调用者来负责

二叉搜索树T中删除结点z的删除过程为:

TREE-DELETE(T, z)
    if z.left == NIL
        TRANSPLANT(T, z, z.right)
    elseif z.right == NIL
        TRANSPLANT(T, z, z.left)
    else 
        y = TREE-MINIMUM(z.right)
        if y.p ≠ z
            TRANSPLANT(T, y, y.right)
            y.right = z.right
            y.right.p = y
        else 
            TRANSPLANT(T, z, y)
        y.left = z.left
        y.left.p = y

4. 随机构造二叉搜索树

5. 二叉搜索树的变体

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页