【连载】关系型数据库是怎么工作的?(3) - 归并排序
当你需要对一个集合排序时,你会怎么做?什么?你会调用排序函数…好吧,也算个好答案…但是对于数据库来说,你必须理解这个排序函数是如何工作的?
有很多高效的排序算法,但让我们聚焦于最重要的一个:归并排序。你现在也许不太明白排序为什么很有用,但是到后续章节讲到查询排序你就会明白了。
归并
和很多有用的算法一样,归并排序其实是用于合并两个有序等长(N/2)数组,并且只需要N个操作。这个操作就被称为归并。让我们来看一个简单的例子:
在上图中可以看到,因为初始的两个4元素数组是有序的,所以生成最终的8元素数组只需要遍历一次2个4元素数组。
- 1)比较两个当前元素的当前元素(第一次就是0号元素);
- 2)比较后较小的元素放入8元素数组;
- 3)移动上次比较元素较小的数组的下标;
- 重复以上3步,直到到达某一个数组的最大下标;
- 然后把另外一个数组的剩余元素都放入最终数组的尾部。
现在我们理解了归并排序的思想,伪代码如下:
array mergeSort(array a)
if(length(a)==1)
return a[0];
end if
//recursive calls
[left_array right_array] := split_into_2_equally_sized_arrays(a);
array new_left_array := mergeSort(left_array);
array new_right_array := mergeSort(right_array);
//merging the 2 small ordered arrays into a big one
array result := merge(new_left_array,new_right_array);
return result;
译者批注:原作的伪代码个人认为不太直观,因此采用Java风格重写了一段实现:
int[4] array_a = {1, 5, 6, 7};
int[4] array_b = {2, 3, 4, 8};
int[8] array_result = {};
for(int i=0, int j=0, int k=0;i < 4, j < 4, k < 8;) {
if(array_a[i] <= array_b[j]) {
array_result[k] = array_a[i];
i++;
k++;
} else {
array_result[k] = array_b[j];
j++;
k++;
}
}
归并排序实际上是把一个大问题拆分成一个一个的小问题,解决每一个小问题最终就可以得到大问题的答案。(译者批注:有没有感觉和MapReduce的思想一模一样呢,其实这是我们解决规模比较大的复杂问题的常用手段和技巧)如果你还没有理解归并排序,不必担心,笔者第一次见到这个算法也不理解。我想将其看成一个两阶段算法对于彻底理解它会很有帮助:
- 1)拆分阶段:将一个大的数组划分为一个一个的小数组;
- 2)排序阶段:将每一个小数组合并成为一个大数组;
拆分阶段
在拆分阶段,用3(因为log(8)=3)步就可以将大数组拆分为8个单元素数组。
我是怎么知道的?很简单,我是上帝咯!(译者说明:笔者好幽默)其实是靠数学计算,其思想其实就是按照2分法拆分原始数组,这就是精确的算法定义。
排序阶段
在排序阶段,你可以通过8步将全部的单元素数组合并为一个大小为N的大数组。
- 在第一步,你将做4次合并,每次合并包含2次操作;
- 在第二步,你将做2次合并,每次合并包含4次操作;
- 在第三步,你将做1次合并,每次合并包含8次操作;
因此,整个过程包括3步(log(N)),并且总共包含16次(N*log(N))操作。
归并排序的强大之处
为什么这个排序这么强大呢?因为:
- 你可以修正它,用于降低内存占用,也就是说你无须创建新的数组就可以直接修改输入数组。(这种算法称为in-place)
- 你可以修正它,用磁盘空间和少量内存就可以避免大量的磁盘IO消耗。其思想是只加载当前正在处理的数据到内存,这很重要,当你只有很小的内存缓存但确要排序海量数据时。(这种算法称为external sorting)
- 你可以修正它,运行多个进程、线程和服务。比如:分布式排序就是Hadoop组件的核心。
- 这个算法还可以点石成金。(译者说明:越简单的理论越重要,也越不容易过时)
这个算法被用在绝大多数数据库中,但不是唯一被使用的排序算法。如果你想了解更多的排序算法,可以查阅这篇介绍各种数据库常用排序算法优缺点的论文。