数据结构-搜索树


搜索树

本文不会讲太多实用操作,主要是梳理一下几种常用搜索树的motivation

二叉搜索树

参考美团技术团队红黑树剖析及Java实现
二叉搜索树的插入和删除:

  • 插入:
    插入最后一定作为叶子节点挂在原树下,查找时使用parent指向当前节点的父节点,最后让parent的left/right指向该插入节点。

  • 删除:
    查找;如果要删除的节点是叶子节点,则直接删除;如果要删除的节点不是叶子节点,则需找到该节点的中序前驱(或后继),与中序前驱(或后继)交换值,然后删除中序前驱(或后继),这是因为非叶子节点的中序(前驱)或后继一定是一个叶子节点(可以举例验证下,叶子-非叶子-叶子-非叶子-叶子……的规律)。

平衡二叉树之AVL树

左旋右旋操作

通过左旋/右旋来保持搜索树的平衡。

插入/删除

从插入/删除的节点从下往上找到第一个不满足{-1,0,1}的节点进行左旋/右旋

B树

为什么要用B树

平衡二叉树log2(N)的性能已经足够好了吗?在内存中似乎是如此,但对数据库来说就不一样了,简单推算了下:

  1. 首先对数据库规模有大概的认识:
    假如数据库中某个表有十亿(10^9,约为1G个)条数据,我们对某个int型字段建立索引,那么仅仅这个表的这个字段的索引就占用4GB的空间,这就意味着,即使是索引,也不可能将整棵查找树加载到内存中,这还只是一个表的一个字段的索引(可能一般用户也没有十亿条数据,但几十个表··X几个不止int型的索引下来,占用空间也不小了),所以只能是存储在硬盘里比较合适了。
    (实际上,打开我笔记本里的mysql文件夹,索引文件只有十几兆,毫无工作经验的我只能如是猜测。
  2. 磁盘I/O预读-深入理解数据库索引采用B树和B+树的原因
    每一次IO时,磁盘不仅仅把当前地址的数据加载到内存,同时也把相邻数据也加载到内存缓冲区中,这是因为当访问一个地址数据的时候,与其相邻的数据很快也会被访问到。每次磁盘IO读取的数据我们称之为一页(page),大小与操作系统有关,通常为4K或者8K。
  3. 如果用二叉树(平衡的)存储
    如果用二叉树(平衡的)存储索引树,那么每个节点包含的数据应该有(我推测):
    • key,字段值
    • value,该条数据在硬盘中的地址,不过对于树来说,value没什么用
    • left, 左子索引节点在硬盘中的地址
    • right, 右子索引节点在硬盘中的地址
      那么,每次对比都要将索引节点的内容从硬盘中读取出来,而这一次I/O读取到的有用数据只有这一个关键字(4KB的页空间有点浪费)。这样就需要进行平均log2(N)次的I/O才能找到目标。
  4. B树存储
    承上,既然如此,那为什么每次不多读几个关键字到内存中然后进行比较呢?B树就是这么做的,大于2阶的B树,相比于平衡的二叉搜索树,可以降低树的高度,进而减少磁盘I/O次数,每次I/O可以读取多个关键字到内存中进行对比,具体查询过程见深入理解数据库索引采用B树和B+树的原因

B树的定义

B树又叫平衡多路搜索树,它不是多叉树,它的查找还是类似二叉的方式。

  1. 一棵m阶的B树特性如下:
    • 树中的所有节点最多有m个孩子节点(节点的关键字个数k<=m-1);
    • 根节点(如果树中不止有根节点的话)至少有两个孩子节点(k>=1,很好理解);
    • 树枝节点(除根节点和叶子节点外的节点)至少有ceil(m/2)个孩子节点(相应的,ceil(m/2)-1<k,且k<=m-1);
    • 所有叶子节点都出现在同一层(这就意味着是一棵平衡的搜索树),并且不包含任何关键字信息(这里的叶子节点不是我们常说的那个叶子节点,他把我们说的那个叶子节点成为终端节点,终端节点有m-1个关键字,终端节点下的null是叶子节点)。
    • 节点中的关键字按从小到大排列,节点中k个关键字是k+1个孩子节点中关键字的值域划分。
  2. 插入
    如同二叉树一样,先查找,而且新插入的关键字一定是插入到了终端节点里。
    在插入key后,若导致原结点关键字数超过上限,则从中间位置(m/2向上取整)将其中的关键字分为两个部分,左部分包含的关键字放在原结点中,右部分包含的关键字放在新节点中,中间位置(m/2向上取整)的结点插入原结点的父节点。若此时导致其父节点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根节点为止,进而导致B树高度增1
  3. 删除
    • 对于非终端节点里关键字的删除,第一步跟二叉树一样,将其与前驱/后继节点中的前驱/后继关键字(应该说是内容,像数据库就还包括该条数据的硬盘地址)交换,将其转换成了叶子节点上关键字的删除。
    • 终端节点但接下来不同的是,二叉树的叶子节点可以直接置为null,但B树还要考虑约束:树枝节点的关键字个数k需满足ceil(m/2)-1<k<=m-1。主要是通过①如果兄弟节点够借,则向相邻兄弟节点借关键字来替补;②如果兄弟节点不够借,则和父关键字、兄弟节点一起合并的方式来满足。

插入和删除的过程见B树——插入和删除
以及B树详解

B+树

B树虽然通过降低树的高度并配合一次I/O取多个关键字到内存中比较的方式减少了单条数据查询的时间,但其范围查询(中序遍历)的复杂度还是很高。

B+树的特征:

  1. 非叶子节点只保存key、不保存数据(或数据的地址),这些key只是用来索引的,并且k个key对应k个孩子节点;
  2. 所有的叶子节点包含了全部的key和数据(或数据的地址),节点内部按key从小到大排列,节点之间从小到大顺序链接;
  3. 所有的非叶子节点的key同时存在于叶子节点。

B+树的优势:

  1. 每个节点可以存储更多的key,树更加矮胖些,I/O的次数更少;
  2. 所有查询都要查找到处于同一层的叶子节点,查询性能稳定;
  3. 所有叶子节点形成有序链表,便于范围查询。

b+树图文详解

红黑树

红黑树是一种含有红黑结点并能自平衡的二叉搜索树,先了解下红黑树的定义:

  1. 每个结点是要么是红色或黑色
  2. 根结点必须是黑色
  3. 叶子结点(类似B树的叶子节点,也就是终端节点的子节点)是黑色
  4. 红色结点不能连续(也就是,红色结点的孩子和父亲都是黑色)
  5. 对于每个结点,从该点至叶子节点的任何路径都包含所相同个数的黑色结点

网上找了很多详解红黑树的文章,都是给出红黑树的性质和更新规则、然后根据规则演示各种情况下的插入和删除,很少能看到有对定义和规则来源进行解释的,搞不明白为什么这么定义、后面的内容总觉得学得不踏实。
直到看到红黑树详细图解B站红黑树历史讲解里提到的红黑树与B树的等价变换,才有点"原来是这么回事"的感觉。其实红黑树的那几条性质可以认为是如何把4阶B树(也称2-3-4树,一个节点最多有3个关键字)转变成红黑树,2-3-4树中的一个节点包含多个关键字,拆分时使用红色和黑色标记、并且拆分前的一个节点只能拆分出一个黑色的节点,这样,红黑树的定义就很好理解了,尤其是第4点和第5点。相应地,也可以把红黑树的红色节点和黑色节点组合成一个节点(只包含一个黑色节点),从而也能构建出相应的B树。

之所以又把B树演变成红黑树,我想,应该是看到了B树平衡的同时调整的也没AVL树那样频繁,至于为什么不直接用B树,也是由应用场景决定的:红黑树一般是将整棵树都放到内存中,这时就不需要像B树那样为了减小I/O次数而把多个关键字放一起,数据结构也不用那么复杂了。

插入删除操作见红黑树详细图解30张图带你彻底理解红黑树Data Structure Visualizations给出了过程动画(除了红黑树,还有其他数据结构和算法的)。

posted @ 2020-09-07 21:01  汉尼拔草  阅读(355)  评论(0编辑  收藏  举报