Skiplist,B,B+tree,LSM tree

SkipList

理解(如何实现):

即使是有序的链表,查找的时间复杂度也是O(n),但链表不能随机访问所以不适用于二分查找,使得查找的时间复杂度为O(logn)。

于是将有序链表和二分查找结合得到了SkipList,类似于二叉搜索树,这是一种以空间换时间的算法,通过在每个节点中增加向前的指针,从而提升查找效率(如下图所示)。

一开始如a所示,是一个有序的链表,时间复杂度O(n)。随后建立了e所示的SkipList,从最顶层往下查找,可以跳过许多元素,减少查找次数。

UNugu8.png

​ (出自论文:Skip Lists: A Probabilistic Alternative to Balanced Trees)

SkipList的特征:

1.一个跳表应该由几个层(level)组成

2.跳表的第一层应该包含所有元素

3.每一层都是一个有序的链表

4.如果元素X出现在第i层,则所有比i小的层都包含X

5.每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素

UNugu8.png

SkipList基本操作

查找

UtbhYq.jpg

​ (图来源于网络)

在跳跃表中查找一个元素x,按照如下几个步骤进行:

1.从最上层的链(Sh)的开头开始

2.假设当前位置为p,它向右指向的节点为q(p与q不一定相邻),且q的值为y。将y与x作比较
(1) x=y 输出查询成功及相关信息
(2) x>y 从p向右移动到q的位置
(3) x<y 从p向下移动一格

3.如果当前位置在最底层的链中,且还要往下移动的话,则输出查询失败

跳表查找任意数据的时间复杂度为O(logn)

插入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-niOdQGLY-1604373549007)(https://s1.ax1x.com/2020/07/14/UNP6PS.png)]

​ 步骤一:确定插入位置(来源于网络)

​ 步骤二:确定插入的高度(来源于网络)

采用“抛硬币算法”插入元素X,分为两步:

1.先利用SkipList的查找功能(O(logn)),,找到比待插入元素X小的最大的数Y,由于链表递增排列,所以待插入元素X必然在Y后面

2.确定插入的高度,定义一个随机决策模块,内容如下:

a: 产生一个0到1的随机数r   r ← random() 

​ b:如果r小于1/2,则执行方案A, if r<1/2 then do A

​ 否则,执行方案B else do B

初始列高为1,如果执行A则将高度加1,继续反复执行随机决策模块,直到第一次执行B,结束决策,并在SkipList中插入高度为i的列

跳表插入任意数据的时间复杂度为O(logn)

删除

在删除节点时,如果这个节点在索引中出现了,那么索引中的也要删除。同时,如果产生了空链,多余的空链也要删除。删除操作需要找到删除结点的前驱结点,然后再通过指针操作完成删除,所以需要先获得前驱节点(O(logn)),所以删除操作的时间复杂度也为O(logn)

SkipList 与AVL对比

AVL可以完成SkipList所能完成的所有工作,但实现困难,AVL为了保证平衡树的平衡因子只能为-1,0,1,所以在插入删除时需要不停地调整各节点的位置关系,使之重新达到平衡(LL,RR,LR,RL平衡旋转),因此除了作为数据结构中编程任务外,很少使用AVL。SkipList使用概率平衡而不是严格意义上的平衡,可在大多数应用中替代AVL。SkipList不需要如此复杂的操作,它只需要在链表的基础上增加或者删除部分前向指针即可。所以插入删除算法比AVL算法简单很多,速度也快很多,同时不需要存储平衡信息。

B树

从算法逻辑上讲,二叉查找树的查找速度和比较次数都是最小的。之所以会出现B树,是出于对磁盘I/O的考虑,数据库索引都是存储在磁盘上,当数据量比较大时索引大小可能几个G甚至更多,在利用索引查询时不可能全部加载到内存,而加载次数与树的高度有关,树高度越大,磁盘I/O次数越多,所以为了减少磁盘I/O次数,把原来“瘦高”的树变成“矮胖”,就得到了B树。B树是针对外存储器设计的多路查找结构,一次外存储器I/O操作的时间代价要比主存储器读写高出上万倍,所以B树可以显著提高查找性能。

特征

B树又称多路平衡查找树,B树中所有结点的孩子结点数的最大值称为B树的阶,通常用m表示,一颗m阶B树或为空树,或为满足如下特性的m叉树:

1.树中每个结点至多有m棵子树,即至多含有m-1个关键字(很好理解,若关键字>m-1,那么含有的子树就>m)

2.若根节点不是终端结点,则至少有两棵子树(如果只有一棵,就会导致不平衡)

3.除根节点和叶节点外,其他每个节点至少有ceil(m/2)个孩子(ceil()为向上取整)(即至少含有ceil(m/2)-1个关键字)

4.所有叶子节点都位于同一层;

5.每个非叶节点包括n个关键字信息,其中ceil(m/2)-1<=n<=m-1

6.所有非叶结点的结构如下:

UdMBrt.png

​ 其中Ki(i=1,2,…,n)为结点关键字,满足K1<K2<…<Kn;Pi(i=0,1,…,n)为指向子树根节点的指针,且指针Pi-1所指子树中所有结点的关键字均小于Ki,Pi所指子树中所有结点的关键字均大于Ki,n为结点中关键字个数

对于第3点,之所以每个节点孩子树M的取值范围为ceil(m/2)<=M<=m且m>=2,是因为如果存在节点孩子树小于ceil(m/2),也可以通过向邻近兄弟节点“借”一个关键数转化为他俩的新父节点,或者通过合并来满足M的取值要求。此种平衡限制是为了最大限度降低层高,提高搜索效率.

B树基本操作

查找

B树上查找与二叉树查找相似,只是每个结点是多个关键字的有序表,每个结点所做的不是两路分支决定,而是根据该结点子树所做的多路分支决定。B树查找包含两个基本操作:①在B树中找结点②在结点内找关键字。前一个操作在磁盘上进行,第二个查找操作是在内存中进行的,即在找到目标结点后,先将结点中的信息读入内存,然后再采用顺序查找法或者折半查找法查找等于K的关键字。若找到就查找成功,否则按照对应的指针信息到所指的子树中继续查找。当查找到叶结点时(对应指针为空指针)则说明没有对应关键字,查找失败。查找的时间复杂度为O(logn)

示例:如要查找元素47

第一步:先查找第一层的第一个结点,发现关键字大于42,则在这个结点上查找失败,将根据42之后的指针到结点的第三个子树继续查找

UdYdBj.png

第二步:查找该结点,发现关键字小于48,则在这个结点上查找失败,将根据48之前的指针到结点的第一个子树继续查找

UdYauQ.png

第三步:查找该结点,发现关键字47,则在这个结点上查找成功

UdYNjg.png

插入

与二叉查找树的插入相比,B树的插入复杂得多,因为B树找到插入位置后并不能简单地将其添加到终端结点中去,会导致整棵树不满足B树的定义要求,其插入过程如下:

(1)定位:利用B树的查找算法,找出插入该关键字的最底层中某个非叶结点(B树中的插入关键字一定是插入在最底层中的某个非叶结点内)

(2)插入:当插入后的结点关键字个数小于m,则可直接插入,当插入后的结点关键字个数大于等于m时,就必须要进行分裂

​ **分裂方法:**取一个新结点,将插入K后的原结点从中间位置将其中关键字分 为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新的结点中,中间位置ceil(m/2)的结点插入到原结点的父结点中,若父结点的关键字个数也超过上限,继续进行分裂操作,直至整个过程传到根结点为止,这会导致B树高数加1。

插入的时间复杂度为O(logn)

删除

B树中的删除操作与插入类似,要使得删除后的结点中的关键字个数≥ceil(m/2)-1,因此会涉及结点的“合并”问题。大致可分为两种情况,所删除的关键字K不在最底层非叶结点和在最底层非叶结点。删除的时间复杂度也为O(logn)

1.所删除的关键字K不在最底层非叶结点

(1)如果小于K的子树中关键字个数>ceil(m/2)-1,则找出K的前驱值K‘,用K’来取代K,再递归的删除K即可

(2)如果大于K的子树中关键字个数>ceil(m/2)-1,则找出K的后继值K‘,用K’来取代K,再递归的删除K即可

(3)若前后两个子树中关键字个数均为ceil(m/2)-1,则直接将两个子结点合并,然后删除K即可

2.所删除的关键字K在最底层非叶结点

(1)若被删除关键字所在结点的关键字个数>ceil(m/2)-1,表明删除K后仍满足B树定义,则直接删除该关键字

(2)兄弟够借:若被删除关键字所在结点的关键字个数=ceil(m/2)-1,且与此结点相邻的左(右)兄弟结点的关键字个数≥ceil(m/2),需要调整该结点、左(右)兄弟结点以及双亲节点,以达到新的平衡

(3)兄弟不够借:若被删除关键字所在结点的关键字个数=ceil(m/2)-1,且与此结点相邻的左(右)兄弟结点的关键字个数=ceil(m/2)-1,则将关键字删除后与左(右)兄弟结及双亲结点中的关键字进行合并

实例(模型取自网络)删除H,T,R,E

UwmIJJ.png

第一步:删除元素H,首先查找H,H在一个叶子结点中,且该叶子结点元素数目3大于最小元素数目ceil(m/2)-1=2,符合2.1的情况,只需要移动K至原来H的位置,移动L至K的位置(也就是结点中删除元素后面的元素向前移动)

Uwm5i4.png

第二步:删除T,T没有在叶子结点中,在中间结点,符合1.2的情况,将W上移到T的位置,然后将原包含W的孩子结点中的W进行删除

UwmhoF.png

第三步:删除R,R在叶子结点中,但是该结点中元素数目为2,删除导致只有1个元素,右相邻兄弟结点中比较丰满(3个元素大于2),所以先向父节点借一个元素W下移到该叶子结点中,代替原来S的位置,S前移;然后X在相邻右兄弟结点中上移到父结点中,最后在相邻右兄弟结点中删除X,后面元素前移。符合2.2的情况

UwmfdU.png

第四步:删除E,删除后会导致很多问题。因为E所在的结点数目为2,相邻的兄弟结点也是同样的情况,所以需要该节点与某相邻兄弟结点进行合并操作;首先移动父结点中的元素(该元素在两个需要合并的两个结点元素之间)下移到其子结点中,然后将这两个结点进行合并成一个结点。随后发现父节点只包含一个元素G,没达标,且相邻兄弟也刚好为2,没有办法去借一个元素,只能与兄弟结点进行合并成一个结点,而根结点中的唯一元素M下移到子结点,这样,树的高度减少一层。符号2.3的情况

UwmWZT.png

B+树

特征

B+树的应数据库所需出现的一种B树的变形树。

一棵m阶的B+树的特征为:

(1)每个分支结点最多有m棵子树

(2)非叶根结点至少有两棵子树,其他每个分支结点至少有ceil(m/2)棵子树

(3)结点的子树个数与关键字个数相等

(4)所有叶结点包含全部关键字及指向相应记录的指针,而且叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来

(5)所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针

与B树的差异

(1)在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的结点含有(n+1)棵子树

(2)在B+树中,每个结点(非根内部结点)关键字个数n的范围是ceil(m/2)≤n≤m(根结点:1≤n≤m),在B树中,每个结点(非根内部结点)关键字个数n的范围是ceil(m/2)-1≤n≤m-1(根结点:1≤n≤m-1)

(3)在B+树中,叶结点包含信息,所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址

(4)在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中;而在B树中,叶结点包含的关键字和其他结点包含的关键字是不重复的。在B+树中有两个头指针,一个指向根节点,一个指向关键字最小的叶结点。

B树与B+树优劣对比

B+ 树优势:

(1)由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率,使得查询的IO次数更少。

(2)B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。

(3)B树的查找性能不稳定(最好情况是只查根结点,最坏情况是查到叶子结点),而B+树每次查找都是稳定的

B树优势:

由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。

下面是B 树和B+树的区别图:

Udn0K0.png****

为什么说B+树比B 树更适合做磁盘数据索引?

(1) B+树的磁盘读写代价更低

B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。

(2) B+树的查询效率更加稳定

由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

B+树基本操作

查找

与B树的查找相似,只是在查找过程中,如果非叶结点上的关键字值等于给定值时并不终止,而是继续向下查找直到叶结点上的该关键字为止,因为B+树的所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址,也无法得到记录的详情信息。所以在B+树中查找,无论查找成功与否,每次查到都是一条从根结点到叶结点的路径。查找时间复杂度为O(logn)

插入

(1) 若被插入关键字所在的结点,其含有关键字数目小于阶数 M,则直接插入结束

(2) 若被插入关键字所在的结点,其含有关键字数目等于阶数 M,则需要将该结点分裂为两个结点,一个结点包含ceil(m/2),另一个结点包含ceil(m/2)。同时,将ceil(m/2)的关键字上移至其双亲结点。假设其双亲结点中包含的关键字个数小于 M,则插入操作完成

(3)在(2)的情况中,如果上移操作导致其双亲结点中关键字个数大于 M,则应继续分裂其双亲结点

(4)如果插入的关键字比当前结点中的最大值还大,破坏了B+树中从根结点到当前结点的所有索引值,此时需要及时修正后,再做其他操作

插入的时间复杂度为O(logn)

删除

(1)删除该关键字,如果不破坏 B+树本身的性质,直接完成操作

(2)如果删除操作导致其该结点中最大(或最小)值改变,则应相应改动其父结点中的索引值

(3)在删除关键字后,如果导致其结点中关键字个数不足,有两种方法:一种是向兄弟结点去借,另外一种是同兄弟结点合并。(注意这两种方式有时需要更改其父结点中的索引值,直至根结点)

删除的时间复杂度为O(logn)

B树与B+树应用场景

B树大量应用在数据库和文件系统当中,如mongoDB数据库使用,单次查询平均快于Mysql

而mysql使用B+树作为索引,比如InnoDB索引和MyISAM索引

LSM树

UsuYY6.png

​ (取自网络)

绿色的部分表示硬盘顺序读取的最大速度,而红色表示随机读取时的速度,可以看出顺序读取与随机读取的速度差了几个数量级

传统关系型数据库使用b树或一些变体作为存储结构,能高效进行查找。但保存在磁盘中时它也有一个明显的缺陷,那就是逻辑上相离很近但物理却可能相隔很远,这就可能造成大量的磁盘随机读写。随机读写比顺序读写慢很多,为了提升IO性能,我们需要一种能将随机操作变为顺序操作的机制,于是便有了LSM树。LSM树能让我们进行顺序写磁盘,从而大幅提升写操作,作为代价的是牺牲了一些读性能。

应用场景

LSM树在 NoSQL 系统里非常常见,基本已经成为必选方案。同时也被用在各种键值数据库中,如 LevelDB,RocksDB,还有分布式行式存储数据库 Cassandra 也用了 LSM树的存储架构

LSM树 VS B+树

与B+树相比,能显著地减少硬盘磁盘臂的开销,并能在较长的时间提供对文件的高速插入(删除)。然而LSM树在某些情况下,特别是在查询需要快速响应时性能不佳。通常LSM树适用于索引插入比检索更频繁的应用系统。

LSM基本操作

Compaction

UsQD0J.png

LSM分为三部分,第一种是内存中的两个 memtable,一个是正常的接收写入请求的 memtable,一个是不可修改的immutable memtable。另外一部分是磁盘上的 SStable,SStable 一共有许多层。下一层的总大小限制是上一层的 K倍。

当数据不断从 Immutable Memtable 序列化到磁盘上的 SSTable 文件中时,SSTable 文件的数量就不断增加,而且其中可能有很多更新和删除操作并不立即对文件进行操作,而只是存储一个操作记录,这就造成了整个 LSM Tree 中可能有大量相同 key 值的数据,占据了磁盘空间。

为了节省磁盘空间占用,控制 SSTable 文件数量,需要将多个 SSTable 文件进行合并,生成一个新的 SSTable 文件。比如说有 5 个 10 行的 SSTable 文件要合并成 1 个 50 行的 SSTable 文件,但是其中可能有 key 值重复的数据,我们只需要保留其中最新的一条即可,这个时候新生成的 SSTable 可能只有 40 行记录。

通常在使用过程中我们采用分级合并的方法,其特点如下:

  1. 每一层都包含大量 SSTable 文件,key 值范围不重复,这样查询操作只需要查询这一层的一个文件即可。(第一层比较特殊,key 值可能落在多个文件中,并不适用于此特性)
  2. 当一层的文件达到指定数量后,其中的一个文件会被合并进入上一层的文件中。

查找

当我们需要查找一个元素的时候,我们会先查找Memtable,它就在内存当中,不需要额外读取文件,如果Memtable当中没有找到,我们再一个一个查找SSTable,由于SSTable当中的数据也是顺序存储的,所以我们可以使用二分查找,整个查找的速度也会非常快

增删改

首先将写入操作加到写前日志中,接下来把数据写到 memtable中,当 memtable 满了,就将这个 memtable 切换为不可更改的 immutable memtable,并新开一个 memtable 接收新的写入请求。而这个 immutable memtable 就可以刷磁盘了。这里刷磁盘是直接刷成 L0 层的 SSTable 文件,并不直接跟 L0 层的文件合并。每一层的所有文件总大小是有限制的,每下一层大K倍。一旦某一层的总大小超过阈值了,就选择一个文件和下一层的文件合并。

修改和删除也一样,如果需要修改的元素刚好在Memtable当中,没什么好说的我们直接进行修改。那如果不在Memtable当中,如果我们要先查找到再去修改免不了需要进行磁盘读写,这会消耗大量资源。所以我们还是在Memtable当中进行操作,我们会插入这个元素,标记成修改或者是删除

由于我们不断地落盘会导致SSTable的数量增加,SSTable的数量增加会影响我们查找的效率,所以我们不能放任它无限增加。再加上我们还存储了许多修改和删除的信息,我们需要把这些信息落实。为了达成这点,我们需要定期将所有的SSTable合并,在合并的过程当中我们完成数据的删除以及修改工作。换句话说,之前的删除、修改操作只是被记录了下来,直到compaction的时候才真正执行。

LSM树的不足

LSM树存在读写放大,读写放大 = 磁盘上实际读写的数据量 / 用户需要的数据量。

写放大:LSM树每次拿上一层的所有文件和下一层合并,下一层大小是上一层的K 倍,这样单次合并的写放大就是K倍

读放大:(如上图所示的模型)为了查询一个 1KB 的数据。最坏需要读 L0 层的8个文件,再读 L1 到 L6 的每一个文件,一共 14 个文件。而每一个文件内部需要读 16KB 的索引,4KB的布隆过滤器,4KB的数据块。一共 24*14/1=336倍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值