这几天一直都在看跟递归有关的内容,于是就顺便复习了一下同样具有递归的分治法思想的归并排序和快排。以前因为一直难以理解递归的思想,导致我理解起快排和归并排序非常的痛苦,所以产生了排斥的心理,现在认真学习了一番发现其实递归的思想并不难理解。
归并排序的算法步骤
1、将数组A[p, r]等分成两个子数组:A[p, q]和A[q+1, r],其中q = (p+r)/2。
2、分别对两个子数组进行归并排序。
3、合并两个已有序的子数组。
核心步骤是第三步,归并排序就是在经过第三步后使得数组由无序变为有序的,所以我们首先要写出第三步的算法,用一个函数merge来实现。既然是要合并两个数组,那函数的参数就是两个有已有序的数组(left和right),返回值是合并后的数组。我们首先用自然语言描述一下合并算法的步骤,然后再贴上代码,如此能够加深对算法的理解。
合并两个子数组的算法步骤
1. 创建一个空的数组result,长度为left.length + right.length
2. 分别取left数组和right数组的首元素,比较它们的大小
3. 将较小的那个添加到result的末尾,并从left或right弹出(pop)
4. 判断left数组和right数组中是否都还有元素,如果还有则跳转到步骤2继续执行;如果有任何一个数组中已经没有元素了,则进行步骤5
5. 将仍有元素的数组中的所有剩余元素直接添加到result数组的末尾
6. 返回result数组,函数退出
Java实现:
public static int[] merge(int[] left, int[] right) {
// 存放结果的数组
int[] result = new int[left.length + right.length];
// i,j是两个子数组的下标,k是result的下标
int i, j, k;
i = j = k = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result[k++] = left[i++];
} else {
result[k++] = right[j++];
}
}
// 将另一序列中剩余的部分直接添加到result的末尾
while (i < left.length) {
result[k++] = left[i++];
}
while (j < right.length) {
result[k++] = right[j++];
}
return result;
}
现在再来考虑下如何实现归并算法,因为使用的是分治法,那么最直观体现这一思想的方法就是使用递归来实现。既然是递归,那就要确定两个必要的要素:base case和recusive case。
Base case:就是数组长度为1时,此时数组中只有一个元素,不需要再进行等分而且可以认为是有序的,到达这个情况的时候,接下来的步骤应该是合并(merge)。
Recursive case:将数组等分成两个子数组。
归并排序Java实现:
public static int[] mergeSort(int[] arr) {
// base case: 如果数组只有一个元素,不需再排序,直接返回
if (arr.length == 1)
return arr;
// #1 将原数组切分成两半
int[] left = Arrays.copyOfRange(arr, 0, arr.length / 2);
int[] right = Arrays.copyOfRange(arr, arr.length / 2, arr.length);
// #2 对切分后的子数组进行排序
left = mergeSort(left);
right = mergeSort(right);
// #3 合并两个子数组
int[] output = merge(left, right);
return output;
}
为了更好的理解函数的递归过程,我从维基百科上找到了一张显示归并排序从切分到合并的图片,直观的解释了归并排序的全过程。
mergeSort函数首先会一直递归到叶子节点,直到left和right都只剩下一个元素,此时可认为left和right是有序的,则接着调用merge函数将left和right合并,排序就是在合并时完成的。
归并排序的时间复杂度
关于复杂度维基百科上写的是无论是最佳情况还是最坏的情况,归并排序都是O(nlogn),我并没有仔细的推算过,因为是递归算法,复杂度不像迭代那样直观。不过可以肯定的是merge函数的复杂度是O(n),因为是将两个子数组按元素大小顺序合并成原数组。
附加上C++实现:
void merge(int a[], const int low, const int mid, const int high) {
int n1 = mid - low + 1,
n2 = high - mid;
int *left = new int[n1],
*right = new int[n2];
int i, j, k = low;
// 将左右子数组的内容拷贝进left和right数组
for (i = 0; i < n1; i++) {
left[i] = a[low + i];
}
for (j = 0; j < n2; j++) {
right[j] = a[mid + 1 + j];
}
i = j = 0;
while (i < n1 && j < n2) {
if (left[i] <= right[j]) {
a[k++] = left[i++];
} else {
a[k++] = right[j++];
}
}
while (i < n1) {
a[k++] = left[i++];
}
while (j < n2) {
a[k++] = right[j++];
}
delete [] left;
delete [] right;
}
void mergeSort(int a[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
mergeSort(a, low, mid);
mergeSort(a, mid + 1, high);
merge(a, low, mid, high);
}
}