算法
归并排序(merge-sort),典型的分治策略(divide and conquer)。核心思路是将整体序列一分为二形成两个子序列,分别对子序列排序,再将两个有序子序列合并成一个有序序列。
思路如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c0SId4YI-1586172341721)(…/asset/image/article/image-20200405121524144.png)]
整体过程分为拆分和合并两大阶段。
拆分,核心问题是确定拆分位置即可,我们利用左右元素索引之和除2即可,也就是:mid = (left + right)/2
,指导拆分到子序列仅仅存在一个元素的基本情形。
合并,merge 是归并排序的核心,将两个已排序子序列合并为一个排序序列的过程。当子序列中仅存在一个元素时,可视为子序列已经排序,因此我们的合并是从两个单一元素子序列开始的。当子序列存在多个元素时,我们需要逐个得到当前最小元素,进而完成整体排序,过程中我们需要一个临时区来存储已排序的部分。
合并思路如下图所示,我们以合并 [2, 3, 5, 6, 8] 和 [0, 1, 4, 7, 9] 为例,进行演示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1BcejyBp-1586172341723)(…/asset/image/article/image-20200405125342380.png)]
如图,实现时,设置 i,j 分别存储两个子序列待比较元素索引。比较后,将小元素移动到临时区,同时右移索引。当其中一个子序列全部元素全部移动到临时区后,另一个子序列将后续元素直接移动到临时区即可,不需要继续比较。最后将临时区已排序数据拷贝回原始序列即可。
编码
Go
// 归并排序
func MergeSort(data []int) {
mergeSort(data, 0, len(data)-1)
}
// @param data []int 待排序切片整体
// @param left int 左元素索引
// @param right int 右元素索引
func mergeSort(data []int, left, right int) {
// 长度 > 1 才处理
if left < right {
// 确定中间元素索引
mid := (left + right ) >> 1
// 归并排序左边部分
mergeSort(data, left, mid)
// 归并排序右边部分
mergeSort(data, mid+1, right)
// 左边和右边部分合并
merge(data, left, mid, right)
}
}
// @param data []int 待排序切片整体
// @param left int 左元素索引
// @param mid int 中间分隔元素索引
// @param right int 右元素索引
func merge(data []int, left, mid, right int) {
// 创建临时数据存储区
temp := make([]int, right-left+1)
// 初始化左右部分的起始索引
i := left
j := mid + 1
// 初始化临时数据索引
ti := 0
// 归并操作
// 在左右两部分同时存在元素的情况下
for i<=mid && j<=right {
// 如果左边小于右边
if data[i] <= data[j] {
// 将左边元素放入临时区
temp[ti] = data[i]
// 递增左边部分索引
i ++
} else { // 否则
// 将右边元素放入临时区
temp[ti] = data[j]
// 递增右边部分索引
j ++
}
// 递增临时索引
ti ++
}
// 将左边未移入临时区的元素,移入
for i<=mid {
temp[ti] = data[i]
ti ++
i ++
}
// 将右边未移入临时区的元素,移入
for j <= right {
temp[ti] = data[j]
ti ++
j ++
}
// 将临时区数据拷贝到 data 对应的位置
for ti=0; left <= right; {
data[left] = temp[ti]
left ++
ti ++
}
}
// 测试通过
data := []int{5, 3, 8, 6, 2, 7, 4, 0, 9, 1}
MergeSort(data)
fmt.Println(data)
// [0 1 2 3 4 5 6 7 8 9]
Python
# 归并排序
def MergeSort(data) :
mergeSort(data, 0, len(data)-1)
# @param data 待排序序列
# @param left 左索引
# @param right 右索引
def mergeSort(data, left, right) :
# 至少含有 1 个元素
if left < right :
# 计算 mid 中间分隔索引
mid = (left + right) >> 1
# 归并排序左边
mergeSort(data, left, mid)
# 归并排序右边
mergeSort(data, mid+1, right)
# 合并结果
merge(data, left, mid, right)
# 合并
# @param data 排序序列
# @param left 左索引
# @param mid 分隔索引
# @param right 右索引
def merge(data, left, mid, right) :
# 构建临时数据区
temp = []
# 初始化索引变量
i = left
j = mid + 1
# 合并,左边和右边都存在未比较的元素时
while i <= mid and j <= right :
# 左边小
if data[i] <= data[j] :
temp.append(data[i])
i += 1
else : # 右边小
temp.append(data[j])
j += 1
# 左边未比较元素放入临时区
for _ in range(i, mid+1) :
temp.append(data[i])
i += 1
# 右边未比较元素放入临时区
for _ in range(j, left+1) :
temp.append(data[j])
j += 1
# 由临时区拷贝回data
i = left
for ti in range(len(temp)) :
data[i] = temp[ti]
i += 1
# 测试通过
data = [5, 3, 8, 6, 2, 7, 4, 0, 9, 1]
MergeSort(data)
print(data)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
JavaScript
function MergeSort(data) {
mergeSort(data, 0, data.length-1)
}
function mergeSort(data, left, right) {
if (left < right) {
// 确定中间索引
let mid = (left + right) >> 1
// 归并排序左边
mergeSort(data, left, mid)
// 归并排序左边
mergeSort(data, mid+1, right)
// 合并
merge(data, left, mid, right)
}
}
function merge(data, left, mid, right) {
// 初始化临时数据 和 迭代变量
let temp = []
let i = left, j = mid + 1
// 合并,左边和右边都存在未比较的元素时
while (i<=mid && j <= right) {
if (data[i] <= data[j]) {
temp.push(data[i++])
} else {
temp.push(data[j++])
}
}
// 左边未比较元素放入临时区
while(i <= mid) {
temp.push(data[i++])
}
// 右边未比较元素放入临时区
while(j <= right) {
temp.push(data[j++])
}
// 由临时区拷贝回data
for(let i = 0; i < temp.length; i ++) {
data[left++] = temp[i]
}
}
// 测试通过
data = [5, 3, 8, 6, 2, 7, 4, 0, 9, 1]
MergeSort(data)
console.log(data)
// Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
PHP
// to be continued
结语
从复杂度上看,合并操作的平均时间复杂度为O(n),拆分的二叉树深度为lgn,总的平均时间复杂度为O(nlgn),并且最好和最坏情况的时间复杂度都为O(nlgn)。
从稳定性上看,data[i] <= data[j]
的元素间比较是稳定的。
参考:《算法导论》第三版!
关注,红牛慕课 发送 算法导论 获取pdf。