之前我们说了冒泡排序和直接插入排序,这两种排序算法都是O(n^2)的时间复杂度,在数据规模小的时候可以使用这两种,但是对于数据规模比较大的情况复杂度就会比较大了,因此在这一节中我们来看看另外两种排序算法,归并排序和快速排序。
归并排序实际上利用了分而治之的思想,我们先将一个无序的数组分解成两个部分,分别对这两个数组递归进行归并排序,最后将结果合并就能得到一个有序的数组了。也就是说我们是将原本一个大的问题分解成多个小的子问题,这些子问题除了数据规模不一样,求解方法都是一样的。这是一种解决问题的思想。我们来看看怎么将数组分解的:
可以看到实际上,merge sort就是两个过程,分解和合并,我们可以递归来解决,那么我们如何将显而易见的过程转换为编码呢?对于递归最重要的就是写递推公式。我们先来分析一下,假设数组的第一个数和最后一个数的位置分别为p和q,第一次分裂的位置为m, 那么我们就可以得到这样一个公式:mergeSort(p…q) = merge(mergeSort(p…m), mergeSort(m+1…q)),那么什么时候就可以不用分解了呢?当p>=q的时候就可以不用分解了。因此我们需要两个函数,一个函数递归调用来分解数组,另一个函数用于分解后的合并工作,因此我们可以将这个过程描述为以下伪代码
// 插入排序伪代码
mergeSort(arry) {
// arry为待排序的数组,另外两个参数为分割下标
mergeDivide(arry, 0, arry.length);
}
mergeDivide(arry, p, q) {
//终止条件
if (p >= q) return;
// 递归调用
m = (p + q) / 2; // 分裂的位置
mergeDivide(arry, p, m);
mergeDivide(arry, m+1, q);
// 合并函数
mergeArry(arry[p,q], arry[p,m], arry[m+1, q]);
}
我们可以看到整个过程还是比较清晰的,首先递归调用分解函数对数组进行分解,然后调用合并函数,将分解后的数组合并成有序数组。需要注意我们在分析或者写递归代码的时候不要尝试从计算机的角度去分析理解递归的每一步的结果和返回,这样要不了几步就被绕晕了,本来计算机就适合处理这样的问题。我们需要做的是,假设之前的问题已经解决了,因此从这个角度去看你就明白了,假设第一步分裂的数组已经有序了,接下来要做的就是将数组合并就行了。那么对于合并这一步我们应该怎么做呢?
我们申请一个临时数组tmp,其大小和原数组相同。附设两个指针p1,p2,分别指向两个数组的第一个元素,然后分别比较指针指向的两个元素大小。如果p1指向的元素小于等于p2,我们就将p1指向的元素放入临时数组的第一个位置,然后p1向后移动一步。否则p2放入,p2向后移动一步。再接着往下比较,直到其中一个数组的元素已经比较完毕,则将另一个数组直接拷贝放到tmp数组的后面位置即可。最后将临时数组拷贝到原数组中即可。
现在我们来用代码实现
public class MergeSort {
// 归并排序算法, a是数组,n表示数组大小
public static void mergeSort(int[] arry) {
int length = arry.length;
mergeSortDivide(arry, 0, length-1);
}
// 递归调