算法导论12章-二叉搜索树(Binary Search Trees)

本文介绍了二叉搜索树的基本概念,包括其性质、中序遍历、查找、插入和删除操作。通过示例展示了如何在Java中实现二叉搜索树的中序遍历和查找算法,并探讨了不同高度对性能的影响。此外,还讨论了后继和前驱结点的查找方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这是我的第一篇 算法导论(Introduction to Algorithms, Third Edition) 读书笔记. 旨在记录重点, 强化自己对算法的学习效果.
这里不会像书本那样详细, 给出复杂的推理证明, 这里只是一点学习心得.

我看到是 算法导论(中文版) 的书, 这里有原著(提取码:cz4v)(英文版PDF) 可以对照阅读.

1 什么是二叉搜索树

一棵 二叉搜索树(BST) 是以一棵 二叉树 来组织的.
二叉搜索树 可以使用一个 链表数据结构(linked data structure ) 表示, 其中每个 结点(node) 就是一个对象. 除了关键字(key) 和 卫星数据(satellite data) 之外, 每个结点还包含属性 left(指向左孩子), right(指向右孩子) 和 p(指向双亲).
如果某个孩子结点和父结点不存在, 则响应属性的值为 NIL, 根结点 是树中唯一 父指针(p) 为 NIL 的结点.

备注:
我们存储在结点里的数据, 可以称为记录(record), 每个记录包含一/多个关键字(key), key是结点之间比较大小时用的值; 记录的剩余部分称为 卫星数据(satellite data), 卫星数据和关键字是一同存取的.

下面是两棵 二叉搜索树:
tid_12_01
(a) 图: 一个具有 6 个结点, 高度为 2 的 二叉搜索树.
(b) 图: 一个有和(a)图相同的 6 个结点, 高度为 4 的 二叉搜索树.

因为 二叉搜索树 的基本操作所花费的时间和 树的高度 成正比. 所以, (b) 是一个比 (a) 低效的 二叉搜索树.

1.1 二叉搜索树的性质

二叉搜索树的性质(binary-search-tree property):
x 是 二叉搜索树 中的一个结点.
如果 yx左子树 的一个结点, 那么 y.key <= x.key.
如果 yx右子树 中的一个结点, 那么 y.key >= x.key.

根据 二叉搜索树 的性质, 给出其数据结构:

public class Node {
    private int key;
    private Node left;
    private Node right;
    private Node p;

    public Node(int key, Node p) {
        this.key = key;
        this.p = p;
    }
    // 省略 getter 和 setter
}

1.2 中序遍历

我们可以通过 一个简单的递归算法(a simple recursive algorithm) 输出 二叉搜索树 中的所有 key.
输出的 根的key 位于其 左子树的key 和 右子树的key 之间, 这种算法称为 中序遍历(inorder tree walk)算法.
类似的, 先序遍历(preorder tree walk)输出的 根的key 在其 左右子树的key 之前;
后序遍历(postorder tree walk)输出的 根的key 在其 左右子树的key 之后.

中序遍历算法INORDER-TREE-WALK(x):
输入BST的根结点, 输出中序BST的所有结点.

INORDER-TREE-WALK(x)
if x != NIL
    INORDER-TREE-WALK(x.left)
    print x.key
    INORDER-TREE-WALK(x.right)

遍历一棵有 n 个结点的 BST 的时间复杂度是 O(n).

上图(a) 中的BST 中序遍历 实现:

public class Bst {
    public List<Integer> inorderTreeWalk(Node root) {
        List<Integer> result = new ArrayList<>();
        doInorderTreeWalk(root, result);
        return result;
    }

    public void doInorderTreeWalk(Node root, List<Integer> result) {
        if (root != null) {
            doInorderTreeWalk(root.getLeft(), result);
            // System.out.println(root.getKey());
            result.add(root.getKey());
            doInorderTreeWalk(root.getRight(), result);
        }
    }

    public static void main(String[] args) {
        Node root = new Node(6, null);
        Node n51 = new Node(5, root);
        root.setLeft(n51);
        Node n52 = new Node(5, n51);
        n51.setRight(n52);
        Node n2 = new Node(2, n51);
        n51.setLeft(n2);
        Node n7 = new Node(7, root);
        root.setRight(n7);
        Node n8 = new Node(8, n7);
        n7.setRight(n8);

        Bst bst = new Bst();
        List<Integer> r = bst.inorderTreeWalk(root);
        r.forEach(i -> System.out.print(i + " "));
    }
}

输出:
2 5 5 6 7 8

2 查询二叉搜索树

我们经常需要 查找 一个存储在 BST 中的 key.
除了 search 以外, BST 还支持 minimum, maximum, successorpredecessor 的查询操作.
在任何高度为 h 的 BST 上, 这些查询操作的时间复杂度都是 O(h).

2.1 查找(searching)

二叉搜索树查找(searching) 算法 TREE-SEARCH(x, k):
输入一个 指向根结点的指针x 和一个 关键字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)

BST 查找算法 TREE-SEARCH(x, k) 的运行时间是 O(h), 其中 h 是 BST 的高度.

我们可以采用 while 循环展开 递归, 用 迭代方式 重写 查找算法.

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

2.2 最小和最大关键字(minimum and maximum)

BST 的最小 key 结点(minimum):
从树根沿着 左孩子指针往下走, 直到遇到 NIL, 走到的最后一个结点就是 最小关键字结点.

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

BST 的最大 key 元素(maximum):
从树根沿着 右孩子指针往下走, 直到遇到 NIL, 走到的最后一个结点就是 最大关键字结点.

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

查找 BST 的最小/大结点 都需要花费 O(h) 时间, h 为 BST 的高度.

2.3 后继和前驱(successor and predecessor)

后继结点(successor):
给定一棵 BST 的一个结点, 按照中序遍历的次序 查找它的后继.

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

查找 x结点的 后继结点, 分两种情况:

  1. x 的右子树非空, 那么 x 的后继就是 右子树中的最小结点.
  2. x 的右子树为空, 那么 x 的后继只可能在其 祖先 之中. 假设 x 的后继是 y, 那么 y 一定是 x 在向上查找路径中出现右拐的第一个节点.

关于情况2可以参考下图理解:
ita_12_successor

前驱结点(predecessor):
给定一棵 BST 的一个结点, 按照中序遍历的次序 查找它的前驱.

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

TREE-PREDECESSOR(x)TREE-SUCCESSOR(x) 是对称的, 它们的时间复杂度都是 O(h).

3 插入和删除

插入和删除操作会引起 BST 的集合结构的变化, 需要修改数据以保持 BST 的性质成立.

3.1 插入

要将一个新值 v 插入一棵 二叉搜索树T 中, 假设 输入时 z 结点(z.key = v, z.left=NIL, z.right=NIL), 把 z 插入 T 的相应位置上:

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

插入算法的过程很简单:
保持 跟踪指针(trailing pointer)(中文版翻译成遍历指针) y 作为 x 的双亲.
x 由根结点向下走(z.key < x.key时 x 往左走, 否则 x 往右走), 最终 x 变为 NIL 时停止, 最终 y 指向叶子结点. 让 z 成为 叶子结点的一个孩子即可.

示例, 把 13 插入 以下BST:
ita_12_insert
TREE-INSERT 在一棵高度为 h 的 BST 上的运行时间为 O(h).

3.2 删除

从 一棵 BST T 中删除一个结点 z 需要考虑以下四种情况:

  1. 如果 z 没有左孩子, 那么用其 右孩子 替换 z. 这个右孩子是不是 NIL 都行.
  2. 如果 z 没有右孩子(只有一个孩子, 且是左孩子), 那么用 左孩子 替换 z.
    除了上述两种情况外, z 有左和右两个孩子. 我们需要查找 z 的后继 y. yz的后继, 所以y是没有左孩子的.
  3. 如果 yz 的右孩子, 那么用 y 替换 z.
  4. 否则, y 位于 z 的右子树中, 但并不是 z 的右孩子. 这种情况下, 先用 y的右孩子 替换 y, 然后再用 y 替换 z.

4 种 删除 z 的情况如下图所示:
ita_12_delete
对于 3 和 4 这两种情况, 需要注意:
删除z, 只能用 z 的后继结点放在 z 的位置. 因为 BST 要求 当前结点的右子树的任一结点都小于 当前结点.

为了实现上图中的替换操作, 先定义一个 移植(transplant)过程.
TRANSPLANT 用一棵以 v 为根的子树 替换 一棵以 u 为根的子树时, u 的双亲就变为 v 的双亲, 并且 u 会成为 v 的双亲的响应的孩子.

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

从 BST 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)  // y是z的后继
    if y.p != z
        TRANSPLANT(T, y, y.right)
        // 把y的右子树换成z的右子树
        y.right = z.right
        y.right.p = y
    TRANSPLANT(T, z, y)
    // 把y的左子树换成z的左子树
    y.left = z.left
    y.left.p = y

除了 TREE-MINIMUM 之外, TREE-DELETE 每一行都是花费常数时间, 因此, 在一棵高度为 h 的BST上, TREE-DELETE 运行时间为 O(h).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值