浅谈二分查找,二叉树,平衡二叉树,B树,B+树

Posted by JustDoDT on May 30, 2019

二分查找

二分查找是最基本的,后面的二叉树,平衡二叉树,B树,B+树都是基于二分查找演变而来的。

二分查找法作为一种常见的查找方法,将原本是线性时间提升到了对数时间范围,大大缩短了搜索时间,但它有一个前提,就是必须在有序数据中进行查找。

代码实现

public int binarySearch(int[] A, int target, int n){
    int low = 0, high = n, mid;
    while(low <= high){
        mid = low + (high - low) / 2;
        if(A[mid] == target){
            return mid;
        }else if(A[mid] > target){
            high = mid - 1;
        }else{
            low = mid + 1;
        }
    }
    return -1;
}

二叉树

二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(Left Subtree)和“右子树”(Right Subtree)。二叉树常被用于实现二叉查找树和二叉堆。

二叉树有如下特性:

  • 每个结点都包含一个元素以及 n 个子树,这里 0≤n≤2。
  • 左子树和右子树是有顺序的,次序不能任意颠倒。左子树的值要小于父结点,右子树的值要大于父结点。

假如我们有一组数[35 27 48 12 29 38 55],顺序的插入到一个数的结构中,最终的结果如下:

数据密集型应用设计

好了,这就是一棵二叉树啦!我们能看到,经过一系列的插入操作之后,原本无序的一组数已经变成一个有序的结构了,并且这个树满足了上面提到的两个二叉树的特性!

但是如果同样是上面那一组数,我们自己升序排列后再插入,也就是说按照[12 27 29 35 38 48 55]的顺序插入,会怎么样呢?

数据密集型应用设计

由于是升序插入,新插入的数据总是比已存在的结点数据都要大,所以每次都会往结点的右边插入,最终导致这棵树严重倾斜。

上图就是最坏的情况,也就是一棵树退化为一个线性链表了,这样查找效率自然就低了,完全没有发挥树的优势了呢!

为了较大发挥二叉树的查找效率,让二叉树不再倾斜,保持各科平衡,所以有了平衡二叉树!

平衡二叉树

  • 概念

    平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树的数据结构。

  • 特点

    平衡二叉树是采用二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据减少无关数据的检索,大大的提升了数据检索的速度;平衡二叉树的数据结构组装过程有以下规则:

    (1)非叶子节点只能允许最多两个子节点存在。

    (2)每一个非叶子节点数据分布规则为左边的子节点小当前节点的值,右边的子节点大于当前节点的值(这里 值是基于自己的算法规则而定的,比如hash值);

平衡树的层级结构:因为平衡二叉树查询性能和树的层级(h高度)成反比,h值越小查询越快、为了保证树的结构左右两端数据大致平衡降低二叉树的查询难度一般会采用一种算法机制实现节点数据结构的平衡,实现了这种算法的有比如AVL、Treap、红黑树,使用平衡二叉树能保证数据的左右两边的节点层级相差不会大于1.,通过这样避免树形结构由于删除增加变成线性链表影响查询效率,保证数据平衡的情况下查找数据的速度近于二分法查找;

前面[35 27 48 12 29 38 55]插入完成后的图,其实就已经是一棵平衡二叉树啦。

那如果按照[12 27 29 35 38 48 55]的顺序插入一棵平衡二叉树,会怎么样呢?

数据密集型应用设计

这棵树始终满足平衡二叉树的几个特性而保持平衡!这样我们的树也不会退化为线性链表了!

我们需要查找一个数的时候就能沿着树根一直往下找,这样的查找效率和二分法查找是一样的呢!

一棵平衡二叉树能容纳多少的结点呢?

这跟树的高度有关系的,假设树的高度为h,那每一层最多容纳的结点数量为 2^(n-1),整棵树最多容纳节点数为 2^0+2^1+2^2+…+2^(h-1)。

这样计算,100w 数据树的高度大概在 20 左右,也就是说从有着 100w 条数据的平衡二叉树中找一个数据,最坏的情况下需要 20 次查找。

如果是内存操作,效率也是很高的!但是我们数据库中的数据基本都是放在磁盘中的,每读取一个二叉树的结点就是一次磁盘 IO,这样我们找一条数据如果要经过 20 次磁盘的 IO?

平衡二叉树的特点:

(1)非叶子节点最多拥有两个子节点;

(2)非叶子节值大于左边子节点、小于右边子节点;

(3)树的左右两边的层级数相差不会大于1;

(4)没有值相等重复的节点;

B树

根据上面的平衡二叉树可以得出,如果数据量大的话,查询某一条数据,会大大增加磁盘IO操作(内存数据库除外),这样会使得性能降低,于是我们便想到是否可以把这一颗平衡二叉树压缩一下,让她的叶子节点变多一些,高度降低一些,于是就演变为了B树。虽然我矮,但是我胖。

这颗矮胖的树就是 B-Tree,注意中间是杠精的杠而不是减,所以也不要读成 B 减 Tree 了。

那 B-Tree 有哪些特性呢?一棵 m 阶的 B-Tree 有如下特性:

  • (1) 每个结点最多 m 个子结点。
  • (2) 除了根结点和叶子结点外,每个结点最少有 m/2(向上取整)个子结点。
  • (3) 如果根结点不是叶子结点,那根结点至少包含两个子结点。
  • (4) 所有的叶子结点都位于同一层。
  • (5) 每个结点都包含 k 个元素(关键字),这里 m/2≤k。
  • (6) 每个节点中的元素(关键字)从小到大排列。
  • (7) 每个元素(关键字)字左结点的值,都小于或等于该元素(关键字)。右结点的值都大于或等于该元素(关键字)。

下面我们以一个[1,2,3,4,5,6,7]的数组插入一棵 3 阶的 B-Tree 为例,将所有的条件都串起来,你就明白了!

数据密集型应用设计

数据密集型应用设计

那么,你是否对 B-Tree 的几点特性都清晰了呢?在二叉树中,每个结点只有一个元素。

但是在 B-Tree 中,每个结点都可能包含多个元素,并且非叶子结点在元素的左右都有指向子结点的指针。

如果需要查找一个元素,那流程是怎么样的呢?我们看下图,如果我们要在下面的 B-Tree 中找到关键字 24,那流程如下:

数据密集型应用设计

从这个流程我们能看出,B-Tree 的查询效率好像也并不比平衡二叉树高。但是查询所经过的结点数量要少很多,也就意味着要少很多次的磁盘 IO,这对性能的提升是很大的。

从前面对 B-Tree 操作的图,我们能看出来,元素就是类似 1、2、3 这样的数值。

但是数据库的数据都是一条条的数据,如果某个数据库以 B-Tree 的数据结构存储数据,那数据怎么存放的呢?

查看下面的图形

数据密集型应用设计

普通的 B-Tree 的结点中,元素就是一个个的数字。但是上图中,我们把元素部分拆分成了 key-data 的形式,Key 就是数据的主键,Data 就是具体的数据。

这样我们在找一条数的时候,就沿着根结点往下找就 OK 了,效率是比较高的。

B+ 树

B+Tree 是在 B-Tree 基础上的一种优化,使其更适合实现外存储索引结构。

B+Tree 与 B-Tree 的结构很像,但是也有几个自己的特性:

  • 所有的非叶子节点只存储关键字信息。
  • 所有卫星数据(具体数据)都存在叶子结点中。
  • 所有的叶子结点中包含了全部元素的信息。
  • 所有叶子节点之间都有一个链指针。

如果上面 B-Tree 的图变成 B+Tree,那应该如下:

数据密集型应用设计

大家仔细对比于 B-Tree 的图能发现什么不同?

  • 非叶子结点上已经只有 Key 信息了,满足上面第 1 点特性!
  • 所有叶子结点下面都有一个 Data 区域,满足上面第 2 点特性!
  • 非叶子结点的数据在叶子结点上都能找到,如根结点的元素 4、8 在最底层的叶子结点上也能找到,满足上面第 3 点特性!
  • 注意图中叶子结点之间的箭头,满足上面第 4 点特性!

B树 VS B+树

在讲这两种数据结构在数据库中的选择之前,我们还需要了解的一个知识点是操作系统从磁盘读取数据到内存是以磁盘块(Block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。

即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。

这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。

预读的长度一般为页(Page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常为 4K)。

B-Tree 和 B+Tree 该如何选择呢?都有哪些优劣呢 ?

①B-Tree 因为非叶子结点也保存具体数据,所以在查找某个关键字的时候找到即可返回。

而 B+Tree 所有的数据都在叶子结点,每次查找都得到叶子结点。所以在同样高度的 B-Tree 和 B+Tree 中,B-Tree 查找某个关键字的效率更高。

②由于 B+Tree 所有的数据都在叶子结点,并且结点之间有指针连接,在找大于某个关键字或者小于某个关键字的数据的时候,B+Tree 只需要找到该关键字然后沿着链表遍历就可以了,而 B-Tree 还需要遍历该关键字结点的根结点去搜索。

③由于 B-Tree 的每个结点(这里的结点可以理解为一个数据页)都存储主键+实际数据,而 B+Tree 非叶子结点只存储关键字信息,而每个页的大小是有限的,所以同一页能存储的 B-Tree 的数据会比 B+Tree 存储的更少。

这样同样总量的数据,B-Tree 的深度会更大,增大查询时的磁盘 I/O 次数,进而影响查询效率。

为啥MySQL不用hash索引而用B+树索引

hash索引的话,她的时间复杂度是O(1),B+树索引的话,她的时间复杂度是log(n),可是为啥MySQL不用hash呢?

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

思考

假如索引的高度为3,叶子节点的大小是4K,假如每个关键字是8个字节,数值为4个字节,那么请问这张表可以存储多大的数据量?

关键字数量:4K / 12 = 341.3333333333333‬

341 * 341 * 341 * 341 * 4 / 1024 /1024 /1024 = 50 TB 即,一张MySQL表可以存储50TB的数据。

鉴于以上的比较,所以在常用的关系型数据库(MySQL)中,都是选择 B+Tree 的数据结构来存储数据!