点击上方蓝色 “铁匠学编程” 关注我,让我们一起学习!
前面给大家分享了冒泡排序、插入排序和选择排序,他们的时间复杂度都是O(n2),比较高,适合小规模的数据排序。今天我们就分享一种时间复杂度为O(nlogn)的算法,它就是--归并排序。
归并排序原理
它的原理比较简单,就是将一个数组从中间分为两部分然后分别进行排序,最后将排好序的两部分合并到一起,这样数组就有序了。
原理如下图:
归并排序用到了分治思想,字面就是分而治之,将一个大问题分成若干个小问题单独进行处理,后面我们再单独讲下。
从上图也可以看出,归并用到了递归,归并一般都是通过递归来实现的。归并是一种解决问题的思想,递归是一种编程技巧。
代码示例
Go示例:
package mainimport "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 = 0while (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的下标
写在最后:最近工作拿到一个需求不大,但涉及部门所有核心业务花费了很多时间学习历史代码和请教同事,昨天停更,深深的一种负罪感!? 记得小浩算法大佬就分享过该怎么持续输出,不一定是每天都写,时间多的时候可以多输出一些,看来最近要好好学习,刚开始分享学习了不少大神的思路和方式,感谢!
最近将有“大事”发生,密谋已久希望成功!关注不会让你失望哦~
都到这了,加个关注吧!
加油!不仅自己~还有你~