缘由

一道 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
*/