Java 数据结构 - 二叉查找树:有了高效的哈希表,为什么还需要二叉树

Java 数据结构 - 二叉查找树:有了高效的哈希表,为什么还需要二叉树

数据结构与算法之美目录(https://www.cnblogs.com/binarylei/p/10115867.html)

在学习二叉查找树之前,我们先看一下,目前已经接触的几种高效的数据结构的时间复杂度:

  • 有序数组:查找的时间复杂度为 O(logn),但如果数据发生变化,查找前就需要重新排序,时间复杂度就升为 O(nlogn)。所以动态数据不适合使用二分法查找,也就是不支持动态数据。
  • 跳表:通过缓存索引来实现链表的二分法查找,其查找的时间复杂度 O(logn)。另外,在插入和删除时也需要先查找到该结点,因此时间复杂度也是 O(logn)。支持动态数据。
  • 哈希表:通过散列函数和数组的随机访问,实现插入、删除、查找的时间复杂度都是 O(1),是一种非常高效的数据结构。支持动态数据。当然散列函数、散列冲突、加载因子的选择直接决定了哈希表的性能。但哈希表的最大缺点是不支持顺序访问,这也是工程中经常将哈希表和链表一起使用的原因。

1. 基本操作

二叉查找树(Binary Search Tree)也叫排序树或有序树或搜索树,它是为实现快速查找而生。二叉查找树的左子树的节点都小于它的父节点,右子树中的节点都大于它的父节点,因此若按中序遍历则从小到大的排序。

二叉排序树或者是一棵空树,或者是具有下列性质的二叉树,它的严格定义如下:

  1. 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 左、右子树也分别为二叉排序树;
  4. 没有键值相等的节点。

二叉排序树在搜索中的应用非常广泛,同时二叉排序树的一个变种(红黑树)是 java 中 TreeMap 和 TreeSet 的实现基础。我们看一下二叉查找树的查找、插入、删除这些基本的操作,重点关注其时间复杂度,也就明白了为什么要使用平衡二叉查找树。

1.1 查找

二叉查找树的查找的代码比较简单。

public Object search(Node tree, int value) {
    Node p = tree;
    while (p != null) {
        if (value < p.data) {
            p = p.left;
        } else if (value > p.data) {
            p = p.right;
        } else {
            return p.data;
        }
    }
    return null;
}

说明: 我们来分析一下二叉查找树的查找的时间复杂度,如果二叉树比较平衡,时间复杂度就是 O(logn)。但如果二叉树退化成了链表,那么时间复杂度就降为 O(n)。

1.2 插入

二叉查找树的插入也很简单,因为只能插入到叶子结点上。

private Node tree;
public void add(int value) {
    if (tree == null) {
        tree = new Node(value);
        return;
    }
    Node p = tree;
    while (p != null) {
        if (value < p.data) {
            if (p.left == null) {
                p.left = new Node(value);
                break;
            }
            p = p.left;
        } else {
            if (p.right == null) {
                p.right = new Node(value);
                break;
            }
            p = p.right;
        }
    }
}

说明: 在插入前也需要查找结点所在的位置,因此平衡二叉树的最好时间复杂度为 O(logn),但如果退化为链表则最坏时间复杂度为 O(n)。

1.3 删除

删除元素则会比较复杂一点,初删除的结点可能是叶子结点、左子树或右子树只存在一个、左子树或右子树两个都存在。前两种情况比较简单一点(被删除结点 p,删除结点的父结点 pp)

  1. 被删除结点没有子结点:直接将其父结点对应的左子树或右子树设置为 null。如删除结点 4 时,pp.left = null 或 pp.right = null。
  2. 被删除结点只有一个子结点:直接将其父结点指向被删除结点的左子树或右子树。如删除结点 7 时,pp.left = p.left 或 pp.right = p.right。
  3. 被删除结点有两个子结点:此时删除比较复杂,需要先从被删除结点的右子树查找最小值结点 p2 赋值给被删除结点。这样就变成删除这个右子树查找最小值结点 p2 的问题,同第一种情况完全一样。如删除结点 20 时间,需要在其右子树中查找一个最小的值结点 25,将 20 对应结点的值替换成 25,然后再删除 25。
public void remove(int data) {
    Node p = tree;  // p指向要删除的节点,初始化指向根节点
    Node pp = null; // pp记录的是p的父节点
    while (p != null && p.data != data) {
        pp = p;
        if (data > p.data) p = p.right;
        else p = p.left;
    }
    // 没有找到
    if (p == null) return;

    // 要删除的节点有两个子节点
    if (p.left != null && p.right != null) { // 查找右子树中最小节点
        Node minP = p.right;
        Node minPP = p;             // minPP表示minP的父节点
        while (minP.left != null) {
            minPP = minP;
            minP = minP.left;
        }
        p.data = minP.data;         // 将minP的数据替换到p中
        p = minP;                   // 下面就变成了删除minP了
        pp = minPP;
    }

    // 删除节点是叶子节点或者仅有一个子节点
    Node child;                     // p的子节点
    if (p.left != null) child = p.left;
    else if (p.right != null) child = p.right;
    else child = null;

    if (pp == null) tree = child;   // 删除的是根节点
    else if (pp.left == p) pp.left = child;
    else pp.right = child;
}

说明: 删除时首先也要查找这个被删除的结点,时间复杂度为 O(logn)。如果有两个子结点时,还需要查找其右子树的最小值,时间复杂度也是 O(logn)。最后删除这个结点。因此总的时间复杂度也是 O(logn)。但如果退化成链表,时间复杂度还是 O(n)。

总结: 二叉查找树的时间复杂度都和树的高度息息相关,也就是 O(height)。如果满足平衡二叉树时树高度为 logn,如果退化成链表时树高度为 n。因此,实际工程使用中,我们使用的是平衡二叉查找树,但为什么又不是完全满足平衡二叉树的红黑树呢?我们在下节继续分析。

2. 二叉查找树 vs 哈希表

前面也说了,哈希表的插入、删除、查找的时间复杂度都是 O(1),而平衡二叉查找树的插入、删除、查找的时间复杂度都是 O(logn),那为什么还要使用二叉树呢?

  1. 散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。
  2. 散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定。而尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O(logn)。
  3. 尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。
  4. 散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。

参考:

  1. 《平衡二叉树的操作》:https://www.cnblogs.com/zhangbaochong/p/5164994.html
  2. 《实现一个自己的二叉查找树》:https://www.cnblogs.com/Dylansuns/p/6793032.html

每天用心记录一点点。内容也许不重要,但习惯很重要!

posted on 2018-12-11 08:18  binarylei  阅读(786)  评论(0编辑  收藏  举报

导航