前置知识:算法笔试中处理输入和输出递归和 master 公式

思路

左部分排好序、右部分排好序、利用 merge 过程让左右整体有序

merge 过程:谁小拷贝谁,直到左右两部分所有的数字耗尽,拷贝回原数组

实现

测试链接:

递归实现

package main
 
import (
	"bufio"
	"os"
	"strconv"
)
 
const (
	// 题目没有说数据量,按道理是要说的
	// 根据实验,长度500以内够用了
	// 如果有一天牛客升级了数据量导致出错
	// 把这个值改大即可
	MAXN = 501
)
 
var ( // 全局静态变量
	arr  = [MAXN]int{}
	help = [MAXN]int{}
	n    int
)
 
func main() {
	input := bufio.NewScanner(os.Stdin)
	// 设置自定义的分隔函数
	input.Split(bufio.ScanWords)
	out := bufio.NewWriterSize(os.Stdout, 4096)
 
	for input.Scan() {
		n, _ = strconv.Atoi(input.Text())
		if n == 0 {
			continue
		}
		for i := 0; i < n; i++ {
			input.Scan()
			arr[i], _ = strconv.Atoi(input.Text())
		}
		mergeSort(0, n-1)
 
		out.WriteString(strconv.Itoa(arr[0]))
		for i := 1; i < n; i++ {
			out.WriteByte(' ')
			out.WriteString(strconv.Itoa(arr[i]))
		}
		out.WriteByte('\n')
		out.Flush()
	}
}
 
func mergeSort(left, right int) {
	if left == right {
		return
	}
	mid := left + (right-left)>>1
	mergeSort(left, mid)
	mergeSort(mid+1, right)
	merge(left, right, mid)
}
 
func merge(left, right, mid int) {
	i := 0
	l := left
	r := mid + 1
	for l <= mid && r <= right {
		if arr[r] < arr[l] {
			help[i] = arr[r]
			r++
		} else {
			help[i] = arr[l]
			l++
		}
		i++
	}
	for l <= mid {
		help[i] = arr[l]
		l++
		i++
	}
	for r <= right {
		help[i] = arr[r]
		r++
		i++
	}
	copy(arr[left:right+1], help[:right+1-left])
}

master 公式 分析时间复杂度:,时间复杂度为

非递归实现

以步长为一组进行 merge,步长每次乘二,直到覆盖数组长度

package main
 
import (
	"bufio"
	"os"
	"strconv"
)
 
const (
	// 题目没有说数据量,按道理是要说的
	// 根据实验,长度500以内够用了
	// 如果有一天牛客升级了数据量导致出错
	// 把这个值改大即可
	MAXN = 501
)
 
var ( // 全局静态变量
	arr  = [MAXN]int{}
	help = [MAXN]int{}
	n    int
)
 
func main() {
	input := bufio.NewScanner(os.Stdin)
	// 设置自定义的分隔函数
	input.Split(bufio.ScanWords)
	out := bufio.NewWriterSize(os.Stdout, 4096)
 
	for input.Scan() {
		n, _ = strconv.Atoi(input.Text())
		if n == 0 {
			continue
		}
		for i := 0; i < n; i++ {
			input.Scan()
			arr[i], _ = strconv.Atoi(input.Text())
		}
		mergeSort()
 
		out.WriteString(strconv.Itoa(arr[0]))
		for i := 1; i < n; i++ {
			out.WriteByte(' ')
			out.WriteString(strconv.Itoa(arr[i]))
		}
		out.WriteByte('\n')
		out.Flush()
	}
}
 
func mergeSort() {
	left, right, mid := 0, 0, 0
	for step := 1; step < n; step <<= 1 {
		left = 0
		for left < n {
			mid = left + step - 1
			if mid+1 >= n {
				// 已经没有右侧了
				break
			}
			right = min(left+step<<1-1, n-1)
			merge(left, right, mid)
			left = right + 1
		}
	}
}
 
func merge(left, right, mid int) {
	i := 0
	l := left
	r := mid + 1
	for l <= mid && r <= right {
		if arr[r] < arr[l] {
			help[i] = arr[r]
			r++
		} else {
			help[i] = arr[l]
			l++
		}
		i++
	}
	for l <= mid {
		help[i] = arr[l]
		l++
		i++
	}
	for r <= right {
		help[i] = arr[r]
		r++
		i++
	}
	copy(arr[left:right+1], help[:right+1-left])
}
 
// 牛客 go 版本太低,没有内置的 min 函数
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

复杂度分析

  • 时间复杂度:
  • 空间复杂度:
    • merge 时,需要辅助数组

归并排序为什么比 的排序快?因为比较行为没有浪费!

  • 选择排序:每一轮比较,只获得了一个最小值
  • 冒泡排序:每一轮比较,只将一个最大值移到了正确位置
  • 插入排序:每一轮比较,只将一个值插到了正确位置(接下来的轮次没有用到这轮的比较结果,比较行为被浪费)
  • 归并排序:每一轮比较,都使得数组部分有序,而这有序部分又为 merge 提供了便利(每一轮的比较行为都没有被浪费)

注意

有些资料说可以用原地归并排序,把额外空间复杂度变成 ,不要浪费时间去学

因为原地归并排序确实可以省空间,但是会让复杂度变成

归并分治

利用归并排序的便利性可以解决很多问题——归并分治

备注

有关排序更多的概念、注意点、闭坑指南,将在后续课程继续