目录
前言
在实际场景中,选择合适的排序算法对于提高程序的效率和性能至关重要,本节课主要讲解"归并排序"的适用场景及代码实现。
归并排序
归并排序(Merge Sort)是一种分而治之的排序算法。它将一个大列表分成两个小列表,分别对这两个小列表进行排序,然后将排序好的小列表合并成一个最终的排序列表。归并排序的关键在于合并(Merge)过程,它确保了在合并的过程中,两个已排序的序列被合并成一个新的、有序的序列。
代码示例
下面我们使用Go语言实现一个归并排序
1. 算法包
创建一个 pkg/algorithm.go
touch pkg/algorithm.go
(如果看过上节课的堆排序,则已存在该文件,我们就不需要再创建了)
2. 归并排序代码
打开 pkg/algorithm.go 文件,代码如下
从小到大 排序
package pkg
// BubbleSort 冒泡排序
...
// SelectionSort 选择排序
...
// InsertionSort 插入排序
...
// QuickSort 快速排序
...
// partition 分区操作
...
// HeapSort 堆排序
...
// heapify 将以 i 为根的子树调整为最大堆
...
// MergeSort 归并排序
func MergeSort(arr []int) []int {
if len(arr) <= 1 {
return arr
}
// 找到中点,分割数组
mid := len(arr) / 2
left := MergeSort(arr[:mid])
right := MergeSort(arr[mid:])
// 合并两个已排序的切片
return merge(left, right)
}
// merge 函数用于合并两个已排序的切片
func merge(left, right []int) []int {
var result []int
i, j := 0, 0
for i < len(left) && j < len(right) {
if left[i] < right[j] {
result = append(result, left[i])
i++
} else {
result = append(result, right[j])
j++
}
}
// 如果左侧有剩余,则追加到结果切片
result = append(result, left[i:]...)
// 如果右侧有剩余,则追加到结果切片
result = append(result, right[j:]...)
return result
}
3. 模拟程序
打开 main.go 文件,代码如下:
package main
import (
"demo/pkg"
"fmt"
)
func main() {
// 定义一个切片,这里我们模拟 10 个元素
arr := []int{98081, 27887, 31847, 84059, 2081, 41318, 54425, 22540, 40456, 3300}
fmt.Println("arr 的长度:", len(arr))
fmt.Println("Original data:", arr) // 先打印原始数据
newArr := pkg.MergeSort(arr) // 调用归并排序
fmt.Println("New data: ", newArr) // 后打印排序后的数据
}
4. 运行程序
go run main.go
能发现, Original data 后打印的数据,正是我们代码中定义的切片数据,顺序也是一致的。
New Data 后打印的数据,则是经过归并排序后的数据,是从小到大的。
5. 从大到小排序
如果需要 从大到小 排序也是可以的,在代码里,需要将两个 if 判断比较的 符号 进行修改。
修改 pkg/algorithm.go 文件:
package pkg
// BubbleSort 冒泡排序
...
// SelectionSort 选择排序
...
// InsertionSort 插入排序
...
// QuickSort 快速排序
...
// partition 分区操作
...
// HeapSort 堆排序
...
// heapify 将以 i 为根的子树调整为最大堆
...
// MergeSort 归并排序
func MergeSort(arr []int) []int {
if len(arr) <= 1 {
return arr
}
// 找到中点,分割数组
mid := len(arr) / 2
left := MergeSort(arr[:mid])
right := MergeSort(arr[mid:])
// 合并两个已排序的切片
return merge(left, right)
}
// merge 函数用于合并两个已排序的切片
func merge(left, right []int) []int {
var result []int
i, j := 0, 0
for i < len(left) && j < len(right) {
if left[i] > right[j] {
result = append(result, left[i])
i++
} else {
result = append(result, right[j])
j++
}
}
// 如果左侧有剩余,则追加到结果切片
result = append(result, left[i:]...)
// 如果右侧有剩余,则追加到结果切片
result = append(result, right[j:]...)
return result
}
只需要一丁点的代码即可
从 package pkg 算第一行,上面示例中在第四十五行代码,我们将 "<" 改成了 ">" ,这样就变成了 从大到小排序了
归并排序主要操作
主要操作包括 分割 和 合并
1. 合并
合并操作由 merge 函数实现,它接收两个已排序的切片 left 和 right,并返回一个新的、包含两个切片所有元素且已排序的切片。
- 初始化:首先,创建一个空的切片 result 用于存储合并后的结果。同时,使用两个索引 i 和 j 分别指向 left 和 right 的起始位置
- 比较与合并:然后,使用一个循环,比较 left[i] 和 right[j] 的大小。将较小的元素追加到 result 中,并移动相应的索引。这个过程一直持续到任一切片中的所有元素都被添加到 result 中
- 追加剩余元素:如果 left 和 right 中还有剩余的元素(即某个切片的索引没有遍历完),则直接将剩余的元素追加到 result 的末尾。这是因为在循环结束时,剩余的元素一定是已排序的(它们来自原始的已排序切片)
2. 分割(Divide)与递归排序(Conquer)
分割与递归排序操作由 mergeSort 函数实现。
- 基本情况:如果输入的切片 arr 的长度小于或等于 1,则不需要排序,直接返回该切片。因为单个元素或空切片都可以被认为是已排序的
- 分割:找到切片的中点 mid,将切片分为两部分:arr[:mid] 和 arr[mid:]
- 递归排序:对着两部分分别调用 mergeSort 函数进行递归排序。这会将问题分解成更小的子问题,直到子问题小到满足基本情况
- 合并:最后,使用 merge 函数将这两个递归排序后的切片合并成一个有序的切片,并返回该切片
总体思想
归并排序通过递归地将数组分解成越来越小的半子表,对半子表排序,然后再将排好序的半子表合并成有序的表来工作。这个过程需要额外的存储空间来存放合并后的数组,因此其空间复杂度为 O(n)。然而,归并排序的时间复杂度是稳定的 O(n log n),并且由于其分治特性,它在实际应用中非常有效,尤其是在处理大数据集时。
循环次数测试
参照上面示例进行测试(因考虑到每次手动输入 10 条、20 条、30 条数据太繁琐,所以我写了一个函数,帮助我自动生成 0到100000 的随机整数)
假如 10 条数据进行排序
总计循环了 32 次
假如 20 条数据进行排序
总计循环了 84 次
假如 30 条数据进行排序
总计循环了 137 次
假设 5000 条数据,对比 冒泡、选择、插入、快速、堆
- 冒泡排序:循环次数 12,502,499 次
- 选择排序:循环次数 12,502,499 次
- 插入排序:循环次数 6,323,958 次
- 快速排序:循环次数 74,236 次
- 堆排序:循环次数 59,589 次
- 归并排序:循环次数 60,288 次
归并排序的适用场景
归并排序在以下场景表现良好
1. 大数据集
对于非常大的数据集,归并排序通常比快速排序或插入排序更有效,因为归并排序的时间复杂度是 O(n log n),并且它的性能相对稳定,不会因数据集的不同而大幅度变化
2. 链表排序
由于归并排序在合并过程中不需要额外的空间(除了递归栈),所以在链表排序时非常高效。链表数据结构的特性使得分割和合并操作相对简单
3. 外部排序
当数据集太大,无法全部加载到内存时,可以使用归并排序的外部版本。在这个版本中,数据被分割成多个块,每块单独排序后存储在磁盘上,然后通过归并操作将它们合并成一个有序的文件
4. 稳定性需求
归并排序是稳定的排序算法,这意味着相等的元素在排序后仍然保持原来的顺序。这在需要保持元素原始顺序的某些应用中非常有用
尽管归并排序在很多场景下都很有用,但它也有缺点,主要是需要额外的空间 O(n) 来存储临时数组。这在内存受限的情况下可能是一个问题。