树型结构小结

前言

早上坐公交的时候刷到一篇朋友圈,名为:为什么Mysql数据库要使用B+树存储索引?看了下受益匪浅,不禁感慨大学学的数据结构知识全还回去了,因此准备回顾总结复习下树的知识,不过可能不全只会记一小部分自己需要的把,当然最后也会给出上面这个问题的答案。

 

树的基本概念

树和图一样都是非线性结构,树是n个节点的有限集合,当n=0时,称该树为空树,非空树有以下两个特征:

1、有且仅有一个称为根的结点。
2、如果n>1, 除根结点以外其它结点可以分为m(m>0)个不相交的集合T1,T2,T3,T4,......,Tm,其中每一个集合都是一棵树。树T1, T2, T3,......,Tm称为这棵对的子树。

通俗一点说,树存在的意义就是为了方便查找,下面看下树的相关术语:

1.结点的度(Degree):结点的子树个数。(例如上面的图中A有三个节点,那么A的度为3)

2.树的度:树的所有结点中最大的度数。 (上图树的度为3)

3.叶结点( Leaf): 度为0的结点。(如上图的K,L)

4.父结点( Parent):有子树的结点是其子树的根结点的父结点。 (如上图中E是K的父节点)

5.子结点( Child):若A结点是B结点的父结点,则称B结点是A结点的子结点;子结点也称孩子结点。(如上图中K是E的子节点)

6.兄弟结点( Sibling):具有同一父结点的各结点彼此是兄弟结点。 (如上图中K和L互为兄弟节点)

7.祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点。 

8.子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙。 

9.结点的层次( Level):规定根结点在1层,根的子节点为第2层,以此类推。(如上图中E为第3层) 

10.树的深度( Depth) :树中所有结点中的最大层次是这棵树的深度。 (如上图中深度为4)
 

二叉树

二叉树是每个节点最多有两个子树的树结构,通常子树被称作左子树与右子树,左右子树可为空,二叉树常被用于实现二叉查找树和二叉堆。当二叉树只有一连串的左子树或者一连串的右子树时,此时称为斜二叉树,这时就几乎变成了链表结构,是完全用不到的废树,如下图这种,另外二叉树的形态之分无非就是节点是否在同一层是否填满之类的,没什么必要。

1、二叉树的遍历

主要分为三种方式即前中后序遍历,对于我这种小菜鸡来说以前都是靠记口诀来应付所有二叉树的遍历,特别简单= =,待遍历的二叉树图如下:

a、前序遍历

口诀为:根左右,即先遍历根节点,然后是左节点,最后是右节点。记忆的时候先把根节点拎出来,左右顺序一直不变,前序则把根节点放在第一位,中序则放在第二位,为左根右,后续则把根节点放在最后一位即左右根,记忆的时候你就假设只有根节点的位置在不停变,而左右节点的顺序排位其实不变。

先贴结果:ABCDEFGHK

然后讲下实操,即这个口诀具体该怎么用。针对上面这个图我们在遍历的时候永远以一个三角形来看,即我们先看这个三角形:

根据口诀,先遍历根节点A,然后遍历左节点B,这时发现B存在子节点那么我们不管E继续往下看B下面的三角形:

我们发现它并没有左节点则跳过直接遍历右节点即C,C又存在子节点则继续遍历直到再也找不到"三角形"。这时在根节点左边这一圈下来我们已经得到结果为ABCD,对应于最开始的三角形我们已经遍历完左节点了,此时该遍历右节点E,然后依次往下,规则与上面的都相同,得到结果为FGHK,最后拼接即为ABCDEFGHK。

b、中序遍历

口诀为:左根右,即先遍历左节点,然后是根节点,最后是右节点,这里与前序遍历规则上有个不一样的地方就是只要左节点下面还存在左节点,此时该左节点其实已经作为另外一个三角形的根节点了,则按照口诀得先遍历其下面的左节点,直到再也不存在左节点。

先贴结果:BDCAEHGKF

讲下实操:第一个三角形下先走左节点B,发现以B作为根节点的三角形并没有左节点,则直接开始遍历C的子树,根据左根右的规则先遍历D(此时注意如果D还存在子节点则先不算D,得先遍历D的子树),再是C。然后看下根节点的右侧,按照规则先遍历左节点,ei发现没有,则记E,开始遍历E的右节点,发现F节点下存在左节点G,G下又存在左节点H,此时H下面再也无左节点,那么最先被遍历的则为H,然后一步步按照规则往上即可。

c、后序遍历

口诀为:左右根,即先遍历左节点,然后是右节点,最后是根节点,以根节点为准的那个最大三角形为例,即先把左节点下的所有子孙节点全部遍历完,再把右节点下的所有子孙节点全部遍历完,最后才是根节点。

结果为:DCBHKGFEA,这里就不推了,大同小异。

 

完全二叉树

对于一个树高为h的二叉树,如果其第0层至第h-1层的节点都满;如果最下面一层节点不满,则所有的节点在左边的连续排列,空位都在右边。这样的二叉树就是一棵完全二叉树。如下图:

完全二叉树最重要的性质:如果n个节点的完全二叉树的节点按照层次并按从左到右的顺序从0开始编号,对于任意个节点都有:

  • 序号为0的节点是根
  • 对于i>0,其父节点的编号为(i-1)/2
  • 2·i+1<n,其左子节点的序号为2·i+1,否则没有左子节点。
  • 2·i+2<n,其右子节点的序号为2·i+2,否则没有右子节点。

完全二叉树的该性质用处:(堆排序中便用到了该思想)

  • 使二叉树可以方便的存入表或者数组,直接根绝元素下标就可以找到一个节点的子节点或者父节点(也就是可以完全确定二叉树的结构),无须用额外的形式记录树结构信息。
  • 使其可以方便地存入一系列连续位置,一般二叉树不能方便地映射到线性结构,完全二叉树到线性结构有定义非常自然的双向映射,可以方便地从其线性结构恢复完全二叉树。

 

二叉排序树

又名二叉查找树或者二叉搜索树,顾名思义用于查找所用,其也是二叉树下的一种特殊的树。满足以下三个条件的即为二叉排序树:

1、若它的左子树不空,则左子树上所有节点的值均小于根节点的值
2、若它的右子树不空,则右子树上所有节点的值均大于根节点的值
3、它的左右子树也都是二叉排序树

这里我们应用上述的三条性质来演示下二叉排序树的创建过程,以此加深对二叉排序树的印象:

假设我们要将集合 {62, 88, 58, 47, 35, 73, 51, 99, 37, 93} 中的元素存储到二叉排序树中(反过来假设这些数据已经存储在二叉排序树中,那么输出该树时得到的结果即为上面的有序序列):

1、初始化状态下我们的根节点为空,每一次插入无非就是对二叉排序树的先搜索再插入的反复循环过程

2、取出62元素,先搜索树发现并没有元素,则直接将62插入到根节点

3、取出88元素,开始搜索发现88>62,根据定义则需插入到右节点

4、取出58元素,开始搜索发现58<62,插入到左节点

5、取出47元素,发现47<58<62,则插入到58的左节点

... 如此循环判断即可,最终二叉排序树如下图所示(图自网络):

当插入有序序列时,即形如 {1,2,3,4,5} ,二叉排序树即退化为链表,此时无任何意义。

由于树的查询效率与树的高度有关,所以当二叉排序树是平衡时,其查找效率为O(Log(2^n)),类似于折半查找当最坏情况下即为上述所说的链表格式,变为顺序查找,效率为O(n),所以总的而言二叉排序树的查找效率在O(Log(2^n)) ~ O(n) 之间。

Ps:这里有个很坑的地方就是,计算机科学里的lgn就是数学里的以2为底的logn

 

二叉平衡树

主要为了解决二叉树转变为链表的窘境,当变为链表时查找效率极其低下,所以需要对数进行平衡,在插入的时候同时调整这棵树(通过旋转,旋转定义太多这边只要知道一切操作都是为了使树保持平衡即可),让它的节点尽可能均匀分布,二叉平衡树归定每棵树的左右子树的高度相关不能超过1。从而保证查找效率不会来到O(n)。

 

红黑树

是一种自平衡二叉查找树,也是平衡树的一种,它的复杂的定义和规则也都是为了保持树的平衡,在java中用它作为数据结构底层的有TreeSet、TreeMap,在jdk1.8之后,HashMap的底层也加了红黑树的结构。红黑树的时间复杂度也为log(2^n),红黑树有如下5种特性:
1、每个节点或者是黑色,或者是红色。
2、根节点是黑色。
3、每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
4、如果一个节点是红色的,则它的子节点必须是黑色的。
5、从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

在对红黑树进行删除、插入操作时,为了使之仍然满足红黑树的特性需通过旋转,与上述平衡树类似,只是规则可能会不太一样,这块我其实也没去好好研究,我感觉我也记不下来,暂时就了解下红黑树为什么这么设计,被用在哪里对我而言已经足够了。虽然很片面,但是学习一门技术,理解它背后的设计理念、原理和解决问题的方法,远远比技术本身是什么更重要。

1、为什么费劲心思要保证树的平衡?

因为树的查找性能取决于树的高度,让树尽可能的平衡,就是为了降低树的高度。

 

B树

B树是一种平衡多路搜索树,与红黑树的最大区别在于它的每个节点可以同时拥有多于两个孩子节点。M路的B树最多能拥有的M个孩子节点。B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。B树常用作文件系统和数据库的索引,不过B+树更适合,在实际应用中基本也是B+树。下面是一个4阶的B树,即3路:

1、为什么文件系统和数据库的索引要用B树而不用红黑树或者有序数组呢?

文件系统和数据库的索引都是存在硬盘上的,并且如果数据量大的话,不一定能一次性加载到内存中。当一棵树都无法一次性加载进内存时,这时B树的多路存储威力就出来了,可以每次加载B树的一个节点,然后一步步往下找。假设内存一次性只能加载2个数,一个很长的有序数组是无法一次性加载进内存,我们把它组织为一颗3路的B树,这样每个节点最多有2个数,查找的时候,每次载入一个节点进内存就行。相反针对红黑树,它的h一般比B树更大,因此效率上相比会差一点。

总结:如果在内存中,红黑树比B树效率更高,但是涉及到磁盘操作,B树就更优了。

2、为啥要设计成多路?

路数越多,树的高度越低,效率更高,一旦被设计成无限多路则退化成有序数组了,参考上面的应用场景无限多路是很不可取的。

 

B+树

B+树是对B树的一种变形树,与B树的主要区别为:

1、非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。

2、树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。

如下图是一个B+树:

B+树常用作mysql的索引,那么为什么B+树比B树更适合呢?

这主要是和业务场景相关的,比如在数据库中 Select 数据,不一定只选一条,很多时候会选多条,比如按照 ID 排序后选 10 条。如果是多条的话,B 树需要做局部的中序遍历,可能要跨层访问。而 B+ 树由于所有数据都在叶子结点,不用跨层,同时由于有链表结构,只需要找到首尾,通过链表就能把所有数据取出来了。

 

哈希索引

为了开头那个问题作下铺垫,哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。下图是哈希索引的实例图:

从上图可以发现,哈希索引与B+树索引的区别只要如下:

1、如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;当然了,这个前提是,键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;

2、从示意图中也能看到,如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索;

3、同理,哈希索引也没办法利用索引完成排序,以及like ‘xxx%’ 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询);

4、哈希索引也不支持多列联合索引的最左匹配规则;

5、B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在所谓的哈希碰撞问题。

 

回到开始

为什么Mysql数据库要使用B+树存储索引?

首先在mysql中B+树索引一般写作BTREE,然后mysql中也有哈希索引的存在,一般就用作等值查询,即如果只选一个数据,那确实是 hash 更快。但是数据库中经常会选择多条,这时候由于 B+ 树索引有序,并且又有链表相连,它的查询效率比 Hash 就快很多了。而且数据库中的索引一般是在磁盘上,数据量大的情况可能无法一次装入内存,B+ 树的设计可以允许数据分批加载,同时树的高度较低,提高查找效率。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清茶_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值