缘由
一道 leetcode 题目路径总和 II,引发了我对 slice 的思考,题目注解如下:
给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
[][]int
下面先放上贴自己的解:
type TreeNode struct {
Val int
Left, Right *TreeNode
}
// pathSum 总的思路还是走 dfs 递归遍历并存储每次判断相等的路径切片
func pathSum(root *TreeNode, targetSum int) [][]int {
// 返回值结果
res := make([][]int, 0)
var DFS func(*TreeNode, int, []int)
// DFS 仅在某叶子节点判断符合后进行结果保存
DFS = func(curNode *TreeNode, curSum int, pathNums []int) {
if curNode == nil {
return
}
// 如果此处不生成新的切片(该切片指其指针指向新一块内存)则遍历过程中将导致紊乱
newPath := make([]int, 0)
newPath = append(newPath, pathNums...)
newPath = append(newPath, curNode.Val)
// 如果是叶子节点
if curNode.Left == nil && curNode.Right == nil {
if curSum == curNode.Val {
res = append(res, newPath)
}
return
}
// 递归调用左右子树
DFS(curNode.Left, curSum-curNode.Val, newPath)
DFS(curNode.Right, curSum-curNode.Val, newPath)
}
DFS(root, targetSum, make([]int, 0))
return res
}
pathNumspathNums
DFSnewPathpathNumsappendappend
解决上述的问题有两个办法,在每次的递归调用中手动生成新中间切片,并且拷贝其形参元素,以解决指针带来的影响。
二,就是下面的官方解答当中用到的技巧,及时对切片缩容。(在官方解中改变了些变量的命名)
func pathSumOfficial(root *TreeNode, targetSum int) [][]int {
res := make([][]int, 0)
// curPathNum 记录当前遍历路径时的结果
curPathNum := make([]int, 0)
// DFS 同样是深度优先遍历,不同是在切片的应用上
var DFS func(*TreeNode, int)
DFS = func(curNode *TreeNode, curNumLeft int) {
if curNode == nil {
return
}
// 路径判断时同样传递的是剩余值
curNumLeft -= curNode.Val
curPathNum = append(curPathNum, curNode.Val)
// 对切片进行缩容
defer func() {
curPathNum = curPathNum[:len(curPathNum)-1]
}()
if curNode.Left == nil && curNode.Right == nil && curNumLeft == 0 {
res = append(res, append(make([]int, 0), curPathNum...))
return
}
DFS(curNode.Left, curNumLeft)
DFS(curNode.Right, curNumLeft)
}
DFS(root, targetSum)
return res
}
从来自 go blog 的图片可以看出,切片的长度 len 就可以理解为一个窗口,决定了底层数组的可见性。
况且在本题目用以存储二叉树节点的值,又完全能被一个底层数组,扩缩调节可见性来存储。其切片头部在这里reflect.SliceHeader:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
传参
接下来还要注意这里 blog.golang.org/slices,其中有写到:
It’s important to understand that even though a slice contains a pointer, it is itself a value. Under the covers, it is a struct value holding a pointer and a length. It is not a pointer to a struct.
尽管 slice 结构中包括一个指针,但它本身却还是一个值。slice 是一个包含有指针和长度的 struct 值而非指针本身。这很重要,当我们在函数形参中传递一个切片时,仅其内部的指针使我们可以改写底层数组的值,却无法改写其 len 属性值。因为在调用函数当中所得到的也仅是一个拷贝而已,尽管其中包含有两个指向了同一处的指针。
如下例程所:
func AddOneToEachElement(slice []byte) {
for i := range slice {
slice[i]++
}
}
func AddOneToEachElementAndDoubleIt(slice []byte) {
for i := range slice {
slice[i]++
}
slice = append(slice, slice...)
}
func main() {
s1 := []byte{1,2,3,4,5,6,7}
fmt.Println(s1)
AddOneToEachElement(s1)
fmt.Println(s1, len(s1))
AddOneToEachElementAndDoubleIt(s1)
fmt.Println(s1, len(s1))
}
/* output:
[1 2 3 4 5 6 7]
[2 3 4 5 6 7 8] 7
[3 4 5 6 7 8 9] 7
*/
我们所通过形参能改变的,仅是 SliceHeader struct 当中被指针所指的底层数组的值而已,尽管在调用函数当中这是一份拷贝,他们所指向的也是同一块内存。
想要修改切片 len 等信息,只能通过传递指针的方式,或是编写作用在其上的方法。
func (p *ptrByteSlice) SubtractOneFromLength() {
*p = (*p)[:len(*p)-1]
}
func PtrSliceDouble(slicePtr *[]byte) {
*slicePtr = append((*slicePtr), (*slicePtr)...)
}
type ptrByteSlice []byte
func (p *ptrByteSlice) SubtractHalf() {
*p = (*p)[:len(*p)/2]
}
func main() {
s1 := []byte{1, 2, 3, 4, 5, 6, 7}
fmt.Println(s1, len(s1), cap(s1))
PtrSliceDouble(&s1)
fmt.Println(s1, len(s1), cap(s1))
s2 := ptrByteSlice(s1)
s2.SubtractHalf()
fmt.Println(s2, len(s2), cap(s2))
}
/* output:
[1 2 3 4 5 6 7] 7 7
[1 2 3 4 5 6 7 1 2 3 4 5 6 7] 14 16
[1 2 3 4 5 6 7] 7 16
*/