算法原理
归并排序是采用分治法的一个典型的应用:
分割(分):
- 获取数组的中点 mid,根据此点将数组一分为二
- 递归分割左右数组,直到数组中只剩下一个元素
合并(治):
- 只剩下了一个元素之后,将左右子数组合并,合并的过程中将其进行排序
- 两两排好序的子数组合并以后会变成大的排好序的子数组,这样递归结束后整个数组就是排好序的了。
算法图解
根据算法的原理可以知道,归并排序的重点在于 分 和 治。假设有数组 [9, 4, 6, 8, 1, 3, 2, 5] ,要让其从小到大进行排列,其归并排序的思路图如下所示。
在 治 的过程中是将两两已经有序的子数组合并成一个大的有序的数组,这其中需要有一个 temp 数组用来临时存放。以上面最后一次合并为例,将 [4, 6, 8, 9] 和 [1, 2, 3, 5] 合并成 [1, 2, 3, 4, 5, 6, 8, 9] ,其具体过程如下所示,其中涉及到的英文缩写:
- lp:lPointer,指向左子数组开头的指针
- rp:rPointer,指向右子数组开头的指针
- temp:临时数组
- tp:tPointer,指向 temp 数组开头的指针
javascript 代码
代码整体的递归流程如下图所示,其中:
- mS:对应代码中的 mergeSort
- m:对应代码中的 merge
- a:对应代码中的 arr
- t:对应代码中的 t
/**
* 归并排序
* 输入:
* 待排序的数组
* 数组左边界
* 数组右边界
* 临时数组
* 输出:从小到大排好序的数组
*/
function mergeSort(arr, left, right, temp) {
// 递归进行分治
if (left < right) {
// 计算出中间值
let mid = Math.floor((left + right) / 2);
mergeSort(arr, left, mid, temp); // 左子数组
mergeSort(arr, mid + 1, right, temp); // 右子数组
merge(arr, left, right, temp); // 合并子数组
}
return arr;
}
/**
* 合并子数组
* 输入:
* 待排序的数组
* 子数组左边界
* 子数组右边界
* 临时数组
* 输出:子数组排好序的数组
*/
function merge(arr, left, right, temp) {
let mid = Math.floor((left + right) / 2);
let lPointer = left; // 左子数组指针
let rPointer = mid + 1; // 右子数组指针
let tPointer = 0; // 临时数组的指针
// 比较左右子数组,然后把较小的挑出来放到 temp
while (lPointer <= mid && rPointer <= right) {
if (arr[lPointer] < arr[rPointer]) {
// 将左子数组中的值赋值到 temp 数组中
temp[tPointer++] = arr[lPointer++];
} else {
// 右子数组中的值较小,将其赋值到 temp 数组中
temp[tPointer++] = arr[rPointer++];
}
}
// 左右子数组中的一个已经赋值完,那么将另一个中的剩余值依次赋值到 temp 中即可
// 左子数组没完,右子数组完了
while (lPointer <= mid) {
temp[tPointer++] = arr[lPointer++];
}
// 左子数组完了,有子数组没完
while (rPointer <= right) {
temp[tPointer++] = arr[rPointer++];
}
// 将 temp 数组的内容赋值回原数组中
tPointer = 0;
for (let i = left; i <= right; i++) {
arr[i] = temp[tPointer++];
}
}
// 测试
let testArr = [9, 4, 6, 8, 1, 3, 2, 5];
let left = 0;
let right = testArr.length - 1;
let temp = [];
console.log(mergeSort(testArr, left, right, temp));