排序算法之归并排序
这里是传送门⇒总结:关于排序算法
平均时间复杂度 | 最优时间复杂度 | 最差时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|
二路归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
归并排序采用分治策略,先划分,再合并。若排序中每次合并的时候是2个有序子序列合并,称“二路归并排序”。这里讨论的就是“二路归并排序”的递归做法(好像递归可能爆栈,如果可以,建议研究下非递归做法)
- 算法描述
- 将待排序列分成2部分,分别使2个子序列有序,之后把2个有序子序列合并成更大的有序序列
- 而使2个子序列有序的方法是把子序列再分成2部分,再分别使2个子序列有序,再合并
- 而使2个子序列...
- 直到子序列只有1个元素
- 合并的具体实现
- 设置2个指针分别从左到右扫描2个有序子序列,依次寻找2个子序列较小元素放入temp数组,用temp中合并排序好的子序列替换原数组array中没排序好的原子序列
- JS实现
// 此处传入的array会被直接改变
function Merge(array, left, mid, right) {
var i = left;
var j = mid + 1;
var k = 0;
var temp = [];
while (i <= mid && j <= right) {
if (array[i] <= array[j]) {
temp[k++] = array[i++];
} else {
temp[k++] = array[j++];
}
}
while (i <= mid) {
temp[k++] = array[i++];
}
while (j <= right) {
temp[k++] = array[j++];
}
while (k > 0) {
array[--j] = temp[--k];
}
}
// 此处传入的array会被直接改变
function MergeSort(array, left, right) {
if (left < right) {
var mid = Math.floor((left + right) / 2);
MergeSort(array, left, mid);
MergeSort(array, mid + 1, right);
Merge(array, left, mid, right);
}
}
- 分析
- 归并排序过程中,需要不断对当前序列进行对半划分,直到子序列长度为1,也就是说每一次划分后序列长度都为原本的1/2。这意味着需要划分log2n次(由n/2k = 1计算可得次数k)
- 每一次划分之后都会进行合并操作,每一次划分后的合并操作会将长度为n的待排序列遍历一遍。过程如下:在第1次划分中,出现2个长度为n/2的子序列,需要合并1次,每次遍历n个数据;在第2次划分中,出现4个长度为n/4的子序列,需要合并2次,每次遍历n/2个数据;...当第log2n次划分中,出现n个长度为1的子序列,需要合并n/2次,每次遍历2个数据。通过这个过程可以知道每一层(划分+合并)的时间复杂度为O(n)
- 每一层的时间复杂度为O(n),结合上面分析的划分次数为log2n,可以得到归并排序的时间复杂度为O(nlogn)
- 在归并排序的
Merge
函数中,合并2个子序列时,需要借助额外的存储空间。每次合并过程中都需要申请额外的内存空间,但是合并完成后,临时开辟的内存空间就被释放掉了,在任意时刻,只会有一个Merge
函数在执行,也就只会有一个临时的内存空间在使用。临时空间再大也不会超过n个数据的大小,所以空间复杂度是O(n) - 由于在合并时,如果有两个相同的元素,是先把左边子序列中的元素放进temp,再放右边子序列的相同元素,所以两个相同元素的相对位置不变,即归并排序是稳定的