内存空间和文件空间的索引设计区别

索引(Index)本质上是一种用空间换时间,提高查找性能的手段。从数据结构的角度看,索引是一种键值(key-value)映射关系表。图书的目录就是一种索引,它的key是章节,value是对应章节所在的页码。

在没有索引情况下,想在一个群体中找出个体,需要逐个遍历,其时间复杂度是O(n)。最优解当然是不做任何遍历直接找到个体,时间复杂度是O(1)。但是索引降低查找时间复杂度是以提高空间复杂度为代价的,无法做到在所有情况下都能实现O(1)索引。因此,索引对查找性能提升后的时间复杂度介于O(1)O(n)之间。那么在不同的空间限制条件下,索引是如何建立的呢?

内存级别空间索引:Hash、查找树

内存级别空间的数据量是相对比较小的,一般不超过G量级。这个量级下的索引设计选择比较多,Hash用O(n)的空间复杂度换取到O(1)的时间复杂度,查找树通过调整群体的空间结构实现O(logn)的时间复杂度。

Hash在实现时用Hash函数建立key与value之间的映射关系,这个映射关系就是Hash表。Hash表是一种支持随机读取的存储结构(数组)。常用的Hash函数有:直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法。好的Hash函数主要看三个指标:函数的计算速度快输出值的空间范围小分布均匀冲突概率低。在实际场景中,会专门设计Hash函数,下图是JDK1.8中HashMap的Hash函数实现。Hash函数对不同key计算的结果可能相同,称为Hash冲突。发生冲突时,有以下几种方式:开放定址法、再散列法、链地址法(拉链法)、建立一个公共溢出区。Java中的HashMap就是采用链地址法解决Hash冲突。

查找Tree通过建立树的空间结构,将查找时间复杂度降为O(logn),也就是树的高度。树结构可以是逻辑建立(无需内存空间),也可以是物理建立(需要内存空间)。例如对有序群体上进行二分查找就是不需要额外的内存空间来建树。

由于查找树的查找性能取决于树高,因此如何降低树高是关键。主要两个方向:多叉树平衡树

多叉树每个节点可以有2个以上子树,它是通过增加树的宽度来降低树的高度。多叉树主要用于文件级别空间索引,不会用于内存级别空间索引。因为多叉树查找时高度和宽度两个方向都要考虑,由于都在内存空间,宽度上的查找可以进一步再优化成二叉查找树。所以对于内存空间的查找使用多叉树没有意义。

平衡树基于短板理论,保证树的各个子树高度相差不超过1。内存空间的查找主要用平衡二叉树,它的主要实现有AVL树、红黑树、伸展树等。其中红黑树在实际应用场景使用较多,C++中STL的map和set基于红黑树实现,Java中的TreeSet和TreeMap也是基于红黑树实现。但其实红黑树的平衡性不如AVL树,它没有严格实现子树高度差不超过1。单看查找效率的话,AVL树优于红黑树。但是平衡性越高,保持平衡性的代价也越大。对于AVL树,由于高度平衡,几乎每次节点的增删都会破坏平衡,导致它的复衡(Rebalance)率非常高。并且为了达到高度平衡,AVL树每次复衡操作也比较复杂(最坏情况时间复杂度O(logn))。红黑树的复衡操作在增删节点下都是O(1)。所以红黑树是同时考虑读写性能的折中实现。当然如果你的实际应用场景读操作远大于写操作,可以考虑用AVL树。

文件级别空间索引:B树

当数据规模超出内存空间限制后,需要存储到磁盘文件。从内存查找到文件查找,除了数据量级增加外,还有一个非常关键的质变:查找需要跨越磁盘和内存两层存储介质。那么这两层存储介质有什么区别呢?首先当然是速度的不同,以读取1Mb数据为例,内存需要的时间在100ns量级,普通硬盘10ms量级,固态硬盘2ms量级。内存读写速度比磁盘要高4~5个数量级。另外一个关键区别是,内存支持随机读写磁盘喜欢顺序读写。了解了存储介质读写速度和读写方式的区别,再来看下文件空间的索引该如何设计。

首先考虑下Hash索引,不合适,因为Hash索引的代价是O(n)的空间,此外Hash的内在逻辑是要『打散』,还记得Hash的中文怎么翻译的吗?散列。这与磁盘的读写方式是相违背的,磁盘喜欢连续存储,顺序读取。

再来看查找树。前面提到过,多叉树可以用于文件级别空间索引。一定要多叉吗,二叉不行吗?答案是二叉不行。和多叉相比,二叉是一个纯“离散”的数据结构,而多叉是一个“离散”和“连续”相结合的数据结构。多叉树节点之间是离散的树形结构,节点内部是一个连续的线形结构。这就是为什么内存索引不喜欢多叉,因为内存可以随机读写,多叉“离散”的不彻底,多叉中的“连续”结构可被进一步“离散”,最终优化成二叉。但是多叉中的“连续”性,正好符合磁盘读写性质,所以在文件索引中广泛使用。

B树是多叉平衡树,它同时从宽度和平衡性两个方向减低了树高。在实际场景中,会结合具体需求,对B树做些改造。例如MySQL中的索引采用B树的变种B+树实现,而MongoDB就采用B树实现。B树和B+树的核心区别是,B树所有节点都有data域,而B+树只有叶子节点有data域。MySQL是关系型DB,区间查询(范围查询)的诉求强烈,所有数据存叶结点,上层节点就能存更多索引信息,减少区间查询过程中的磁盘IO。MongoDB是NoSQL型DB,更看重单个个体的查询效率,每层节点存放data域,当索引到该节点时可以立即读到数据,避免继续磁盘IO。它们设计的核心原则是一样的:减少磁盘IO。

 

总结

索引的本质是用空间换时间,提高查找效率。索引的设计过程需要综合考虑数据的量级、存储介质的特点、写操作的影响以及查找的特点。内存空间的索引可以做到O(1)的查找效率,但是代价是O(n)的空间。相对而言,二叉查找树对时空的综合要求更低。当数据量级超出内存空间后需要存储到磁盘文件。磁盘的IO特性决定B树的数据结构更适合文件空间的索引。

发布了10 篇原创文章 · 获赞 11 · 访问量 2113

猜你喜欢

转载自blog.csdn.net/Tim_mwt/article/details/104262722