
本教程详细探讨了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] 的中间索引。
例如,如果 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 函数修正
Zyro AI Background Remover
Zyro推出的AI图片背景移除工具
145
查看详情
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语言归并排序深度解析:避免栈溢出的正确实现与优化的详细内容,更多请关注其它相关文章!
# 两种
# 新网站建设素材
# 吴忠网站seo推广费用
# 贴吧营销推广怎么做的快
# 网站SEO做手机网站好优化吗
# 蓬莱seo免费优化
# 网站优化课程故事ppt
# 辽宁网站设计推广
# 保定网站推广托管
# 建设个人你网站
# 常州个人网站建设活动公示
# 它在
# 因为它
# go
# 并已
# 是一种
# 是在
# 较小
# 的是
# 遍历
# 递归
# 排序算法
# ai
# 栈
# go语言
相关文章:
响应式CSS Grid布局:优化网格项在小屏幕下的堆叠与宽度适配
c++如何使用chrono库处理时间_c++标准库时间与日期操作
QQ邮箱在线使用入口 QQ邮箱个人账号网页版登录
Python字典中优雅地迭代剩余元素的方法
在J*a中如何使用Exception包装底层异常_异常包装与信息传递方法说明
BetterDiscord插件中安全更新用户简介的实践指南
C++ string find函数返回值npos详解_C++字符串查找失败的判断条件
优化Django表单:提交验证失败后保留用户输入
汽水音乐车机版8.9下载 汽水音乐车机版8.9版本安装入口
PHP实现即时文章发布与单次数据库写入:自提交模式教程
ACG动漫视频网入口 ACG动漫*免费正版观看地址
html网页设计源代码怎么运行_运行html网页设计源代码步骤【指南】
J*aScript DOM操作:高效清空列表元素的策略与实践
CSS实现侧边栏导航项全宽圆角悬停背景效果
Golang如何通过reflect获取匿名字段方法_Golang reflect匿名字段方法访问技巧
C++如何比较两个字符串_C++ string compare函数与操作符对比
在Google App Engine Go中实现独立模块代码库与灵活路由
漫蛙网页登录入口 漫蛙漫画官方授权网址
12306选座怎么选到特殊座位_12306特殊座位选择注意事项
如何创建没有密码的Windows本地账户_跳过微软账户登录的技巧【教程】
Win11怎么关闭快速启动_Win11彻底关机设置教程
Lar*el 递归关系中排除指定分支的教程
J*a TimerTask中HashMap意外清空的深层原因与解决方案
J*a应用集成GitHub CLI与API认证指南
解决Python单元测试中Mock异常方法调用计数为零的问题
PS5 Pro有点优势但不多! 《燕云十六声》PS5平台与PC性能画面对比
不会效仿卡普空!《铁拳》制作人澄清:不采取赛事付费|直播|
c++如何使用TBB库进行任务并行_c++ Intel线程构建模块
微信客户端如何收红包_微信客户端接收红包使用教程
Archive of Our Own官网直达 AO3最新可用地址一览
Yandex搜索引擎官方地址 俄罗斯网络世界的主要入口
学习通网页版快速入口 学习通官网网页版直接打开
Mac怎么查看崩溃日志_Mac控制台错误报告分析
必由学登录入口 必由学官方网站在线访问链接
Python实时数据流中的动态最值查找策略
优化Log4j2控制台输出性能:解决异步日志瓶颈
探索高级语言到C/C++的转译路径:以Go为例及内存管理策略
“音游” × “怪文书” 题材的节奏冒险游戏 《晕晕电波症候群》确定于2026年4月发售!
离线运行Go语言之旅:本地部署与GOPATH配置指南
京东京造J1和网易云音乐氧气真无线有什么不同_国产电商蓝牙耳机音质对比
现代化 SciPy 一维插值:interp1d 的替代方案与最佳实践
TikTok搜索结果不显示如何解决 TikTok搜索刷新优化方法
c++如何实现单例设计模式_c++线程安全的单例模式写法
护手霜蹭到袖口上了如何清洗? 怎样避免留下一圈油印?
电脑屏幕颜色不舒服怎么办_Windows夜间模式与色彩校准教程【护眼技巧】
Win10怎么制作U盘启动盘 Win10系统安装U盘制作教程【详解】
在J*a中如何开发简易电子商务商品管理系统_商品管理系统项目实战解析
在J*a中如何使用BigDecimal进行高精度计算_BigDecimal类应用指南
蛙漫安全无毒 官方认证的绿色入口
千牛数据看板网页版_千牛数据看板网页版访问方法
*请认真填写需求信息,我们会在24小时内与您取得联系。