查找树(一):初探二叉查找树

1. 二叉查找树(Binary search tree)

查找是编写程序过程中最常用的操作之一,为此,出现了许多专门针对查找的数据结构与算法,旨在更加快速的定位指定元素的位置。说起查找算法,大家想必都会联想到二分查找;作为一种常见的查找算法,对于一组有序的元素,二分查找首先定位序列最中心的元素,然后将中心元素与目标值做比较,以确定目标值出现在“左半部分”或“右半部分”。

根据二分查找的流程可以知道,每次比较都可以将查找范围缩小一半,因此它的时间复杂的可以达到O(log2n) 。二分查找可以说是在算法层面上,对查找操作做了一定的优化,以获得更低的时间复杂度,那么有没有某种数据结构,可以从“结构”方面对查找做一些优化呢? 答案当然是肯定的,通过树我们就可以达到此目的;当然,为达到优化时间复杂度的目的,肯定需要诸多的限制条件,以约束树的结构。

树是一种十分常见的数据结构,关于树的定义或基本概念,可以查看树(一种分支结构的数据组织方式)。这里主要讲一下二叉查找树,它是一种“类似”二分查找的二叉树(想要真正的类似,还需要一定的平衡条件):

  • 所谓的二叉查找树 T,要么是一棵空树,要么是以 r = (key, value) 为根节点的二叉树,而且其左、右子树都是二叉查找树;

  • r 的 左子树中的所有节点(如果存在的话)的 关键码均不大于key;

  • r 的 右子树中的所有节点(如果存在的话)的 关键码均不小于key。

下图展示了一个二叉查找树的简单示例:

二叉查找树示例

2. 插入操作

构造二叉查找树(或者是向树中添加元素)并不复杂,只需要根据新增节点的key与当前节点的key的大小关系,就可以确定后续将新增节点放在当前节点的左子树或者右子树,也就是说,新节点总是作为叶子节点的子节点被插入。插入操作的伪代码如下所示(为方便理解,伪代码中的key均以int为例):

public void insert(Node node) {
    if (null == r) { // 如果当前树为空,则构造一个二叉查找树并返回
        r = node;
        return;
    }
    
    Node nowNode = r;
    while (true) {
        if (node.key <= nowNode.key) { // 添加到左子树
            if (null == nowNode.left) {
                nowNode.left = node;
                break;
            } else {
                nowNode = nowNode.left;
            }
        } else { // 添加到右子树
            if (null == nowNode.right) {
                nowNode.right = node;
                break;
            } else {
                nowNode = nowNode.right;
            }
        }
    }
}

3. 查找操作

对于二叉查找树,其查找过程就变得十分简单了,只需要根据节点的key与目标key的大小,就可以找到或确定下一步的查找方向(“向左”或“向右”查找)。查找操作的伪代码如下所示:

public Node get(int key) {
    Node node = r; // 当前节点为根节点
    while (node != null) {
        if (key == node.key) {
            return node;
        } else if (key < node.key) {
            node = node.left;
        } else {
            node = node.right;
        }
    }
    return null;
}

4. 删除操作

对于删除操作:

  • 如果要删除的节点是叶子节点,则直接删除它,删除操作并不会对二叉查找树的结构造成影响;
  • 如果要删除节点d的左子树(或右子树)为空,则让d的父节点直接指向d的右(左)孩子;如果想要将被删除节点移动到叶子节点再删除的话,可以让d节点与孩子节点的位置互换,然后继续递归的删除改变了位置的d节点。
  • 如果要删除节点d的左右子树均非空,找到其右子树key最小的节点(后继节点w),用w代替d,然后递归的删除d(右子树key最小的节点必然没有左子树,删除操作得到简化)。

其伪代码如下所示:

public Node remove(int key) {
    Node d = get(key);
    if (null = d) {
        return null;
    }
    
    if (null == d.left) { // 左子树为空
        让d的父节点直接指向d的右孩子,相当于是删除了的节点;
        // 或与子节点互换后递归的删除替换后的节点
    } else { // 左子树非空
        if (null == d.right) { // 右子树为空
            让d的父节点直接指向d的左孩子,相当于是删除了的节点;
            // 或与子节点互换后递归的删除替换后的节点
        } else { // 左右子树均非空
            Node w = findMin(d.right);
            用w的key和element代替d的key和element;
            用w的右子树代替w(不存在则用null代替);
        }
    }
    
    return node;
}

private Node findMin(Node node) {
    while (node.left != null) {
        node = node.left;
    }
    
    return node;
}

5. 平衡二叉树

讲到这里,二叉查找树的基本结构已经比较清晰,他与二分查找类似,都是根据key的大小关系决定查找方向,从而排除一部分的“无关节点”。当然,现在的类似只能说是“形似”,它目前还不具备二分查找可以优化时间复杂度的特点,观察下面这棵树:

二叉查找树的特例

可能有人会怀疑,它是二叉查找树吗?很不幸的是,它是,它符合二叉查找树的定义,因此这就是一棵二叉查找树。可以看到,在一些极端的情况下,二叉查找树退化成了链表;显然,它的查找操作的时间复杂度为O(n),并不具备二分查找O(log2n)的时间复杂度的优点。

仔细观察二分查找与二叉查找树,可以发现相较于二分查找,在二叉查找树中缺少了很重要的一步,“寻找中点”。在构造二叉查找树的时候,我们并不能保证它的所有节点的左右子树都是“平衡”的,因此这里的比较操作并不是与“中点”做比较,这也是引起二叉查找树查找操作的时间复杂度没有得到优化的原因。

为解决此问题,显然我们需要对二叉查找树再增加一些限制条件,以让其达到”平衡“。比较常见的带有平衡条件的二叉查找树有AVL树、红黑树等。

  • AVL树所有节点的左子树和右子树的高度差不超过1;
  • 红黑树通过标红和标黑节点,并增加一系列的限制条件,以达到平衡。

当然,这里所说的平衡并没有二分查找的中点那么严格,它只是一种相对的平衡,真正的像中点那样的平衡很难(也没有必要,代价太大)达到,可以将其看成类似中点的一种平衡。通过为二叉查找树增加平衡条件,就可以将查找操作的时间复杂度优化到O(log2n)。

6. 多路查找树

至此,我们已经可以用二叉树比较好的模拟出二分查找的操作。不妨再进一步的思考,对于这里只是对二叉树进行了合理的约束,我们知道,二叉树只是树的一个特例(虽然它是最常用的,但不代表所有),如果将上述操作中的二叉树替换为M叉树会是什么样的情景呢?

  • 时间复杂度可以优化到O(logM(n))。

在一些情况下(如数据库,数据被固化在磁盘上),单次的查找操作代价比较大,对树的高度有很高的要求,这时候平衡二叉树就不能满足需求了,需要用M叉树代替,典型的例子就是B-树B+树

当然,性能的提升带来的往往是实现复杂度的增加,AVL树、红黑树、B-树和B+树的实现都比较复杂,此处不做深究,将会在后续的文章中进行探讨。

后续

关于AVL树,可以参考:查找树(二):AVL树.
关于红黑树,可以参考:查找树(三):红黑树(red black tree).
关于B-树和B+树,可以参考:查找树(四):B-树和B+树.

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值