B/B+树 AVL 红黑树

B/B+树在MySQL索引中的应用,参见索引

B/B+树

  B/B+树是一种平衡多路查找树(相对于二叉,B树每个内节点有多个分支),与红黑树相比,在相同的的节点的情况下,一颗B/B+树的高度远远小于红黑树的高度(在下面B/B+树的性能分析中会提到)。B/B+树上操作的时间通常由存取磁盘的时间和CPU计算时间这两部分构成,而CPU的速度非常快,所以B树的操作效率取决于访问磁盘的次数,关键字总数相同的情况下B树的高度越小,磁盘I/O所花的时间越少。

B树的性质

一个m阶的B树规定了:

  • 根结点至少有两个子女
  • 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
  • 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
  • 所有的叶子结点都位于同一层
  • 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划
    在这里插入图片描述
    上面的图中的35节点中35代表一个key(索引),而小黑块代表的是这个key所指向的内容在内存中实际的存储位置,是一个指针。
      B树每一层存放了更多的节点,由AVL树的“瘦高”变成了“矮胖”。可以相对减少磁盘IO的次数。MongoDB的索引就是用B树实现的。B树也是一种自平衡的树,在进行插入和删除操作时也需要对结点进行旋转等操作。不过,B树的查找不稳定,最好的情况就是在根节点查到了,最坏的情况就是在叶子结点查到。另外,B树在遍历方面比较麻烦,由于需要进行中序遍历,所以也会进行一定数量的磁盘IO。为了解决这些问题,出现了B+树。

  B 树应用在数据库和文件系统当中。它的设计思想是,将相关数据尽量集中在一起,以便一次读取多个数据,减少硬盘操作次数。B树算法减少定位记录时所经历的中间过程,从而加快存取速度。

B+树

  B+树是应文件系统所需而产生的一种B树的变形树(文件的目录一级一级索引,只有最底层的叶子节点(文件)保存数据),非叶子节点只保存索引,不保存实际的数据;数据都保存在叶子节点中

B+树的性质
一个m阶的B+树规定了:

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

看下图:
在这里插入图片描述
非叶子节点(比如5,28,65)只是一个key(索引),实际的数据存在叶子节点上(5,8,9)才是真正的数据或指向真实数据的指针。因为非叶子结点中存放的元素不存放数据,所以每一层可以容纳更多元素,也就是磁盘中的每一页可以存放更多元素。这样在查找时,磁盘IO的次数也会减少。另外,B+树的查找稳定,因为所有的数据都在叶子结点。每个叶子结点也通过指针指向构成了一种链表结构,所以遍历数据也会简单很多。

B+树的非叶子节点值在实际数据库中代表着索引,叶子节点的值在实际数据库中代表着指向数据的指针,指针在实际数据库中代表着数据存储的地址。

B+树的插入操作
在B+树中插入关键字时,需要注意以下几点:

  • 插入的操作全部都在叶子结点上进行,且不能破坏关键字自小而大的顺序
  • 由于 B+树中各结点中存储的关键字的个数有明确的范围,做插入操作可能会出现结点中关键字个数超过阶数的情况,此时需要将该结点进行 “分裂”

B+树中做插入关键字的操作,有以下 3 种情况:
1、 若被插入关键字所在的结点,其含有关键字数目小于阶数 M,则直接插入
2、 若被插入关键字所在的结点,其含有关键字数目等于阶数 M,则需要将该结点分裂为两个结点,一个结点包含 ⌊M/2⌋ ,另一个结点包含 ⌈M/2⌉ 。同时,将⌈M/2⌉的关键字上移至其双亲结点。假设其双亲结点中包含的关键字个数小于 M,则插入操作完成
3、在第 2 情况中,如果上移操作导致其双亲结点中关键字个数大于 M,则应继续分裂其双亲结点
4、若插入的关键字比当前结点中的最大值还大,破坏了B+树中从根结点到当前结点的所有索引值,此时需要及时修正后,再做其他操作
B+树插入、删除操作

B/B+树性能分析

  层数log(m)n,每一层平均都要遍历m/2个节点才能确定往下面哪一个分支走,所以一共是(m/2) * log(m)n,系数1/2不影响复杂度可以略掉,对mlog(m)n,可以把m放到底数中去,即log(m的1/m次方)n。当m取3时,底数取最大值,复杂度取最小值,此时底数约为三次根号3,约等于1.44。(通常取最小值m=3,此时B-树中每个内部结点可以有2或3个孩子,这种3阶的B-树称为2-3树)

为什么说B+tree比B树更适合实际应用中操作系统的文件索引和数据索引

  • 磁盘读写代价更低
      因为存储引擎的设计巧妙利用了磁盘的存储结构,即磁盘的最小存储单位是扇区(sector),而操作系统的块(block)通常是整数倍的sector,操作系统以页(page)为单位管理内存,一页(page)通常默认为4K,数据库的页通常设置为操作系统页的整数倍,因此索引结构的节点被设计为一个页的大小。每次读取的时候,把整个节点的数据读取到内存中,然后在内存中查找。而提升查找速度的关键就在于尽可能少的磁盘I/O,每个节点中的key个数越多,那么树的高度越小,需要I/O的次数越少。因为B+Tree的非叶节点中不存储数据,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了
  • 查询速度更稳定
      由于B+Tree非叶子节点不存储数据(data),因此所有的数据都要查询至叶子节点,而叶子节点的高度都是相同的,因此所有数据的查询速度都是一样的
  • B树在提高了IO性能的同时并没有解决元素遍历的效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作
操作平衡二叉树红黑树
查找O(logn)O(logn)
插入O(logn)O(logn)
删除O(logn)O(logn)

二叉查找树

  二叉查找树也称为有序二叉查找树,满足二叉查找树的一般性质是指一棵空树具有如下性质:

  • 任意节点左子树不为空,则左子树的值均小于根节点的值
  • 任意节点右子树不为空,则右子树的值均大于于根节点的值
  • 任意节点的左右子树也分别是二叉查找树
  • 没有键值相等的节点.

局限性及应用
  一个二叉查找树是由n个节点随机构成,所以对于某些极端情况,二叉查找树会退化成一个有n个节点的线性链。如下图:
在这里插入图片描述
b图为一个普通的二叉查找树,大家看a图,如果根节点选择是最小或者最大的数,那么二叉查找树就完全退化成了线性结构。因此,在二叉查找树的基础上,又出现了AVL树和红黑树。它们两个都是基于二叉查找树,只是在二叉查找树的基础上又对其做了限制。
  如果二叉排序树是平衡的,则n个节点的二叉排序树的高度为Log2(n+1),其查找效率为O(Log2n),近似于折半查找。如果二叉排序树完全不平衡,则其深度可达到n,查找效率为O(n),退化为顺序查找。一般的,二叉排序树的查找性能在O(Log2n)到O(n)之间。因此,为了获得较好的查找性能,就要构造一棵平衡的二叉排序树。

AVL树

  AVL树是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。和红黑树相比,它是严格的平衡二叉树。平衡因子指的是该节点的两个子树,即用左子树的高度减去右子树的高度不管是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,由此AVL树适合用于插入删除次数比较少,但查找多的情况

旋转操作

  进行旋转处理的时候分成两种大类处理:单旋转和双旋转。根据树型结构的不同,可以分成四种情况来进行旋转处理:
1、在一个节点的左子树的左子树上插入一个新节点,即LL。在这种情况下,可以通过将节点右旋使其平衡。
2、在一个节点的右子树的右子树上插入一个新节点,即RR。在这种情况下,可以通过将节点左旋使其平衡。
3、在一个节点的左子树的右子树上插入一个新节点,即LR。在这种情况下,不能直接通过将节点左旋或右旋来使其平衡了。这里需要两步来完成,先让树中高度较低的进行一次左旋,这个时候就变成了LL了。再进行一次单右旋操作即可。
4、在一个节点的右子树的左子树上插入一个新节点,即RL。在这种情况下,不能直接通过将节点左旋或右旋来使其平衡了。这里需要两步来完成,先让树中高度较低的进行一次右旋,这个时候就变成了RR了。再进行一次单左旋操作即可。

局限性
  由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。当然,如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树

应用
Windows NT内核中广泛存在

红黑树

  一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是red或black。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍。它是一种弱平衡二叉树(由于是若平衡,可以推出,相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数变少,所以对于搜索、插入、删除操作多的情况下,使用红黑树

性质

  • 每个节点非红即黑
  • 根节点是黑的
  • 每个叶节点(叶节点即树尾端NUL指针或NULL节点)都是黑的
  • 如果一个节点是红的,那么它的两儿子都是黑的
  • 对于任意节点而言,其到叶子点树NIL指针的每条路径都包含相同数目的黑节点
  • 每条路径都包含相同的黑节点
    在这里插入图片描述
    应用
    1、广泛用于C ++的STL中,地图和集都是用红黑树实现的
    2、著名的Linux的的进程调度完全公平调度程序,用红黑树管理进程控制块,进程的虚拟内存区域都存储在一颗红黑树上,每个虚拟地址区域都对应红黑树的一个节点,左指针指向相邻的地址虚拟存储区域,右指针指向相邻的高地址虚拟地址空间
    3、IO多路复用的epoll的实现采用红黑树组织管理的sockfd,以支持快速的增删改查
    4、Nginx的的的中用红黑树管理定时器,因为红黑树是有序的,可以很快的得到距离当前最小的定时器
    5、Java的的的中TreeMap中的中的实现

  红黑树在查找方面和AVL树操作几乎相同。但是在插入和删除操作上,AVL树每次插入删除会进行大量的平衡度计算,红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,结合变色,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。
  相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。
  红黑树的算法时间复杂度和AVL相同都是O(logN),但统计性能比AVL树更高,所以红黑树应用还是高于AVL树的。实际上插入AVL树和红黑树的速度取决于所插入的数据,如果数据分布较好,则比较宜于采用 AVL树(例如随机产生系列数),但是如果想处理比较杂乱的情况,则红黑树是比较快的。红黑树广泛用于TreeMap、TreeSet,以及jdk1.8后的HashMap。

红黑树插入数据(变色,左旋、右旋)

  1. 红黑树在插入数据的时候,会先遍历数据应该插入到哪个位置,插入的位置肯定在底部,不可能在中间突然插入一个值
  2. 插入的数据一定是红色的(因为要遵守红黑树的第五条规则,如果有一条分支增加了一个黑色节点,就会打破该规则)
  3. 插入之后,为了满足规则4,就需要用到换色与左旋、右旋的操作了

旋转分两种:
左旋::对节点进行左旋,相当于把节点的右节点作为其父节点,即将节点变成一个左节点。原始的样子:
在这里插入图片描述
正在变换:
在这里插入图片描述
变换结束:
在这里插入图片描述
**右旋:**对节点进行右旋,相当于把节点的左节点作为其父节点,即将节点变成一个右节点。原始的样子:
在这里插入图片描述
正在变换:
在这里插入图片描述
变换结束:
在这里插入图片描述
关于红黑树旋转的实现,其实每个节点有这么几个属性:
1、父节点的指向
2、子节点的指向(有两个,一个左子节点,一个右子节点)
3、该节点的颜色
4、该节点存储的value(当然如果是map,那就是该节点的key和value都要存,然后按照key的大小来插入新节点)

所以当做旋转的时候,只需要把该节点的父节点指向该节点的子节点,然后把子节点的一条腿接到该节点上。

插入:① 被插入的节点是根节点,直接将其涂为黑色。② 被插入节点的父节点是黑色的,不做处理,节点插入后仍是红黑树。③ 被插入节点的父节点是红色的,一定存在非空祖父节点,根据叔叔节点的颜色分类处理。

删除:① 被删除的节点没有子节点,直接将其删除。② 被删除节点只有一个子节点,直接删除该节点,并用其唯一子节点替换其位置。③ 被插入节点有两个子节点,先找出该节点的替换节点,然后把替换节点的数值复制给该节点,删除替换节点。

调整平衡:在插入和删除节点后,通过左旋、右旋或变色使其重新成为红黑树。① 如果当前节点的子节点是一红一黑,直接将该节点设为黑色。② 如果当前节点的子结点都是黑色,且当前节点是根节点,则不做处理。③ 如果当前节点的子节点都是黑色且当前节点不是根节点,根据兄弟节点的颜色分类处理。

哈夫曼树

哈夫曼树相关的几个名词
路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径
路径长度:在一条路径中,每经过一个结点,路径长度都要加 1 。例如在一棵树中,规定根结点所在层数为1层,那么从根结点到第 i 层结点的路径长度为 i - 1
结点的权:给每一个结点赋予一个新的数值,被称为这个结点的权
结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积
WPL:树的带权路径长度为树中所有叶子结点的带权路径长度之和

什么是哈夫曼树
  当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”,有时也叫“赫夫曼树”或者“哈夫曼树”。
  在构建哈弗曼树时,要使树的带权路径长度最小,只需要遵循一个原则,那就是:权重越大的结点离树根越近。

构建哈夫曼树
对于给定的有各自权值的 n 个结点,构建哈夫曼树有一个行之有效的办法:
1、在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和
2、在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推
3、重复 1 和 2 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树

哈弗曼树中结点结构
  构建哈夫曼树时,首先需要确定树中结点的构成。由于哈夫曼树的构建是从叶子结点开始,不断地构建新的父结点,直至树根,所以结点中应包含指向父结点的指针。但是在使用哈夫曼树时是从树根开始,根据需求遍历树中的结点,因此每个结点需要有指向其左孩子和右孩子的指针。

哈弗曼树中的查找算法
  构建哈夫曼树时,需要每次根据各个结点的权重值,筛选出其中值最小的两个结点,然后构建二叉树。
  查找权重值最小的两个结点的思想是:从树组起始位置开始,首先找到两个无父结点的结点(说明还未使用其构建成树),然后和后续无父结点的结点依次做比较,有两种情况需要考虑:

  • 如果比两个结点中较小的那个还小,就保留这个结点,删除原来较大的结点
  • 如果介于两个结点权重值之间,替换原来较大的结点

哈夫曼编码
  哈夫曼编码就是在哈夫曼树的基础上构建的,这种编码方式最大的优点就是用最少的字符包含最多的信息内容。根据发送信息的内容,通过统计文本中相同字符的个数作为每个字符的权值,建立哈夫曼树。对于树中的每一个子树,统一规定其左孩子标记为 0 ,右孩子标记为 1 。这样,用到哪个字符时,从哈夫曼树的根结点开始,依次写出经过结点的标记,最终得到的就是该结点的哈夫曼编码。文本中字符出现的次数越多,在哈夫曼树中的体现就是越接近树根。编码的长度越短。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值