数据结构中的各种树(二叉查询树、平衡树、B/B+树、红黑树)

2,207 阅读16分钟

引言

  • 我们都学过数据结构,里面很重要的一种结构就是"树结构"
  • 二叉树、完全二叉树、平衡二叉树、二叉查找树等等我们都应该是比较熟悉的——回忆不起来的同学自己课后去补哈,这里就不说了
  • 今天咱们就讲讲几种比较让人头疼,但是有很好用的树结构:B树(又称B-树)、B+树、红黑树

基础回顾

本来是不准备将二叉树基础的,这个真的是基础中的基础,但是我相信不明白的同学课后也不一定会去补,算了,这边我还是和大家一起回忆一下吧

二叉树

二叉树特点

  • 每个节点最多拥有2个子树,左右节点是有顺序的
  • 一个非空二叉树的第i层,最多有2^(i-1)个节点
  • 一个高度为h的二叉树最多有2^h-1个节点——反过来说一个拥有n个节点的二叉树最小高度为log2(n)

几种特殊形态的二叉树

  • 真二叉树:所有的节点的度要么是0,要么都是2
  • 满二叉树:所有节点的度要么都是0 ,要么都是2,并且所有的叶子节点都在最后一层——真二叉树附加一个条件
  • 完全二叉树:说实话,这三种二叉树中,就属完全二叉树有点难捉摸,它的标准定义是这样的:
    • 对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。
    • 你品,你细品,其实简单的理解就是:对于完全二叉树来说,叶子节点只会出现在最后2层,且最后一层的叶子节点都靠左对齐。

真二叉树、满二叉树、完全二叉树的区别联系

  • 真二叉树 和 满二叉树 要求 除叶子节点外,每个分支节点都是满的(都有左右孩子节点),满足这个条件,就是真二叉树,如果在此条件上再加上一个条件,所有叶子节点在同一层级,那就是满二叉树;
  • 对于完全二叉树,叶子结点只能在最后两层,且除了最后一层叶节点,其他节点都是满的,即如果倒数第二层也有叶子节点,那必须是满的,最后一层的叶子结点不一定满,但是只能是左孩子。
有点抽象,自己去领悟吧^~^

进阶

二叉查找树(二叉搜索树)

什么样的树是二叉查找树呢

  1. 左子树所有节点的值均小于或等于它的根结点的值;
  2. 右子树上所有结点的值均大于或等于它的根结点的值
  3. 左、右子树也分别为二叉排序树 不难看出,二叉查找树的定时其实是一个递归定义,下面是一个典型的二叉查找树:
    image

提出二叉查找树有啥用呢

二叉树的提出其实主要就是为了提高查找效率,但是二叉树本身没有太大用处,只是一个树形的数据结构而已。但是如果我们规定二叉树上元素,所有比它小的元素在它的左子树,比它大的元素在它的右子树,那么我们不就可以很快的查找某个元素了吗?——还记得数据结构里边的二分查找吗

  • 可以想象,在理想情况下,一个n个节点的二搜索树,对应树的高度为log2(n),所以我们查找元素、插入元素的时间也为log2(n);
  • 不过这毕竟是理想情况,举个例子:如果插入的数据按照递增或者递减的顺序出现,那么所有的元素会线性排列,树形结构会退化成链表,显然在这种情况下,查找和插入的效率会蜕化成O(n)
为了解决二叉搜索树不平衡的问题,平衡二叉树诞生了

平衡二叉树

平衡二叉树的出现就是为了保证树不至于太倾斜,尽量保证两边平衡;那么什么样的树称为平衡二叉树呢?

  • 定义:要么是一个空树;要么保证左右子树的高度差 <= 1,同时每一个子树都是平衡二叉树(递归环定义)
  • 当然,为保持二叉树的平衡性,平衡二叉树在添加和删除节点时需要进行旋转以保持树的平衡
  • 既然能保持左右子树的高度差 <= 1,其实也就保证的平衡二叉树的插入、查询的时间复杂度都是O(log2(n))
  • 上面图示的二叉查找树其实就是平衡二叉树

重点

B树(B-树)

这边的B-树,不是B减树,中间那个是一个杠,千万别读成B减树啦,丢人^~^

为什么数据库索引没有选用二叉查找树作为存储结构

我们都知道,数据库索引使用的是树结构存储,是因为树的查询效率高,而且可以保持有序,但是为什么没有使用二叉查找树而选用了B-/B+树来实现呢?二叉查找树的时间复杂度是O(log2(n)),效率已经很高了呀,为嘛数据库索引没有选用它作为数据结构呢?

  • 是的,从算法逻辑上来讲,二叉查找树的查询速度和比较次数都是最小的。但是我们不得不考虑一个现实问题:磁盘IO;
  • 数据库索引是存储在磁盘上的,当数据量比较大的时候,索引的大小有可能达到G级别甚至更多,当我们利用索引查询的时候,不可能将整个索引全部加载到内存,而是逐一加载每个磁盘页,磁盘页对应着索引树的节点; 我们先来看一下上面的二叉查找树查找元素 7 的查询过程:
    image
  1. 第一次磁盘IO【将9对应的磁盘块数据加载到内存中】
  2. 在内存中定位【和9比较 一次】
  3. 第二次磁盘IO【将6对应的磁盘块数据加载到内存中】
  4. 在内存中定位【和6比较 一次】
  5. 第三次磁盘IO【将8对应的磁盘块数据加载到内存中】
  6. 在内存中定位【和8比较 一次】
  7. 第四次磁盘IO【将7对应的磁盘块数据加载到内存中】
  8. 在内存中定位【和7比较 一次】
可以看出,在二叉查找树中,查找元素7进行了4次磁盘IO和4次内存比较
  • 现在我们来考虑,如果利用二叉查找树作为索引结构,容易得知,磁盘IO的次数取决于树的高度,而且最坏情况下,磁盘IO次数等于树的高度;
  • 所以,减少磁盘IO的次数就必须要压缩树的高度,让瘦高的树尽量变成矮胖的树,所以B-树就在这样伟大的时代背景下诞生了。

那B-树究竟是什么样的一类树呢?

m阶B-树满足以下条件:

  1. 根节点至少有2个子树
  2. 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
  3. 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
  4. 所有的叶子结点都位于同一层
  5. 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划

B-树是如何实现高效查询的呢?

下面已跟上面二叉查找树一样的元素,构造一个3阶B-树为例,查找元素5的过程:

image

image

查找过程如下:

  1. 第一次磁盘IO【将9对应的磁盘块数据加载到内存中】
  2. 在内存中定位【和9比较 1次】
  3. 第二次磁盘IO【将4和7对应的磁盘块数据加载到内存中】
  4. 在内存中定位【和4,7比较 2次】
  5. 第三次磁盘IO【将5和6对应的磁盘块数据加载到内存中】
  6. 在内存中比较【和5,6比较 1次】
可以看出,在3阶B树中,查找元素5进行了3次磁盘IO和4次内存比较

从查找过程中发现:

  • B树其实是一种多路平衡二叉树,它的每个节点最多包含m个子节点,m被称为B树的阶,它的大小取决于磁盘页大小
  • B树的比较次数和磁盘IO的次数与二叉树相差不了多少,所以这样看来并没有什么优势;
  • 但是我们要知道,比对是在内存中完成的,不涉及到磁盘IO,耗时可以忽略不计,而且B-树中一个节点中可以存放很多的key(个数由树阶决定);
  • 相同数量的key在B树中生成的节点要远远少于二叉树中的节点,相差的节点数量就等同于磁盘IO的次数。这样到达一定数量后,性能的差异就显现出来了;
  • 只要树的高度足够低,IO次数足够少,就可以提升查找性能。

B-树是如何实现插入删除的呢?

其实,从B-的定义上就能看出,对于一个B-树的限制条件很多,我们都知道,限制条件多,就意味着我们在插入、删除元素的时候,为了维持这些限制条件,可能会变得非常麻烦,

对于上面的四阶B-树,实现插入和删除,有兴趣的同学可以自行学习,这里主要将查询效率,所以不做过多演示;

B-树主要应用于文件系统以及部分数据库索引,如MongoDB,大部分关系型数据库索引则是使用B+树实现。

好了,既然提到了mysql的索引,那B+树是一类什么样的树?为什么mysql采用B+树作为索引呢?

B+树

B+树定义

一个m阶的B+树满足以下条件:

  1. 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点
  2. 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接
  3. 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素

宏观理解B+树

看着定义很复杂,下面用个例子来看下B+树的结构:

image

  • 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点
  • 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接
  • 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素

卫星数据

B+树B-树的一个重要不同就是,就是卫星数据的位置

卫星数据是啥鬼东西

所谓卫星数据,指的是索引所指向的数据记录 比如数据库中的某一行,在B-树中,无论是中间节点还是叶子节点都带有卫星数据 B+树中中间非叶子节点不存放卫星数据,卫星数据位置只跟叶子结点有关: - 在数据库的聚集索引(ClusteredIndex)中,叶子节点直接包含卫星数据 - 在非聚集索引(NonClusteredIndex)中,叶子节点带有指向卫星数据的指针

那么B+树这么设计,有啥好处呢?

B+树这么设计,好处主要体现在查询性能上,尤其是单行查询和范围查询

B+树查询过程演示

下面已跟上面二叉查找树一样的元素,构造一个3阶B+树为例

单行查询演示,查找元素5的过程:

image
查找过程如下:

  1. 第一次磁盘IO【将7、9对应的磁盘块数据加载到内存中】
  2. 在内存中定位【和7比较 1次】
  3. 第二次磁盘IO【将4、6、7对应的磁盘块数据加载到内存中】
  4. 在内存中定位【和4,6比较 2次】
  5. 第三次磁盘IO【将5和6对应的磁盘块数据加载到内存中】
  6. 在内存中比较【和5比较 1次】

可以看出,在3阶B+树中,查找元素5进行了3次磁盘IO和4次内存比较

  • 查询流程看起来跟B-树差不多
  • 但是,有两个很大的不同
    • 首先,B+树中间节点没有卫星数据,所以同样大小的磁盘页可以容纳更多的节点元素,这意味着,数据量相同情况下,B+树的结构比B-树更矮胖,因此查询IO次数也更少;
    • 其次,B+树查询必须最终查到叶子节点,而B-树只要找到匹配元素即可,因此B-树的查询性能并不稳定【最好情况就是只查根节点,最坏情况是查到叶子节点】,而B+树每次查询都是稳定的

范围行查询演示,查找5~11之间的所有元素的过程:

我们可以知道,在B-树中进行范围查询,只能依靠繁琐的中序遍历,感兴趣的同学可以自行学习

image

查找过程如下:

  1. 自顶向下,查询到范围下限元素5
  2. 通过链表指针,遍历到元素7和8
  3. 通过链表指针,遍历到元素9和11

B+树的优势

综上可知,B+树相比B-树的优势有3个:

  1. 单一节点存储更多的元素,使得查询的IO次数更少
  2. 所有查询都要查找到叶子节点,查询性能稳定
  3. 所有叶子节点形成有序链表,便于范围查询

为什么MongoDB索引采用B-树,而MySQL索引采用B+树

首先我们要总结下,B/B+树的区别:

  1. B+树查询时间复杂度固定是logn,B-树查询复杂度最好是 O(1)
  2. B+树相邻接点的指针可以大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找
  3. B+树更适合外部存储,也就是磁盘存储。由于内节点无 data 域,每个节点能索引的范围更大更精确
  4. 注意这个区别相当重要,是基于(1)(2)(3)的,B-树每个节点即保存数据又保存索引,所以磁盘IO的次数很少,B+树只有叶子节点保存,磁盘IO多,但是区间访问比较好。

接着,我们需要了解下索引:

  1. 索引本身也很大,不可能全部存储在内存中,往往以索引文件的形式存储的磁盘上
  2. 因此,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级
  3. 所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数

然后我们要了解下MongoDB和MySQL的概念:

  1. MongoDB 是文档型的数据库,是一种 nosql,它使用类 Json 格式保存数据
  2. MongoDB使用B-树,所有节点都有Data域,只要找到指定索引就可以进行访问,无疑单次查询平均快于Mysql
  3. Mysql作为一个关系型数据库,数据的关联性是非常强的,区间访问是常见的一种情况,B+树由于数据全部存储在叶子节点,并且通过指针串在一起,这样就很容易的进行区间遍历甚至全部遍历。

红黑树

为什么要有红黑树呢

  • 我们知道,二叉查找树会因为插入元素的顺序不同,可能会退化成链表,这个时候插入和查询复杂度都会变成O(log2(n)),出现这种情况的原因呢就是二叉查找树没有自平衡机制
  • 于是有了平衡二叉树,平衡二叉树保证了在最差的情况下,二叉树依然能够保持绝对的平衡,即左右两个子树的高度差的绝对值不超过1
  • 但是这又会带来一个问题,那就是平衡二叉树的定义过于严格,导致每次插入或者删除一个元素之后,都要去维护二叉树整体的平衡,这样产生额外的代价又太大了
  • 二叉搜索树可能退化成链表,而平衡二叉树维护平衡的代价开销又太大了,那怎么办呢?这就要谈到“中庸之道”的智慧了
  • 说白了就是把平衡的定义适当放宽,不那么严格,这样二叉树既不会退化成链表,维护平衡的开销也可以接受。没错,这就是我们要谈的红黑树了。

红黑树特性

红黑树是一种自平衡的二叉查找树,除了符合二叉查找树的基本特性之外,还具有以下特性:

  1. 节点是红色或者黑色的
  2. 根节点是黑色的
  3. 每个叶子节点都是黑色的空节点
  4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

红黑树的例子

image

索引的存储结构为什么不用红黑树呢?

注意一点,红黑树是一种二叉树,这意味着,相同数量的节点,红黑树要比B/B+树更高,查询时的IO次数也就多,所以一般涉及到磁盘上查询的数据结构,用B+树