二叉排序树、平衡二叉树、B树&B+树、红黑树的设计动机、缺陷与应用场景

之前面试时曾被问到“如果实现操作系统的线程调度应该采用什么数据结构?”,因为我看过ucore的源码,知道ucore是采用斜堆的方式实现的,可以做到O(n)的插入、O(1)的查找。我回答了斜堆,但面试官坚持让我在B树和红黑树之间选择一个,由于实际上很少用到B树和红黑树,所以我也不太清楚,只是隐约记得红黑树用于磁盘读取比较好,好像和数据大小和数据连续性相关,显然我记错了。当时觉得红黑树还有一些应用,应该命中的可能性比较大,就随意答了红黑树。当然回答错啦,面试官还给我简单讲解了一下,一直想着有机会好好总结一下这几个树的设计动机、缺陷和应用场景。最近有些时间,决定好好复习一下,毕竟数据结构才是基础。

为什么要引入树呢?一般在学习数据结构时,往往先学习向量和列表,然后是栈与队列。事实上,栈与队列只是在组织数据时额外加入了一些限制,分别是先进后出和先进先出,向量和列表都可以实现这种策略。那么向量和列表又存在什么缺陷呢?一般来讲,我们需要对数据结构做静态操作和动态操作,静态操作就是查找,动态操作就是插入和删除。在C/C++中一般采用数组实现向量,采用链表实现列表。接下来我们看一下它们的静态和动态操作的时间开销。

| | 向量 | 列表

  • | :-: | -:
    静态操作(查找) | O(1)| O(n)
    动态操作(插入、删除) | O(n) | O(1)

可以看到,已有的数据结构不能很好的平衡静态操作和动态操作的时间开销。

二叉排序树、平衡二叉树

一种简单的方法是采用二叉排序树(也叫二叉搜索树,BST,Binary Search Tree),构造一颗二叉排序树十分简单。一般来讲,大于根节点的放在根节点的右子树上,小于根节点的放在根节点的左子树上(如果等于根节点,则可视情况而定),如果写程序的话,可以采用递归的方式,而且由于不存在重叠子问题的情况,因此递归的性能已经足够好(不考虑栈溢出的情况)。
二叉排序树在通常情况下可以达到O(lgN)的静态、动态操作的时间复杂度,但是存在一种特殊情况,即输入的数据本身就是有序的,这时二叉排序树退化成向量。
为了消除二叉排序树对于输入敏感的特性,引入平衡二叉树,事实上平衡二叉树应该叫平衡二叉排序树也许更合理。它采用递归的方式定义,这里我就不去查书上的标准定义了,只要保证每个节点左子树和右子树的高度差小于等于1就可以了。
平衡二叉树(也叫AVL树)就比较好用了,可以弥补二叉排序树对输入敏感的缺陷,可以确保静态和动态操作的时间复杂度为O(loN)。

B树&B+树

1981年,Bill Gates曾说过这样一段话“640KB is ought to be enough for anybody.”,可能当时intel生产的内存只有640KB,今天看来这句话也许很荒唐,目前好一点的机器都已经达到16GB的内存了,但是事实上这句话仍然有一定的道理。
只要学习过操作系统的人都知道,存储器的访问速度和容量往往成反比(这句话不是很妥当,只要领会意思就好),最快的当然是CPU上的寄存器,然后是cache(多级cache,不同硬件平台不同),然后是内存、外部硬盘等等。当两个处在不同层级上的存储器彼此之间交互数据时(例如内存与硬盘之间),我们称之为I/O,事实上这种I/O操作相当耗时,应该尽可能避免。
B树的出现就是为了解决这个问题,B树由于是多路二叉树,事实上它的高度要远低于二叉平衡树。一般来讲,我们可以认为二叉平衡树每下降一层需要执行一次磁盘I/O操作,以1G数据为例,平均需要30次磁盘I/O才能读取到数据,而B树每下降一层,每个超级结点都会读入多个关键码,需要的磁盘I/O次数小于AVL树,因此B树适用于实现磁盘的读写逻辑。

红黑树

对于向量、列表、树和图来说,它们每次的动态操作都会完全遗忘之前的状态,转而到达全新的状态,这种数据结构称为ephemeral structure。另一种数据结构可以记录某一历史时刻的状态,在访问时可以根据版本好+目标数据进行访问,这种数据结构称为persistent structure。事实上,红黑树可以实现这种对历史版本的记录,我也不懂它为什么有这么奇妙的功能,这里就不再深入研究了,因为我觉得研究明白以后我还是会忘,有机会再看。

结论

回到最初的问题,即面试官提到的问题。我觉得面试官强迫我在B树和红黑树之间选择没有多少道理,因此线程调度的数据量很小,不涉及磁盘I/O的问题,同时线程调度好像也没有记录历史版本的需求,而这两个数据结构的静态和动态操作的时间复杂度一样,空间复杂度也接近,因此它们两个应该差不多才对。话说,它们两个的实现效率都不如斜堆。

posted @ 2018-02-28 19:50  miao_zheng  阅读(4001)  评论(0编辑  收藏  举报