golang slice | 理解 go 切片
技術標籤:golangleetcode資料結構go語言指標二叉樹
緣由
一道 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
}
這種遞迴的呼叫方式將導致大量的中間變數pathNums
的存在,遞迴至某一節點時的pathNums
變數僅是為了能復原現場。
如果在DFS
中試圖省略newPath
,則當某次對變數pathNums
的append
操作未導致其擴容(改變底層陣列)的話,在接下來的遞迴操作中其實使用的還是原切片地址;相應的在其左子樹的append
操作將會影響到原切片,也會影響到接下來的右子樹中。
解決上述的問題有兩個辦法,在每次的遞迴呼叫中手動生成新中間切片,並且拷貝其形參元素,以解決指標帶來的影響。
二,就是下面的官方解答當中用到的技巧,及時對切片縮容。(在官方解中改變了些變數的命名)
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
*/