如何避免 append 函数修改底层数组?

作者站长头像
站长
· 阅读数 5

我使用 Go 语言编写数组所有组合的算法,例如:

输入 {1, 2, 3}

返回 {[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]}

算法代码如下,当输入数组元素少于 5 个时,能返回正确答案。但当使用 {1, 2, 3, 4, 5} 进行测试时,发现了一个我无法理解的问题。

package main

import "fmt"

func main() {
    res := subsets([]int{1, 2, 3, 4, 5})

    fmt.Println("---")
    for _, line := range res {
        fmt.Println(line)
    }

}

func subsets(nums []int) [][]int {
    res := [][]int{{}}
    n := len(nums)
    combine(0, n, &nums, []int{}, &res)

    return res
}

func combine(i, n int, nums *[]int, pre []int, res *[][]int) {
    for ; i < n; i++ {
        next := append(pre, (*nums)[i])
        fmt.Println(next)
        *res = append(*res, next)
        combine(i+1, n, nums, next, res)
    }
}

在第 25-27 行,我声明了一个 next []int 变量来保存即将加入到 res [][]int 数组里的值,然后将它打印出来。当算法之行结束后,我再检查 res 的值(9-11 行),却发现两次输出的结果不一致,输出如下:

[1]
[1 2]
[1 2 3]
[1 2 3 4]
[1 2 3 4 5]
[1 2 3 5]
[1 2 4]
[1 2 4 5]
[1 2 5]
[1 3]
[1 3 4]
[1 3 4 5]
[1 3 5]
[1 4]
[1 4 5]
[1 5]
[2]
[2 3]
[2 3 4]
[2 3 4 5]
[2 3 5]
[2 4]
[2 4 5]
[2 5]
[3]
[3 4]
[3 4 5]
[3 5]
[4]
[4 5]
[5]
---
[]
[1]
[1 2]
[1 2 3]
[1 2 3 5]
[1 2 3 4 5]
[1 2 3 5]
[1 2 4]
[1 2 4 5]
[1 2 5]
[1 3]
[1 3 4]
[1 3 4 5]
[1 3 5]
[1 4]
[1 4 5]
[1 5]
[2]
[2 3]
[2 3 4]
[2 3 4 5]
[2 3 5]
[2 4]
[2 4 5]
[2 5]
[3]
[3 4]
[3 4 5]
[3 5]
[4]
[4 5]
[5]

为了查看方便,我将两次结果对比:

如何避免 append 函数修改底层数组?

可以看到在第 4 行,两次输出结果不一致。我调试的时候发现,在插入 res 的第 6 个元素前,执行 next := append(pre, (*nums)[i]) 语句时,突然修改了第 4 个元素的值。go 语言中数组不是指传递吗,并且这里的 next 我也是新声明的数组,另外当输入数组长度低于 5 时不会出现这种情况。这是为什么呢?

回复
1个回答
avatar
test
2024-07-06

问题出在 append 函数上,如文档介绍的:

The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice...

只有当切片的底层数组有的容量不足时,才会分配新的底层数组。也就是说,使用 append 可能会修改原来切片的值,例如:

package main

import "fmt"

func main() {
    a := []int{1,2,3,4,5}
    
    fmt.Println(cap(a))
    
    b := append(a, 6)
    c := append(b[:2], 100)
    
    fmt.Println(a)
    fmt.Println(b)
    fmt.Println(c)
    
}

输出:

5
[1 2 3 4 5]
[1 2 100 4 5 6]
[1 2 100]

切片 b 的值在运行 c := append(b[:2], 100) 语句时被修改了,而 a 的值不受影响。

但在上例中,之所以出现底层数组被修改是因为手动将 b 进行了切片(b[:2]),所以追加元素才不会超过原来的长度。那么在问题算法的代码里并没有切片的操作,一直在切片的末尾追加元素,为什么也会出现类似的问题呢?

append 的长度追加

这是因为 Go 语言在使用 append 追加元素时不是逐个增加底层数组的长度的,而是以一定倍数递增:

package main

import "fmt"

func main() {
    a := []int{}
    for i := 0; i < 10; i++ {
        a = append(a, i)
        fmt.Println(cap(a))
    }
}

输出:

1
2
4
4
8
8
8
8
16
16

可以看到,前两次 append 操作只增加了 1 个容量,而当第三次时追加了 2 个容量,底层数组的长度来到了 4。当这个数组被用满时,再次执行 append 时直接申请了一个长度为 8 的数组。而下一次申请新数组时更是达到了 16。也就是说,虽说上例执行了 10 次 append,但只有 5 个底层数组。具体地,第 1、2 个切片分别占用一个底层数组,第 3、4 个切片共用一个长度为 4 的底层数组,第 5、6、7、8 个切片共用一个长度为 8 的数组,9、10 共用一个。

具体地,结合到问题中那个算法,插入切片 [1,2,3] 时生成的底层数组长度为 4,那么插入 [1,2,3,4] 切片时实际上和 [1,2,3] 共用了一个数组。当递归回溯到 [1,2,3] 后面要追加元素 5 时,由于 [1,2,3] 底层数组长度为 4,不需要申请新的空间,所以直接在元数组上修改,导致了 [1,2,3,4] 也修改为 [1,2,3,5]

解决方案

问题定位到了,解决方案就是强制为新切片分配一个新的底层数组:

next := make([]int, len(pre))
copy(next, pre)
next = append(next, (*nums)[i])
回复
likes
适合作为回答的
  • 经过验证的有效解决办法
  • 自己的经验指引,对解决问题有帮助
  • 遵循 Markdown 语法排版,代码语义正确
不该作为回答的
  • 询问内容细节或回复楼层
  • 与题目无关的内容
  • “赞”“顶”“同问”“看手册”“解决了没”等毫无意义的内容