1. 程式人生 > 其它 >golang slice | 理解 go 切片

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,則當某次對變數pathNumsappend操作未導致其擴容(改變底層陣列)的話,在接下來的遞迴操作中其實使用的還是原切片地址;相應的在其左子樹的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 就可以理解為一個視窗,決定了底層陣列的可見性。blog.golang.org
況且在本題目用以儲存二叉樹節點的值,又完全能被一個底層陣列,擴縮調節可見性來儲存。其切片頭部在這裡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
*/