24 | 二叉树基础(下):有了如此高效的散列表,为什么还需要二叉树?

这节学习一种特殊的二叉树—二叉查找树。它最大的特点是支持动态数据集合的快速插入、删除、查找操作。但是散列表也是支持这些操作的,并且散列表的这些操作比二叉查找树更高效,时间复杂度是 O(1)

问题引入

既然有高效的散列表,二叉树的地方是不是都可以替换成散列表呢?哪些地方是散列表做不了,必须要用二叉树来做?

二叉查找树(Binary Search Tree)是二叉树中最常用的一种类型,也叫二叉搜索树。它不仅支持快速查找一个数据,还支持快速插入、删除一个数据。二叉查找树要求在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。二叉查找树支持快速查找、插入、删除操作,这三个操作是如何实现的。

1.二叉查找树的查找操作

如何在二叉查找树中查找一个节点。先取根节点,如果等于要查找的数据那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。查找的代码实现

public class BinarySearchTree {

  private Node tree;

  public Node find(int data) {

    Node p = tree;

    while (p != null) {

      if (data < p.data) p = p.left;

      else if (data > p.data) p = p.right;

      else return p;

    }

    return null;

  }

  public static class Node {

    private int data;

    private Node left;

    private Node right;

 

    public Node(int data) {

      this.data = data;

    }

  }

}

2.二叉查找树插入操作

插入过程有点类似查找。新插入数据一般都是在叶子节点上,从根节点开始依次比较要插入的数据和节点的大小关系。如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,类推。插入的代码

public void insert(int data) {

  if (tree == null) {

    tree = new Node(data);

    return;

  }

  Node p = tree;

  while (p != null) {

    if (data > p.data) {

      if (p.right == null) {

        p.right = new Node(data);

        return;

      }

      p = p.right;

    } else { // data < p.data

      if (p.left == null) {

        p.left = new Node(data);

        return;

      }

      p = p.left;

    }

  }

}

3. 二叉查找树删除操作

删除操作就比较复杂,针对要删除节点的子节点个数不同需要分三种情况来处理。

第一种情况是,如果要删除的节点没有子节点,只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。

第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),只需要更新父节点中指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。

第三种情况是,如果要删除的节点有两个子节点。需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。

https://static001.geekbang.org/resource/image/29/2c/299c615bc2e00dc32225f4d9e3490e2c.jpg

public void delete(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;

}

关于二叉查找树的删除操作,还有个非常简单、取巧的方法,就是单纯将要删除的节点标记为“已删除”,但是并不真正从树中将这个节点去掉。这样原本删除的节点还需要存储在内存中,比较浪费内存空间,但是删除操作就变得简单了很多。而且,这种处理方法也并没有增加插入、查找操作代码实现的难度

4.二叉查找树的其他操作

除了插入、删除、查找操作之外,二叉查找树中还可以支持快速地查找最大节点和最小节点、前驱节点和后继节点。二叉查找树除了支持上面几个操作之外,还有一个重要的特性,就是中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是 O(n),非常高效。因此,二叉查找树也叫作二叉排序树。

5.支持重复数据的二叉查找树

二叉查找树除了存储数字外,在实际的软件开发中存储的,是一个包含很多字段的对象。我们利用对象的某个字段作为键值(key)来构建二叉查找树。把对象中的其他字段叫作卫星数据。前面的二叉查找树的操作,针对的都是不存在键值相同的情况。那如果存储的两个对象键值相同,这种情况该怎么处理呢?

有两种解决方法。第一种方法比较容易。二叉查找树中每一个节点不仅会存储一个数据,因此我们通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。第二种方法比较不好理解,不过更加优雅。每个节点仍然只存储一个数据。在查找插入位置的过程中,如果碰到一个节点的值,与要插入数据的值相同,我们就将这个要插入的数据放到这个节点的右子树,也就是说,把这个新插入的数据当作大于这个节点的值来处理。

当要查找数据的时候,遇到值相同的节点,我们并不停止查找操作,而是继续在右子树中查找,直到遇到叶子节点,才停止。这样就可以把键值等于要查找值的所有节点都找出来。

对于删除操作,也需要先查找到每个要删除的节点,然后再按前面讲的删除操作的方法,依次删除。

6.二叉查找树的时间复杂度分析

二叉查找树的插入、删除、查找操作的时间复杂度。

如何求一棵包含 n 个节点的完全二叉树的高度?树的高度就等于最大层数减一,为了方便计算,我们转换成层来表示。包含 n 个节点的完全二叉树中,第一层包含 1 个节点,第二层包含 2 个节点,第 K 层包含的节点个数就是 2^(K-1)。对于完全二叉树来说,最后一层的节点个数在 1 个到 2^(L-1) 个之间(我们假设最大层数是 L)。如果我们把每一层的节点个数加起来就是总的节点个数 n。也就是说,如果节点的个数是 n,那么 n 满足这样一个关系:n >= 1+2+4+8+...+2^(L-2)+1n <= 1+2+4+8+...+2^(L-2)+2^(L-1)借助等比数列的求和公式, L 的范围是[log2(n+1), log2n +1]。

完全二叉树的层数小于等于 log2n +1,完全二叉树的高度小于等于 log2n。我们需要构建一种不管怎么删除、插入数据,在任何时候,都能保持任意节点左右子树都比较平衡的二叉查找树,一种特殊的二叉查找树—平衡二叉查找树。平衡二叉查找树的高度接近 logn,所以插入、删除、查找操作的时间复杂度也比较稳定,是 O(logn)。

散列表的插入、删除、查找操作的时间复杂度可以做到常量级的 O(1),非常高效。而二叉查找树在比较平衡的情况下,插入、删除、查找操作时间复杂度才是 O(logn),相对散列表,好像并没有什么优势,那我们为什么还要用二叉查找树呢?

有下面几个原因:

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

综合这几点,平衡二叉查找树在某些方面还是优于散列表的,这两者的存在并不冲突。需要结合具体的需求来选择使用哪一个。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/419079.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

25 | 红黑树(上):为什么工程中都用红黑树这种二叉树?

问题引入 二叉查找树在频繁的动态更新过程中&#xff0c;可能会出现树的高度远大于 log2n 的情况&#xff0c;从而导致各个操作的效率下降。极端情况下&#xff0c;二叉树会退化为链表&#xff0c;时间复杂度会退化到 O(n)。要解决这个复杂度退化的问题&#xff0c;需要设计一…

Rabbitmq如何设置优先级队列?如何限流?如何重试?如何处理幂等性?

优先级队列 方式一&#xff1a;可以通过RabbitMQ管理界面配置队列的优先级属性&#xff0c;如下图的x-max-priority 方式二&#xff1a;代码设置 Map<String,Object> args new HashMap<String,Object>(); args.put("x-max-priority", 10); channel.que…

【Qt】Qt之进程间通信(Windows消息)【转】

简述 通过上一节的了解&#xff0c;我们可以看出进程通信的方式很多&#xff0c;今天分享下如何利用Windows消息机制来进行不同进程间的通信。 简述效果发送消息 自定义类型与接收窗体发送数据接收消息 设置标题重写nativeEvent效果 发送消息 自定义类型与接收窗体 包含所需库&…

启动nginx服务报错Job for nginx.service failed because the control process exited with error code.

nginx使用service nginx restart报错 启动nginx服务时如果遇到这个错误 Job for nginx.service failed because the control process exited with error code. See "systemctl status nginx.service" and "journalctl -xe" for details. 可能原因: 1、配…

27 | 递归树:如何借助树来求解递归算法的时间复杂度?

目的 借助递归树来分析递归算法的时间复杂度 递归树 递归的思想就是将大问题分解为小问题来求解&#xff0c;然后再将小问题分解为小小问题。这样一层一层地分解&#xff0c;直到问题的数据规模被分解得足够小&#xff0c;不用继续递归分解为止。 如果我们把这个一层一层的…

28 | 堆和堆排序:为什么说堆排序没有快速排序快?

如何理解“堆” 堆排序是一种原地的、时间复杂度为 O(nlogn) 的排序算法 堆的两个特点&#xff1a; 一颗完全二叉树堆中每个节点都必须大于等于&#xff08;或者小于等于&#xff09;其左右子节点的值&#xff1b; 对于每个节点的值都大于等于子树中每个节点值的堆&#xff…

29 | 堆的应用:如何快速获取到Top 10最热门的搜索关键词?

为什么评价算法性能是根据时间和空间复杂度&#xff0c;而不是别的参数&#xff1f;是因为计算机结构是冯诺依曼体系&#xff0c;除了输入输出设备和控制器&#xff0c;就剩下运算器和存储器了 问题引入 搜索引擎的热门搜索排行榜功能是如何实现的&#xff1f;搜索引擎每天会…

多线程——线程间的同步通信

1、概要 线程间的相互作用&#xff1a;线程之间需要一些协调通信&#xff0c;来共同完成一件任务。线程间的协调通信主要通过wait方法和notify方法来完成。因为wait和notify方法定义在Object类中&#xff0c;因此会被所有的类所继承。这些方法都是final的&#xff0c;即它们都是…

30 | 图的表示:如何存储微博、微信等社交网络中的好友关系?

列出功能需求->翻译成逻辑算法->抽象出数据结构->确定物理存储结构 后面的不会脱离前面的独立存在&#xff0c;只存在于工作流的运用中&#xff0c;所以不能把它们独立地看。 问题引入 在微博中&#xff0c;两个人可以互相关注&#xff1b;在微信中&#xff0c;两个…