查找——数据的查找(二)

树的查找

在查找算法中,我们也可以把查找元素集合转化为树的问题结构,好处是:能够快速插入、删除并且查找也很快。
也就是说,树的查找能够解决,动态查找这个问题。

二叉排序树

二叉排序树提供了一种思路,但是并不完善。对于一棵普通的二叉排序树,它的插入、删除当然是比较简单啦,但是根据树的深度,查找的速度会有不同,但是一定比O(n)快(线性表就是深度最大的二叉排序树)。


  • 插入:看到图中的二叉树,当要插入一个元素的时候,都是在找到空位插入,因为一个结点只能有俩子树,但是呢插入的位置要正确,比双亲结点大时候插入左子树,小的时候右子树。这样才能通过不断的判断来找到关键字。
  • 删除则有些不同,删除一个结点,代替它的要是它的遍历时的前驱或者后继才行(就是要找到它左子树的最右边和右子树的最左边)

那么影响查找时间的自然是树的深度了,但是呢普通的二叉排序树没办法控制深度噢

平衡二叉树

平衡二叉树是用来解决查找效率问题的,因为一个满二叉树查找的效率是O(logn),而如果所有的结点都偏重一边的话会越来越趋近O(n),因此我们需要用一个算法,每插入一个新元素后就对数据的树形结构进行重新平衡,删除也是,每删除一次就维护一次平衡

平衡二叉树是将一棵二叉排序树变成一个平衡的二叉排序树这个过程其实是把一个树高度给降低的过程,是一定会降低,因为不平衡就是因为某个子树比另一个子树高,而平衡就是把它“折中”,那么一定会把高度给减少才能实现),这个问题的输入和输出也是显而易见了

实现这个问题的思路是:当左右子树的高度差超过一定值,就对树进行适当的左旋或右旋。
实现的方法:在所有的不平衡情况中,都是按照“寻找最小不平衡树”->“寻找所属的不平衡类别”->“根据2种类别(是单旋还是双旋)进行固定化程序的操作”。

——最小不平衡树,就是最底层的不平衡的结点为根的树(这一步使得有一个递归的过程,每一个最小不平衡树被平衡后就去找上一层的最小不平衡树,直到整棵树都是平衡的)
——所谓的不平衡的类别就是指平衡因子,如果最小不平衡树的结点是平衡因子2(左子树比右子树高2),而左孩子的平衡因子是-1(左孩子的右子树比左孩子的左子树高1,这里不可能是-2,因为如果是的话这就成了最小不平衡树了)的时候,一正一负就代表需要进行双旋,把正负统一需要一次旋转,然后平衡一次旋转。

维护平衡的方法

具体的方法是:

  1. 寻找最小不平衡树
    递归:在递归函数中,先执行递归语句,然后判断平衡因子的绝对值是否>2
  2. 寻找所属的不平衡类
    找到最小不平衡树的根结点的平衡因子,然后找到较高子树的根结点的平衡因子,判断两者是否符号统一
  3. 进行旋转操作
    如果符号不同一,则先进行最小不平衡树较高子树的旋转。之后进行最小不平衡子树的平衡旋转。如图2


    图2

旋转操作就分两种,一种左旋,一种右旋,但是一定要保持树的有序性!所谓左旋右旋只是形象化来表示,实际上是把最小不平衡树的根结点和它其中一个孩子结点顺序交换一下。

为了保证树的有序性(旋转过程的细节)(如图3):

  • 最小不平衡树的根结点和它其中一个孩子结点交换过程(旋转过程)中,两个结点整体来说最靠边的两个孩子指针不改变,靠内的两个指针交换,若指针指向自己,那么则指向两个结点中的另一个。
  • 最后指向最小平衡树的指针也要指向新的结点。


    图3

多路查找树

多路查找树是考虑到一个问题,
啥问题呢:之前我们一直说的数据结构,其实是在内存里数据的数据结构,平时我们的电脑是分成cpu、内存、硬盘等几部分的,数据放在硬盘里,在硬盘里的存储就没有那么多结构,因为不需要考虑到数据和数据之间的逻辑关系,只需要存起来就行,而我们要用数据的时候,比如说搜索引擎搜索相应关键字,这时候cpu要操作数据就必须把数据放在内存中,因为读取速度快,内存因为价格贵,所以远远比硬盘容量要小,这时内存中的数据要有逻辑关系了,就会出现数据结构这种东西,那么如果要存入内存操作的数据量特别大都溢出了,怎么办呢?
——拓展,把硬盘一部分拓展成内存,而实际物理层面上,就是交换的过程,把在硬盘“内存”上的数据分次换进来。

在硬盘上拓展内存的过程,让硬盘的一部分变变成了内存,但是整个逻辑内存在使用的时候也要从假内存往真内存里面转换,所以多路查找树就是提高假内存往真内存里面交换数据效率的一种数据结构

读取假内存的时间毕竟是读取硬盘会非常慢,因此需要读多少次硬盘很影响时间复杂度,如果要遍历上万个溢出的数据要读上万次假内存,和遍历n个溢出数据只需要读几次假内存。

我们之前谈的树都是一个结点只存一个数据元素,数据量大的时候,树的度或高度会非常大(意味着循环次数多)。对于这样一棵树,平常的真假内存交换的算法是,到了需要某个结点的时候才读取一次假内存把它调入真内存,所以树的度和高度会非常影响读取假内存的数量(每一次循环都需要读取)。如何提高时间效率?从两方面:

  • 改进算法:想办法从算法角度增加一次读取的数据数量,例如每次需要读取的就把所读取结点的兄弟一起存进来。
  • 改进数据结构(什么数据结构才能保留树的特性,又能增加一次读取的数据数量?):把每一个结点的所能容纳的数据元素个数增加。这样每次读取一个结点,可以有很多数据

2-3树、2-3-4树、B树

这里的2、3、4代表的是一个结点有多少个孩子,而非数据元素个数,但是数据元素个数是孩子数-1(因为孩子指针和数据元素插空存储的)。2-3树就是3阶查找树,同理2-3-4树是4阶查找树。

多路查找树的思想:不允许一个分支还没有完全插满的时候增加下一层;新增一层时所有分支同时增加一层
如何去理解?
根据图4慢慢听我道来:

  1. 对只有结点8这一棵树进行插入,此时分支只有一个就是8,这个分支插满了吗?没有,因为一个结点最多可以放两个数据(2-3树),所以不能增加一层。
  2. 插入数据12,这时分支也只有一个,是4&8结点,这个分支插满了吗?对的,那么就可以新增下一层了,新增一层后,注意,上一层的所有孩子指针就代表了分支结构,也就是说在一个拥有三个孩子指针的结点下面新增一层,那么就意味着有三个分支,这些分支不可为空,怎么办呢?很简单,把三个孩子指针的结点变成两个孩子指针的结点,多出来的一个数据成为它的孩子。就像第二步,插入12,意味着要新增一层,然后就会多出来三个分支,我数据不够啊,所以我就把上一层的3孩子结点变为2孩子结点,数据4变成了结点8的左孩子。——这就是我们所说的新增一层时所有分支同时增加一层
  3. 插入数据5,同样,应该插入在分支1上,那分支1插满了吗?没有,那就插入
  4. 插入数据6,分支1依旧没有插满,但是4&5结点满了,所以要往上去插入,但是呢如图第一种插入是错误的,因为要记住分支没插满不可以有下一层,如果把数据6插入到8结点的话,就会出现中间有一个分支是空的,这个分支为空就意味着下一层不能存在。因此我们把数据5放到8结点,再插入数据6,满足我们所说的不允许一个分支还没有完全插满的时候增加下一层。
    image.png

思路如此, 具体的方法如下(参考这篇文章有图解哦)B树和B+树的插入删除

插入

  1. 找到插入的地方
    插入的地方其实就是找到B树的叶子结点
  2. 判断插入地方的叶子结点是什么类型
    ——若为空树,插入一个2型结点即可,完成。
    ——若为k(k,则将数据插入到此结点,使k型结点变成k+1型结点,完成。
    ——若为m型结点则进行第3步
  3. 当叶子结点已经是m型结点:
    先插入元素到叶子结点,此时叶子结点元素个数比规定超过1,那么以排序后到中间元素为界进行二分裂,然后中间元素插入到双亲结点(实际上双亲结点每形态增加1,就意味着多一个孩子,就表示要把原本的孩子拆分一半多出一个来
    ——若叶子结点的双亲结点A是m-1型结点,则双亲结点形态增1,变为m型结点,孩子多了一个
    ——叶子结点的双亲结点A是m型结点,则看双亲的双亲是否是m-1型结点,若是则把此m-1型结点变为m型结点,若不是那么就插入之后挑出中间元素放到它的双亲结点
    ——分支结构已经插满,则增加一层:这意味着插入到根结点后,根结点超过最大元素容量,这时,根结点分裂成三块:左边元素、中间元素、右边元素,左边、右边元素各自保持自己的指针孩子,然后中间元素的左右孩子变为左右两边元素

这里解释一下B树的非根结点为什么孩子指针数量范围是在:(对m/2向上取整)< m
因为在B树的结点满了之后进行插入的话,需要把已经超过一个元素的结点进行以中间元素为分界的三分裂(左中右三部分),其中中间元素自己为一部分,用公式来表达就是[(m+1)-1]/2

删除

具体的看之前说的那篇博客吧emmm
总结来说要注意几点:

  1. 删除高层的非根结点里的元素,也是一个递归的过程
  2. 对于非根结点元素的删除的思想就是找后继覆盖然后删除原后继,然后对自己的孩子进行重新填充和合并

你可能感兴趣的:(查找——数据的查找(二))