红黑树 (一): 基础----二叉查找树

前言

 红黑树是一种非常优秀的数据结构,通过一系列的机制其能达到自平衡的效果从而提供非常高效的查询与修改操作,而笔者在网上查询相关资料想了解红黑树的过程中发现大部分的说明都不是很好理解(可能是我比较菜),而后将目光转向书籍遂查看了《算法导论》中对于红黑树的说明感到依然比较晦涩,最终是在《算法》第三章中阅读相关内容后觉得豁然开朗,于是想将书中内容结合一些自己的理解形成文字记录下来。

 系列文章预计三篇,第一篇即本篇准备从较为基础的二叉查找树入手,可将红黑树视为其的一个升级版本理解二叉查找树的各种操作对理解红黑树很重要,第二篇则会按照《算法》中的内容讲解二叉查找树与红黑树之间过渡的一种数据结构 2-3 查找树,了解它能够极大的降低学习红黑树的难度,最后一篇即我们的主角红黑树

 本文涉及的所有代码可从 Github 获得,笔者水平有限,文中如有错误请不吝指出,。

二叉查找树概述

 二叉查找树 (Binary Search Tree) 又称二叉排序树 (Binary Sort Tree) 以下简称其为 BST,是二叉树的一个变种,从其另一个名字就可以看出其存储的数据是有序的,具体来说一颗 BST 的有序来自与以下这个特性:BST 中的每个节点都含有一个可比较 (Comparable) 的健,且每个节点的键值都大于其左子树中的任意节点而小于其右子树中的任意节点。下面这张图 (图中省略 null 节点) 很好的展示了 BST 的这种特性或者说这个特性所带来的有序性,可以看到将所有节点的键值向一条直线进行投影会得到一个升序序列,而这种投影的方式对应了一种名为中序遍历的遍历方法,因此对于 BST 而言其中序遍历的结果是一个升序序列

linked-node

二叉查找树的实现

基本实现

  OK,有了一个大概的概念我们就用代码结合文字的方式来探寻一下 BST 的具体实现,首先我们要研究一下 BST 中的最小单元节点的实现,然后作为一种能提供高效查询性能的数据结果,我们会实现其 get()put() 方法

节点定义

 BST 中的节点定义与一颗普通二叉树并没有特别大的区别,唯有一点需要注意的是节点中的 key 应是可比较的这样才能保证其有有序性,在实现中我们还在节点中加入了一个属性 N 其表示以当前节点为根节点的子树所包含的节点数量,其不是必须的但是加上它能帮助我们实现一些额外功能:

public class BST<Key extends Comparable<Key>, Value> {
    private Node root;
    
    /**
     * Node 内部类,表示 BST 中的一个节点
     * Key  : 键
     * Val  : 值
     * left : 左孩子
     * right: 右孩子
     * N    : 以该节点为根节点的子树共有多少个节点
     */
    private class Node{
        private Key key;
        private Value val;
        private Node left, right;
        private int N;

        public Node(Key key, Value val, int n) {
            this.key = key;
            this.val = val;
            N = n;
        }
    }
    public int size(){
        return size(root);
    }
	
    public int size(){
        return size(root);
    }

    /**
     * 
     * @param node 当前子树的根节点
     * @return 以 node 为根节点的子树的节点数量
     */
    private int size(Node node){
        if( node == null ){
            return 0;
        }else{
            return node.N;
        }
    }
}
get() 方法

 对于 BST 这种递归结构而言结合其特性能够非常方便的用递归方法来实现 get() 方法。具体来说,我们比较当前递归节点的 key 与传入的 key 的大小,如果传入的 key 小于当前节点那么我们递归其左子树,相反的如果大于当前节点那么我们递归其右子树,如果等于当前节点那么说明我们找到了目标直接返回 value 值,如果递归到底部依然没有找到目标那么说明当前 BST 中不存在传入的 key 值我们返回 null,OK,读完这段文字其实 get() 放法就已经完成了下面来看一下代码实现:

public Value get(Key key){
    return get(root, key);
}


private Value get(Node node, Key key){
    if( node == null ){
        return null;
    }
    int cmp = key.compareTo(node.key);
    // 根据 key 的比较结果分别递归左右子树或返回当前 value
    return cmp < 0 ? get(node.left, key) :(cmp == 0 ? node.val : get(node.right, key));
}

 递归的实现非常的简洁,但一般来说会使用非递归的方式来实现 get() 因为在使用过程中查找是最经常被使用的功能,而递归会开辟栈空间从而在某种意义上造成浪费,因此接下来给出 get() 方法的非递归版本:

public Value get(Key key){
    // 非递归实现 get
    Node node = root;
    while (node != null){
        // 和递归版本类似依然是根据 key 值的比较结果来选择下一步的行动
        int cmp = key.compareTo(node.key);
        if( cmp == 0 ){
            return node.val;
        }else if( cmp  < 0 ){
            node = node.left;
        }else if( cmp > 0 ){
            node = node.right;
        }
    }
    return null;
}

 从 get() 的代码结合概述中的那张图可以发现 BST 的查找实际上就是一个从根节点开始的二分查找过程,因此对于一棵有 N 个节点且随机构建的 BST 而言 get() 的时间复杂度为 O(lgN),可见 BST 能够提供非常优秀的查找性能。

put() 方法

 了解了 get() 方法那么 put() 方法基本上也就明白了,他们的差别主要在于当 get() 方法找到目标时会返回 value,而 put() 方法则会改变 value,相应的当递归到底部发现不存在 key 时 put() 会创建一个新的节点,同样可以很方便的给出一个递归实现的版本:

public void put(Key key, Value value){
    root = put(root, key, value);
}

private Node put(Node node, Key key, Value value){
    if( node == null ){
        // 如果查找不到则创建一个新节点
        return new Node(key, value ,1);
    }
    // 如果能查找到则更新节点的 value 值
    int cmp = key.compareTo(node.key);
    if( cmp < 0 ){
        node.left =  put(node.left, key, value);
    }else if( cmp > 0 ){
        node.right =  put(node.right, key, value);
    }else{
        node.val = value;
    }
    // 依次更新节点的 N 值
    node.N = size(node.left) + size(node.right) + 1;
    return node;
}

 put() 方法同样可以用非递归的方法实现,但是 BST 作为一个主要用于查找的数据结构 put() 其实并不会被频繁的使用因此我们可以直接用方便实现的递归版本。

查找极值以及删除

 OK,上一节中我们完成了 BST 的节点定义并完成了基本的 get() 与 put() 操作,这一节中我们会进行一些拓展,主要在于 BST 的 delete() 方法其是 BST 中较为难以理解的部分但是作为后面红黑树的 delete() 方法的铺垫理解它是十分必要的,我们一步一步来先从查找极值并想办法删除极值开始。

min() 方法

 在 BST 中查找 min 值非常简单,就像在有序数组中查找 min 一样,我们只需要一路递归到数的最左端即可,同样的如果要查找 max 那么就递归到最右端,因此只展示 min() 的实现 max() 只要把 left 换成 right 即可:

public Key min(){
    return min(root).key;
}

private Node min(Node node){
    if( node.left == null ){
        return node;
    }
    return min(node.left);
}
deleteMin() 方法

 第一反应而言 deleteMin() 方法非常简单,因为我们已经能找到 min() 那么直接将其删除不就好了,事实上对于下图的这种情况的确是这样,可以看到图中的 min 也就是我们的 1 是孤零零的一个节点,在这样的情况下我们之间将 3 这个节点的 left 置为 null 就直接完成了 min 的删除,后面它会因为没有被引用而被 CG 自动回收。
linked-node

  但是对于下面这种情况我们就不能使用上述的简单方法了,可以看到此时我们想要删除的 1 节点有一个右子节点 2(不会存在左子节点),如果我们之间删除 1 那么 2 这个节点就从我们的 BST 中丢了,为此我们需要先建立图中红色的这条连接,也就是将父节点的 left 指向待删除节点的 right,而这样过后 3 节点到 1 节点的连接就断开了, 1 节点后面会因为不可达而被 CG 回收。幸运的是,虽然我们在 deleteMIn() 的时候需要考虑俩种情况,但实际上它们是同一的,我们每次都将父节点的 left 指向待删除节点的 right 即可,具体见下面的代码:

linked-node
public void deleteMin(){
    root = deleteMin(root);
}

private Node deleteMin(Node node){
    // 找到最小节点后将父节点的 left 指向自己的 right
    if( node.left == null ){
        return node.right;
    }
    node.left = deleteMin(node.left);
    node.N = 1 + size(node.left) + size(node.right);
    return node;
}

 同样对于 deleteMax() 而言,只需要将上述代码中的 left 和 right 互换即可。

delete()

 OK,我们已经讨论了 deleteMin() 方法,事实上其思想可以用于任何节点的删除但前提条件是这些节点的子节点数量为 0 或 1 个,那么面对子节点数量有 2 个的节点我们改如何删除呢?

linked-node

 从上图可以看到,假设我们现在要删除 3 这个节点,但是其有左右两个子树,因此如果将其删除,无论我们将左子树连接到 9 还是将右子树连接到 9 上面都会导致让一颗子树失去连接的问题,因此我们不能删除 3 这个节点或者说不能删除处于这个位置上的 Node,因为这样会破坏 BST 的结构,因此我们需要找一个替代节点来代替 3 节点被删除,而在被删除前其将它的所有信息覆盖到 3 这个节点上面从而达到删除 3 节点的效果,具体的可以结合下图一起理解,我们首要的目标就是找到替代节点,而这个节点的人选就是待删除节点的右子树中的最小节点,这里我们将其称为后继节点这个选取方法可能一时难以理解但稍微思考一下便能明白,然后我们将 4 节点的相关内容覆盖到 3 节点上从而将 3 节点 “挤出” 我们的 BST 然后我们对右子树调用一下 deleteMin() 方法便可将 4 节点删除同时能完成 8 节点到 6 节点的连接,这样我们就完成了将一个拥有两个子树的节点从 BST 中删除而不丢失任何节点的过程,具体的实现可以见下面的代码:

linked-node
public void delete(Key key){
    root = delete(root, key);
}

/**
     * 对于有俩个子树的节点采用后继节点来代替当前节点的方式进行删除操作
     * @param node 当前递归节点
     * @param key 需要是删除的节点的 key 值
     */
private Node delete(Node node, Key key){
    if ( node == null ){
        return null;
    }
    int cmp = key.compareTo(node.key);
    if( cmp < 0 ){
        node.left =  delete(node.left, key);
    }else if( cmp > 0){
        node.right =  delete(node.right, key);
    }else {
        // 如果当前节点有 0 或 1 个子节点
        if( node.right == null ){
            return node.left;
        }
        if( node.left == null ){
            return node.right;
        }
        // 当前节点有俩个子节点
        // 1 将当前节点保存为 t
        // 2 将 node 只想 min(t.right)
        // 3 将 node.right 指向 deleteMin(node.right)
        // 4 将 node.left 指向 t.left
        Node t = node;
        node = min(t.right);
        node.right = deleteMin(t.right);
        node.left = t.left;
    }
    node.N = 1 + size(node.left) + size(node.right);
    return node;
}

 需要特别指出的是我们不仅能用后继节点来进行删除,前续节点一样可以完成相同的功能,并且实际中我们应该随机的使用后继或前续节点来进行删除从而让 BST 能够一定程度上保持平衡,但需要在实现 Node 时加上额外的 parent 属性来指向父节点,本文是红黑树的前导文章因此就不在继续深入使用前续节点的删除方法了。

结语

 OK,作为红黑树系列的第一篇文章,本文主要讲述的是 BST 的相关内容,但能理解 BST 是理解红黑树的关键甚至于红黑树中的大部分操作都能直接沿用 BST 的代码,因此先从基础的 BST 入手是非常有必要的,而这些方法中最需要花时间理解的就是 delete() 方法了,其使用后继节点来代替当前节点被删除,从而保存了树结构的完整性在红黑树的删除中我们会再次看见相同的操作因此其还是非常重要的。好了本文就先到这里,下一篇我们将一个过渡的数据结构 2-3 查找树,当理解它之后我们就能非常轻松的理解红黑树了。

参考

《算法》(第四版)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值