前面给大家分享了冒泡排序、插入排序和选择排序,他们的时间复杂度都是O(n2),比较高,适合小规模的数据排序。今天我们就分享一种时间复杂度为O(nlogn)的算法,它就是–归并排序。
归并排序原理
它的原理比较简单,就是将一个数组从中间分为两部分然后分别进行排序,最后将排好序的两部分合并到一起,这样数组就有序了。
原理如下图:
归并排序用到了分治思想,字面就是分而治之,将一个大问题分成若干个小问题单独进行处理,后面我们再单独讲下。
从上图也可以看出,归并用到了递归,归并一般都是通过递归来实现的。归并是一种解决问题的思想,递归是一种编程技巧。
代码示例
Go示例:
package main
import "fmt"
func MergeSort(arr []int) {
len := len(arr)
if len <= 1 {
return
}
mergeSort(arr, 0, len-1)
}
func mergeSort(arr []int, start, end int) {
if start >= end {
return
}
middle := (start + end) / 2
mergeSort(arr, start, middle)
mergeSort(arr, middle+1, end)
merge(arr, start, middle, end)
}
func merge(arr []int, start, middle, end int) {
tempArr := make([]int, end-start+1)
i := start
j := middle + 1
k := 0
for ; i <= middle && j <= end; k++ {
if arr[i] <= arr[j] {
tempArr[k] = arr[i]
i++
} else {
tempArr[k] = arr[j]
j++
}
}
for ; i <= middle; i++ {
tempArr[k] = arr[i]
k++
}
for ; j <= end; j++ {
tempArr[k] = arr[j]
k++
}
copy(arr[start:end+1], tempArr)
}
func main() {
arr := []int{8, 3, 4, 5, 9, 2, 1}
MergeSort(arr)
fmt.Println(arr)
}
PHP示例:
function merge_sort($nums)
{
if (count($nums) <= 1) {
return $nums;
}
merge_sort_c($nums, 0, count($nums) - 1);
return $nums;
}
function merge_sort_c(&$nums, $p, $r)
{
if ($p >= $r) {
return;
}
$q = floor(($p + $r) / 2);
merge_sort_c($nums, $p, $q);
merge_sort_c($nums, $q + 1, $r);
merge($nums, ['start' => $p, 'end' => $q], ['start' => $q + 1, 'end' => $r]);
}
function merge(&$nums, $nums_p, $nums_q)
{
$temp = [];
$i = $nums_p['start'];
$j = $nums_q['start'];
$k = 0;
while ($i <= $nums_p['end'] && $j <= $nums_q['end']) {
if ($nums[$i] <= $nums[$j]) {
$temp[$k++] = $nums[$i++];
} else {
$temp[$k++] = $nums[$j++];
}
}
if ($i <= $nums_p['end']) {
for (; $i <= $nums_p['end']; $i++) {
$temp[$k++] = $nums[$i];
}
}
if ($j <= $nums_q['end']) {
for (; $j <= $nums_q['end']; $j++) {
$temp[$k++] = $nums[$j];
}
}
for ($x = 0; $x < $k; $x++) {
$nums[$nums_p['start'] + $x] = $temp[$x];
}
}
$nums = [4, 5, 6, 3, 2, 1];
$nums = merge_sort($nums);
print_r($nums);
JS示例:
const mergeArr = (left, right) => {
let temp = []
let leftIndex = 0
let rightIndex = 0
while (left.length > leftIndex && right.length > rightIndex) {
if (left[leftIndex] <= right[rightIndex]) {
temp.push(left[leftIndex])
leftIndex++
} else {
temp.push(right[rightIndex])
rightIndex++
}
}
return temp.concat(left.slice(leftIndex)).concat(right.slice(rightIndex))
}
const mergeSort = (arr) => {
if (arr.length <= 1) return arr
const middle = Math.floor(arr.length / 2)
const left = arr.slice(0, middle)
const right = arr.slice(middle)
return mergeArr(mergeSort(left), mergeSort(right))
}
const testArr = []
let i = 0
while (i < 100) {
testArr.push(Math.floor(Math.random() * 1000))
i++
}
const res = mergeSort(testArr)
console.log(res)
性能分析
最后我们看下归并排序的性能和稳定性:
- 时间复杂度:是O(nlogn),要优于冒泡和插入排序
- 空间复杂度:需要额外的空间存放排序的数据,不是原地排序
- 算法稳定性:不涉及相等元素位置交换,是稳定的排序算法
归并排序时间复杂度计算方式:
归并的思想将一个复杂的问题a拆解为b和c,再将子问题合并计算结果,最终得到问题的答案,这里我们将归并排序总的时间复杂度设为 T(n),则 T(n) = 2*T(n/2) + n,其中 T(n/2) 是递归拆解的第一步对应子问题的时间复杂度,n 则是合并函数的时间复杂度(一个循环遍历),依次类推,我们可以推导 T(n) 的计算逻辑如下:
T(n) = 2*T(n/2) + n
= 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
= 4(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
= ...
= 2^k*T(n/2^k) + k*n
递归到最后,T(n/2k)≈T(1),也就是 n/2k = 1,计算归并排序的时间复杂度,就演变成了计算 k 的值,2k = n,所以 k=log2n,我们把 k 的值带入上述 T(n) 的推导公式,得到:
T(n) = n*T(1) + n*log2n = n(C + log2n) //其中2为log的下标