一看就懂的二叉查找树和平衡二叉查找树

二叉查找树是一种特殊的二叉树,它可以组织动态数据集合,可以支持数据的快速插入,删除和查找操作。之前我们讨论过哈希表,他的,查找,插如和删除的时间复杂度是O(1),既然哈希表这么高效,那么为什么还需要二叉查找树呢?

二叉查找树的定义和操作

 

二叉查找树是二叉树中一种常用的一种类型。二叉查找树是为了实现快速查找产生的。不过,它不仅支持快速查找,还支持快速插入和删除。这主要归功于二叉查找树的一个特性,那就是树中任一节点,这个节点的左子树的值总是小于这个节点的值,这个节点右子树的值总是大于这个节点的值,如上图所示。下面看看他的查找,插入和删除操作是怎么实现的。

1.查找操作

需要查找一个节点的话,我们先取他的根节点,如果根节点不为空,恰巧还等于需要查找的值,那么直接返回根节点。如果要查找的值小于根节点,那么根据二叉查找树的性质,需要像左子树进行递归。同理,如果大于根节点,那么就要向右子树进行递归。

下面来看一张图

接下来看看代码

public class BinarySearchTree{
   public class Node{
      private int data;
      private Node left;
      private Node right;
      
      public Node(int data){
         this.data = data;
      }   
   } 
   
   private Node tree;
   
   public Node find(int data){
      Node p = tree;
      while(p != null){
         if(data > p.data) p = p.right
         else if(data < p.data) p = p.left;
         else return p;
      }
      return null;
   }

}

 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{
         if(p.left == null){
            p.left = new Node(data);
            retrun;
         }
         p = p.left;
      }
   }
}

3.删除操作

二叉查找树的删除有三种情况

第一种情况:要删除的节点没有子节点,那么我们直接就可以把父节点中指向要删除节点的指针置为null,如下图删除节点55

第二种情况:要删除的节点只有一个子结点,那么我们只需要更新父节点中指向要删除节点的指针,让他指向删除节点的子结点。如图删除节点13

第三种情况:要删除节点有两个子结点,这个时候需要找到这个删除节点的右子树的最小节点,把他替换到要删除节点上。然后删除最小节点,因为最小节点肯定没有左子节点。

 

public void delete(int data){
        Node p = tree;//p指向要删除的节点,初始化指向根节点
        Node pp = null;//pp记录的是p的父节点
        //先查找元素
        while(p != null && data != p.data){
            pp = p;
            if(data > p.data) p = p.right;
            else p = p.left;
        }
        if(p == null) return;//没有找到
        //要删除的节点有两个子节点
        while (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;//数据交换
            p = 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(n),非常高效

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

之前我们默认二叉查找树中不存在数据值相同的节点。针对数据值相同的节点的二叉树,我们有两种存储方式。

第一种:在二叉查找树中,每一个节点存储的不是一个数据,而是一组数据。通过链表和支持动态扩容的数组等数据结构,把直接相同的数据存储在同一个节点上。

第二种:(可以看下图),每一个节点仍然只存储一个数据。当插入数据时,在查找插入位置的过程中,如果碰到一个节点的值要与插入的值相同,我们就将要插入的数据放到这个节点的右子树,也就是说,某个节点的右子树中存储的是大于或等于这个节点的值。

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

 

删除和前面是差不多得,直接看看图

 

二叉查找树的性能分析

对于同一组数据,我们可以构建各种不同的二叉查找树。下图所示,他们是针对同一组数据构建的二叉查找树。由于结构不同,那么增删改的效率也不同,像第一个树,已经退化成了链表,查找的时间复杂度变为了O(n)。

 

这是一种极端的情况,二叉查找树的左右子树极度不平衡,退化成链表。一个理想的情况是 

一个完全二叉树或者满二叉树,一个完全二叉树的高度小于或等于log^{2}N(树的高度等于最大层数减一),他的时间复杂度为O(logn)。

文章开头的问题

哈希表并不能代替二叉树的主要原因

1.哈希表中的数据是无序存储的,如果要输出有序数据序列,需要先进行排序,或者配合有序链表使用。而对于二叉查找树,我们只需要中序遍历,就可以在O(n)的时间复杂度内,输出有限序列。

2.哈希表扩容耗时很多,而且,当遇到哈希冲突时,性能不稳定。在工程中,用平衡二叉排序树的性能非常稳定,时间复杂度O(logn)。

3.笼统地来说,尽管哈希表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的 存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈 希函数的耗时,也不一定就比平衡二叉查找树的效率高。

4,哈希表的构造比二叉查找树要复杂,需要考虑的东西很多。比如哈希函数的设计、冲 突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。

上面说到二叉查找树在频繁的动态更新的时候,在极端的情况下可能会退化为链表,导致时间复杂度降为O(n),所以为了避免时间复杂度退化的问题,我们来看看平衡二叉查找树。

平衡二叉查找树的定义

平衡二叉查找树的定义是这样的:二叉树中任一节点的左右子树的高度相差不能大于1,所以像之前介绍的完全二叉树和满二叉树都是平衡二叉树,非完全二叉树可能是平衡二叉树,我们要着重注意一下“平衡“这两个字的意思,可以简单理解为节点左右子树的分量大约相同,下面看几张图来判别一下。

此图就不是平衡二叉树,因为节点60的左子树不是平衡二叉树。

此图也不是平衡二叉树,因为并不满足一个节点的左右子树的高度相差不能超过1

 

此图为平衡二叉树。

平衡二叉查找树不仅满足平衡二叉树的特点还满足二叉查找树的特点。像AVL树,是最先提出来的平衡二叉查找树,他严格符合平衡二叉查找树的定义,不过今天要讨论的却是红黑树,红黑树并没有那么严格遵守这个规则,红黑树从根节点到各个叶子节点的最长路径有可能会比最短路径长一倍,具体的内容一会再说。

下面再来说说”平衡“这两个字,提出平衡二叉树是为了解决二叉查找树频繁的插入,删除等动态更新导致性能退化的问题。因此,平衡二叉树中的”平衡“的意思是让整个树变得”矮胖“一些,而不是”高瘦“,左右看起来比较对称和均衡,避免出现左子树很高,右子树很矮的情况。

红黑树的定义

红黑树是最长提及的二叉查找树,是一种相对平衡的二叉查找树,不符合严格意义上平衡二叉查找树的定义。

 对于红黑树的节点,一类被标记为黑色,一类被标记为红色,除此之外,还需要满足四个要求。

1.根节点是黑色的

2.每个叶子系欸但都是黑色的空节点(NIL),也就是说,叶子节点并不存储数据

3.任何上下相邻的节点不能同时为红色,也就是说,红色节点被黑色节点隔开

4.对于每个节点,从该节点到其叶子节点的所以路径,都包含相同数据的黑色节点

红黑树的性能分析

平衡二叉查找树的初衷,是为了解决二叉查找树因为动态更新导致的性能 退化问题。所以,“平衡”的意思可以等价为性能不退化。“近似平衡”就等价为性能不会 退化的太严重
二叉查找树很多操作的性能都跟树的高度成正比。一棵极其平衡的二叉 树(满二叉树或完全二叉树)的高度大约是 log^{2}N,所以如果要证明红黑树是近似平衡的,我们只需要分析,红黑树的高度是否比较稳定地趋近 log^{2}N 就好了。
红黑树的高度不是很好分析,我带你一步一步来推导。

 

首先,我们来看,如果我们将红色节点从红黑树中去掉,那单纯包含黑色节点的红黑树的高
度是多少呢?
红色节点删除之后,有些节点就没有父节点了,它们会直接拿这些节点的祖父节点(父节点
的父节点)作为父节点。所以,之前的二叉树就变成了四叉树。 前面红黑树的定义里有这么一条:从任意节点到可达的叶子节点的每个路径包含相同数目的 黑色节点。我们从四叉树中取出某些节点,放到叶节点位置,四叉树就变成了完全二叉树。
所以,仅包含黑色节点的四叉树的高度,比包含相同节点个数的完全二叉树的高度还要小。
之前我们说,完全二叉树的高度近似 log^{2}N  ,这里的四叉“黑树”的高度要低于完全二叉
树,所以去掉红色节点的“黑树”的高度也不会超过 log 2n。
我们现在知道只包含黑色节点的“黑树”的高度,那我们现在把红色节点加回去,高度会变
成多少呢?
从上面我画的红黑树的例子和定义看,在红黑树中,红色节点不能相邻,也就是说,有一个
红色节点就要至少有一个黑色节点,将它跟其他红色节点隔开。红黑树中包含最多黑色节点
的路径不会超过 log2n,所以加入红色节点之后,最长路径不会超过 2log^{2}N ,也就是说,
红黑树的高度近似 2log^{2}N  
所以,红黑树的高度只比高度平衡的 AVL 树的高度(log^{2}​​​​​​​N  )仅仅大了一倍,在性能上,  
下降得并不多。这样推导出来的结果不够精确,实际上红黑树的性能更好。

最后,其实红黑树只做到了近似平衡,并没有做到严格意义上的平衡,因此维护平衡性的成本比AVL树要底,但性能损失不大,所以一般用红黑树比较多。

 

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值