
本文深入探讨了go语言归并排序merge函数实现中常见的引用陷阱。当直接使用切片(slice)的子切片作为左右子数组时,由于go切片的底层数组共享机制,可能导致在合并过程中数据被意外修改。文章将分析问题根源,并提供一种基于临时切片的健壮解决方案,以确保归并操作的正确性。
在Go语言中,切片(slice)是对底层数组的一个连续片段的引用。一个切片由三部分组成:指向底层数组的指针、切片的长度(length)和切片的容量(capacity)。这意味着当你通过 arr[p:q] 创建一个子切片时,它并没有复制原始数组的数据,而是创建了一个新的切片头部,这个头部仍然指向原始数组的相同部分。因此,对子切片或原始切片通过索引进行的修改,都会影响到共享的底层数组。
考虑以下不正确的Go语言Merge函数实现,它试图将两个已排序的子数组合并到一个更大的数组中:
func Merge(toSort *[]int, p, q, r int) {
arr := *toSort
L := arr[p:q] // L是arr[p]到arr[q-1]的视图
R := arr[q:r+1] // R是arr[q]到arr[r]的视图
// fmt.Println(L)
// fmt.Println(R)
i := 0
j := 0
for index := p; index <= r; index++ {
if i >= len(L) {
arr[index] = R[j] // 直接写入arr
j += 1
continue
} else if j >= len(R) {
arr[index] = L[i] // 直接写入arr
i += 1
continue
}
if L[i] > R[j] {
// fmt.Println("right smaller")
arr[index] = R[j] // 直接写入arr
j += 1
continue
}
if L[i] <= R[j] {
// fmt.Println("left smaller")
arr[index] = L[i] // 直接写入arr
i += 1
continue
}
}
}当 Merge 函数被调用时,L 和 R 实际上是 arr 切片的两个“窗口”或视图。问题在于,在合并循环中,我们直接将元素写回 arr[index]。如果 arr[index] 所在的内存区域同时也是 L 或 R 正在读取的区域(即 index 处于 p 到 r 之间,而 L 和 R 也覆盖了这部分区域),那么在 L 或 R 还没有完全读取完其所有元素之前,这些元素可能已经被 arr[index] = ... 操作所覆盖。
例如,假设原始数组 arr = [1, 7, 14, 15, 44, 65, 79, 2, 3, 6, 55, 70],我们希望合并 arr[0:7] (即 [1, 7, 14, 15, 44, 65, 79]) 和 arr[7:12] (即 [2, 3, 6, 55, 70])。 L 引用 arr 的前七个元素,R 引用 arr 的后五个元素。当 arr[index] 被写入时,如果 index 落在 L 或 R 所引用的原始内存区域内,L 或 R 接下来读取到的数据就可能不再是其原始值,而是已经被修改后的错误值。这导致了归并逻辑的混乱和最终结果的不正确,例如输出 [1 2 2 2 2 2 2 2 3 6 55 70]。
为了避免这种“就地修改”导致的并发读写冲突,最直接且推荐的方法是确保在合并过程中,L 和 R 始终读取的是未被修改的原始数据。这可以通过创建 L 和 R 的独立副本,或者将合并结果写入一个全新的临时切片来实现。
立即学习“go语言免费学习笔记(深入)”;
此方案在合并前,先将左右子数组的数据复制到新的、独立的切片中。
func MergeCorrected(arr []int, p, q, r int) {
// 1. 创建L和R的独立副本
// 注意:Go切片作为函数参数时,本身就是引用传递(传递的是切片头部的值拷贝,但底层数组是共享的)。
// 所以这里的arr不需要指针,直接传递即可。
// 但是L和R需要是独立的数据副本,而不是arr的视图。
// 复制左半部分到L
leftLen := q - p
L := make([]int, leftLen)
copy(L, arr[p:q])
// 复制右半部分到R
rightLen := r - q + 1 // 注意这里R的长度计算
R := make([]int, rightLen)
copy(R, arr[q:r+1])
i := 0 // L的当前索引
j := 0 // R的当前索引
k := p // arr的当前写入索引
// 2. 将L和R中的元素合并到原始arr中
for i < len(L) && j < len(R) {
if L[i] <= R[j] {
arr[k] = L[i]
i++
} else {
arr[k] = R[j]
j++
}
k++
}
// 3. 将剩余的L元素复制到arr
for i < len(L) {
arr[k] = L[i]
i++
k++
}
// 4. 将剩余的R元素复制到arr
for j < len(R) {
arr[k] = R[j]
j++
k++
}
}代码解析:
另一种同样有效的策略是,将合并后的结果先写入一个全新的临时切片,最后再将这个临时切片的内容复制回原始切片中对应的范围。
func MergeWithTempResult(arr []int, p, q, r int) {
// 复制左半部分和右半部分到独立的临时切片
left := make([]int, q-p)
copy(left, arr[p:q])
right := make([]int, r-q+1)
copy(right, arr[q:r+1])
merged := make([]int, r-p+1) // 创建一个临时切片存储合并结果
i, j, k := 0, 0, 0 // i for left, j for right, k for merged
for i < len(left) && j < len(right) {
if left[i] <= right[j] {
merged[k] = left[i]
i++
} else {
merged[k] = right[j]
j++
}
k++
}
for i < len(left) {
merged[k] = left[i以上就是Go语言归并排序Merge函数中的Slice引用陷阱与解决方案的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号