归并排序 Merge-sort

算法

归并排序(merge-sort),典型的分治策略(divide and conquer)。核心思路是将整体序列一分为二形成两个子序列,分别对子序列排序,再将两个有序子序列合并成一个有序序列。

思路如图:

image-20200405121524144

整体过程分为拆分和合并两大阶段。

拆分,核心问题是确定拆分位置即可,我们利用左右元素索引之和除2即可,也就是:mid = (left + right)/2, 直到拆分到子序列仅仅存在一个元素的基本情形。

合并,merge 是归并排序的核心,将两个已排序子序列合并为一个排序序列的过程。当子序列中仅存在一个元素时,可视为子序列已经排序,因此我们的合并是从两个单一元素子序列开始的。当子序列存在多个元素时,我们需要逐个得到当前最小元素,进而完成整体排序,过程中我们需要一个临时区来存储已排序的部分。

合并思路如下图所示,我们以合并 [2, 3, 5, 6, 8] 和 [0, 1, 4, 7, 9] 为例,进行演示:

merge

如图,实现时,设置 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]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

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]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

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 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

PHP

// to be continued
1

分析

从复杂度上看,合并操作的平均时间复杂度为O(n),拆分的二叉树深度为lgn,总的平均时间复杂度为O(nlgn),并且最好和最坏情况的时间复杂度都为O(nlgn)。

从稳定性上看,data[i] <= data[j] 的元素间比较是稳定的。

参考:《算法导论》第三版!