
本教程详细探讨了go语言中归并排序(merge sort)的实现,重点解决在使用索引进行递归划分时常见的栈溢出问题。文章将解释错误的中间点(mid)计算如何导致无限递归,并提供两种正确的实现策略:基于索引的修正方法和通过切片操作创建子数组的方法,旨在帮助开发者构建高效且健壮的归并排序算法。
归并排序(Merge Sort)是一种高效、稳定的基于分治思想的排序算法。其核心思想是将一个大数组递归地分成两个较小的子数组,直到子数组只包含一个元素(自然有序),然后将这些有序的子数组两两合并,最终形成一个完全有序的数组。归并排序的时间复杂度为O(n log n),空间复杂度为O(n)。
在实现归并排序的递归部分时,一个常见的错误是在处理数组的某个子区间(由 first 和 last 索引定义)时,错误地计算了中间点 mid。原始问题中出现的栈溢出,正是由于以下代码片段:
func MergeSort(slice []int, first, last int) {
if len(slice) < 2 { // 这里的判断条件可能不适用于带有first, last的场景
return
}
if first < last {
mid := len(slice) / 2 // 错误:这里应该基于 first 和 last 计算
MergeSort(slice, first, mid)
MergeSort(slice, mid+1, last)
Merge(slice, first, mid, last)
}
}错误分析:
当 MergeSort 函数被调用并传入 first 和 last 参数时,它期望对 slice[first:last+1] 这个子区间进行排序。然而,mid := len(slice) / 2 这行代码始终计算的是整个原始切片的中间索引,而不是当前正在处理的子区间 slice[first:last+1] 的中间索引。
立即学习“go语言免费学习笔记(深入)”;
例如,如果 slice 长度为10,first=0, last=9。第一次调用 MergeSort(slice, 0, 9) 时,mid 会被计算为 10 / 2 = 5。然后递归调用 MergeSort(slice, 0, 5) 和 MergeSort(slice, 6, 9)。
问题出在后续的递归调用中。假设我们正在处理 MergeSort(slice, 0, 5)。此时 first=0, last=5。但 mid 仍然会被计算为 len(slice) / 2 = 5。这意味着 MergeSort(slice, 0, 5) 会再次调用 MergeSort(slice, 0, 5) 和 MergeSort(slice, 6, 5)。
这种无限递归导致函数调用栈不断增长,最终超出Go运行时为goroutine分配的栈空间限制,从而引发 runtime: goroutine stack exceeds ...-byte limit 的致命错误。
要解决上述问题,关键在于正确计算 mid 索引,使其反映当前正在处理的子区间的中间位置。
1. MergeSort 函数修正
mid 应该通过 first 和 last 计算,公式为 mid = first + (last - first) / 2 或 mid = (first + last) / 2。前者在 first 和 last 都非常大时可以避免整数溢出,但对于Go的 int 类型通常不是问题。
import "math"
// MergeSort 对 slice[first...last] 区间进行归并排序
func MergeSort(slice []int, first, last int) {
// 递归终止条件:当子区间只包含一个或零个元素时,视为有序
if first >= last {
return
}
// 正确计算中间索引
// mid = (first + last) / 2 也可以,但此写法更健壮,避免潜在溢出
mid := first + (last - first) / 2
// 递归排序左半部分
MergeSort(slice, first, mid)
// 递归排序右半部分
MergeSort(slice, mid+1, last)
// 合并已排序的左右两部分
Merge(slice, first, mid, last)
}2. Merge 函数实现
Merge 函数负责将两个有序的子数组 slice[first...mid] 和 slice[mid+1...last] 合并成一个有序的数组。为了实现这一目标,通常会使用辅助数组。
// Merge 合并 slice[first...mid] 和 slice[mid+1...last] 两个有序子数组
func Merge(slice []int, first, mid, last int) {
n1 := mid - first + 1 // 左子数组的长度
n2 := last - mid // 右子数组的长度
// 创建临时数组 L 和 R
// Go切片是动态数组,可以直接创建所需大小
L := make([]int, n1+1) // +1 用于哨兵值
R := make([]int, n2+1) // +1 用于哨兵值
// 填充 L 数组
for i := 0; i < n1; i++ {
L[i] = slice[first+i]
}
// 填充 R 数组
for j := 0; j < n2; j++ {
R[j] = slice[mid+1+j]
}
// 设置哨兵值,简化合并逻辑
L[n1] = math.MaxInt // 使用 Go 的最大整数值作为“无限大”
R[n2] = math.MaxInt
i, j := 0, 0 // L 和 R 数组的当前索引
// 遍历原始数组的合并区间,将 L 和 R 中较小的元素放入
for k := first; k <= last; k++ {
if L[i] <= R[j] {
slice[k] = L[i]
i++
} else {
slice[k] = R[j]
j++
}
}
}完整代码示例 (基于索引)
package main
import (
"fmt"
"math"
)
// MergeSort 对 slice[first...last] 区间进行归并排序
func MergeSort(slice []int, first, last int) {
// 递归终止条件:当子区间只包含一个或零个元素时,视为有序
if first >= last {
return
}
// 正确计算中间索引
mid := first + (last - first) / 2
// 递归排序左半部分
MergeSort(slice, first, mid)
// 递归排序右半部分
MergeSort(slice, mid+1, last)
// 合并已排序的左右两部分
Merge(slice, first, mid, last)
}
// Merge 合并 slice[first...mid] 和 slice[mid+1...last] 两个有序子数组
func Merge(slice []int, first, mid, last int) {
n1 := mid - first + 1 // 左子数组的长度
n2 := last - mid // 右子数组的长度
// 创建临时数组 L 和 R
L := make([]int, n1+1) // +1 用于哨兵值
R := make([]int, n2+1) // +1 用于哨兵值
// 填充 L 数组
for i := 0; i < n1; i++ {
L[i] = slice[first+i]
}
// 填充 R 数组
for j := 0; j < n2; j++ {
R[j] = slice[mid+1+j]
}
// 设置哨兵值,简化合并逻辑
L[n1] = math.MaxInt // 使用 Go 的最大整数值作为“无限大”
R[n2] = math.MaxInt
i, j := 0, 0 // L 和 R 数组的当前索引
// 遍历原始数组的合并区间,将 L 和 R 中较小的元素放入
for k := first; k <= last; k++ {
if L[i] <= R[j] {
slice[k] = L[i]
i++
} else {
slice[k] = R[j]
j++
}
}
}
func main() {
arr := []int{9, -13, 4, -2, 3, 1, -10, 21, 12}
fmt.Println("原始数组:", arr)
// 调用归并排序,传入整个数组的索引范围
MergeSort(arr, 0, len(arr)-1)
fmt.Println("排序后数组:", arr) // 期望输出:[-13 -10 -2 1 3 4 9 12 21]
arr2 := []int{5, 2, 4, 7, 1, 3, 2, 6}
fmt.Println("原始数组2:", arr2)
MergeSort(arr2, 0, len(arr2)-1)
fmt.Println("排序后数组2:", arr2) // 期望输出:[1 2 2 3 4 5 6 7]
}另一种实现归并排序的方式是避免传递 first 和 last 索引,而是直接通过Go的切片操作来创建子切片。这种方法在代码上可能更简洁,因为它隐藏了索引管理的复杂性,但需要注意每次切片操作可能会导致新的底层数组分配(取决于Go运行时优化),从而增加内存开销。
package main
import (
"fmt"
)
// MergeSortWithSlice 对传入的切片进行归并排序
func MergeSortWithSlice(items []int) []int {
// 递归终止条件:切片长度小于2时,视为有序,直接返回
if len(items) < 2 {
return items
}
// 计算中间点
mid := len(items) / 2
// 递归排序左右子切片
left := MergeSortWithSlice(items[0:mid])
right := MergeSortWithSlice(items[mid:])
// 合并已排序的左右子切片
return MergeWithSlice(left, right)
}
// MergeWithSlice 合并两个有序切片
func MergeWithSlice(left, right []int) []int {
size, i, j := len(left)+len(right), 0, 0
merged := make([]int, size) // 创建新的切片来存放合并结果
for k := 0; k < size; k++ {
if i < len(left) && (j >= len(right) || left[i] < right[j]) {
merged[k] = left[i]
i++
} else {
merged[k] = right[j]
j++
}
}
return merged
}
func main() {
arr := []int{9, -13, 4, -2, 3, 1, -10, 21, 12}
fmt.Println("原始数组:", arr)
sortedArr := MergeSortWithSlice(arr)
fmt.Println("排序后数组 (切片版):", sortedArr)
arr2 := []int{5, 2, 4, 7, 1, 3, 2, 6}
fmt.Println("原始数组2:", arr2)
sortedArr2 := MergeSortWithSlice(arr2)
fmt.Println("排序后数组2 (切片版):", sortedArr2)
}注意事项:
归并排序是一种强大且广泛使用的排序算法。在Go语言中实现时,理解其分治思想并正确处理递归逻辑至关重要。本文通过分析一个常见的栈溢出问题,强调了在基于索引的递归算法中,正确计算子区间的中间索引是避免无限递归和栈溢出的关键。同时,我们提供了两种实现策略:一种是基于索引的修正方法,它在性能和内存效率上通常表现良好;另一种是基于Go切片操作的简化方法,它在代码简洁性上有所优势。开发者应根据具体需求和性能考量选择最合适的实现方式。
以上就是Go语言归并排序深度解析:避免栈溢出的正确实现与优化的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号