距离上次写快排算法的文章已经过去一个半月了,和本文要提到的归并排序算法类似,快排也是分治思想的一种典型应用,如果有不熟悉快速排序的同学可以翻阅我之前写过的的快速排序算法的文章。
分治算法
首先为大家介绍一下什么是分治,分治是将一个大问题分割成若干个和原来问题形式相同但规模更小的子问题,然后处理这些小问题,最终实现整个大问题的过程。
额…上面说的概念确实很难理解,我们换一个生活中的场景来介绍一下什么是分治吧。
小明手里有8枚硬币,其中有1枚是假币,已知假币比真币轻,现在我们有一架天平,那么我们该怎么找出假币呢?
首先我们将硬币分成两组,每组4枚硬币,分别放到天平上称,硬币一定在轻的那一组里,再次将轻的那一边的硬币分成两组,每组2枚硬币,然后再取轻的那一边在进行二分,直到最后将2枚硬币放在天平上,轻的那一枚就是假币。这里用的就是简单的分治思想,每次把问题规模缩小一半,这里不仅是分治思想,其实也是用到了二分法的思想。
归并排序
归并排序是分治算法的一种典型应用,基本思路如下:
-
将数组的前一半进行排序
-
将数组的后一半进行排序
-
将两半数据归并成为一个有序的数组,并拷贝回原数组
不多逼逼,直接给你们看代码。
public void mergeSort(int[] arr, int s, int e, int[] tmp) {
if (s < e) {
int m = s + ((e - s) >> 1);
mergeSort(arr, s, m, tmp);
mergeSort(arr, m + 1, e, tmp);
merge(arr, s, m, e, tmp);
}
}
private void merge(int[] arr, int s, int m, int e, int[] tmp) {
int p = 0; //指向tmp数组当前位置的指针
int p1 = s, p2 = m + 1; //p1和p2分别指向分成两组后两边数组的首位置
while (p1 <= m && p2 <= e) {
if (arr[p1] > arr[p2]) {
tmp[p++] = arr[p2++];
} else {
tmp[p++] = arr[p1++];
}
}
while (p1 <= m) {
tmp[p++] = arr[p1++];
}
while (p2 <= e) {
tmp[p++] = arr[p2++];
}
for (int i = 0; i < e - s + 1; ++i) {
arr[s + i] = tmp[i];
}
}
咱们来梳理一下上面的代码,首先是mergeSort
方法,当起始位置小于终止位置的时候才会执行代码,否则直接结束这个方法。再看看这个判断语句里的内容,首先是求出数组的中点,对数组一分为二进行分治,然后这两个数组又递归调用这个方法,最后对两个已经排好序的数组执行merge
方法,进行归并操作。我们可以知道的是,当分到一定程度(s和e指向相同的元素)时,会结束递归,进行归并。我们用一组数据来模拟这个过程。
以上是一个待排序的数组,数组长度为10,因此传入的s和e分别为0和9,求出的m值为4,于是将数组分为下标04和下标59的两个数组再分别进行归并排序。
为了简化内容,我们尝试对前一个数组进行递归。此时传入的s和e分别是0和4,则m是2,再对前一个数组进行递归,s和e是0和2,m是1,继续递归,s和e是0和1,m是0,这时候两个数组都只有一个数了,这时候就需要对这两个数进行merge
操作。所谓的merge
操作,就是将两个数组中的元素按从小到大的顺序复制到tmp数组中,然后复制回arr数组中。咱们下面用图片来展示一下上面的代码到底对这个数组干了些什么。
实际上,上述过程只是逻辑上的切分和归并,事实上由于递归需要不断压栈以及以上代码需要顺序执行,切分和归并的次数还要更多一些。
最后需要提到的是,归并排序是一种稳定的排序算法,事件复杂度为 。