Go語(yǔ)言中切片操作的那些技巧
本文翻譯自官方wiki,整理了Go語(yǔ)言中關(guān)于切片操作的一些技巧。
備注:由于行文需要,一些細(xì)節(jié)與原文存在些許出入。
切片操作常用技巧
復(fù)制
將切片a中的元素復(fù)制到切片b中。
最簡(jiǎn)單的、最常用的方法就是使用內(nèi)置的copy函數(shù)。
b = make([]T, len(a)) // 一次將內(nèi)存申請(qǐng)到位
copy(b, a)
除了使用內(nèi)置的copy函數(shù)外,還有下面兩種使用append函數(shù)復(fù)制切片的方法。
b = append([]T(nil), a...)
b = append(a[:0:0], a...)
這兩種方法通常比使用copy函數(shù)復(fù)制的方法要慢一些,但是如果在復(fù)制之后有更多的元素要添加到b中,那么它們的效率會(huì)更高。
剪切
將切片a中索引i~j位置的元素剪切掉。
可以按照下面的方式,使用append函數(shù)完成。
a = append(a[:i], a[j:]...)
刪除
將切片a中索引位置為i的元素刪除。
同樣可以按照上面剪切的方式使用append函數(shù)完成刪除操作。
a = append(a[:i], a[i+1:]...)
或者搭配copy函數(shù)使用切片表達(dá)式完成刪除操作。
a = a[:i+copy(a[i:], a[i+1:])]
此外,如果只需要?jiǎng)h除掉索引為i的元素,無需保留切片元素原有的順序,那么還可以使用下面這種簡(jiǎn)單的方式進(jìn)行刪除。
a[i] = a[len(a)-1] // 將最后一個(gè)元素移到索引i處
a = a[:len(a)-1] // 截掉最后一個(gè)元素
剪切或刪除操作可能引起的內(nèi)存泄露
需要特別注意的是。如果切片a中的元素是一個(gè)指針類型或包含指針字段的結(jié)構(gòu)體類型(需要被垃圾回收),上面剪切和刪除的示例代碼會(huì)存在一個(gè)潛在的內(nèi)存泄漏問題:一些具有值的元素仍被切片a引用,因此無法被垃圾回收機(jī)制回收掉。下面的代碼可以解決這個(gè)問題。
剪切
copy(a[i:], a[j:])
for k, n := len(a)-j+i, len(a); k < n; k++ {
a[k] = nil // 或類型T的零值
}
a = a[:len(a)-j+i]
刪除
copy(a[i:], a[i+1:])
a[len(a)-1] = nil // 或類型T的零值
a = a[:len(a)-1]
刪除但不保留元素原有順序
a[i] = a[len(a)-1]
a[len(a)-1] = nil
a = a[:len(a)-1]
內(nèi)部擴(kuò)張
在切片a的索引i之后擴(kuò)張j個(gè)元素。
使用兩個(gè)append函數(shù)完成,即先將索引i之后的元素追加到一個(gè)長(zhǎng)度為j的切片后,再將這個(gè)切片中的所有元素追加到切片a的索引i之后。
a = append(a[:i], append(make([]T, j), a[i:]...)...)
擴(kuò)張的這一部分元素為T類型的零值。
尾部擴(kuò)張
將切片a的尾部擴(kuò)張j個(gè)元素的空間。
a = append(a, make([]T, j)...)
擴(kuò)張的這一部分元素同樣為T類型的零值。
過濾
按照一定的規(guī)則將切片a中的元素進(jìn)行就地過濾。
這里假設(shè)過濾的條件已封裝為keep函數(shù),使用for range遍歷切片a的所有元素逐一調(diào)用keep函數(shù)進(jìn)行過濾。
n := 0
for _, x := range a {
if keep(x) {
a[n] = x // 保留該元素
n++
}
}
a = a[:n] // 截取切片中需保留的元素
插入
將元素x插入切片a的索引i處。
還是使用兩個(gè)append函數(shù)完成插入x的操作。
a = append(a[:i], append([]T{x}, a[i:]...)...)
第二個(gè)append函數(shù)創(chuàng)建了一個(gè)具有自己底層數(shù)組的新切片,并將a[i:]中的元素復(fù)制到該切片,然后由第一個(gè)append函數(shù)將這些元素復(fù)制回切片a。
我們可以通過使用另一種方法來避免新切片的創(chuàng)建(以及由此產(chǎn)生的內(nèi)存垃圾)和第二個(gè)副本:
a = append(a, 0 /* 這里應(yīng)使用元素類型的零值 */)
copy(a[i+1:], a[i:])
a[i] = x
追加
將元素x追加到切片a的最后。
這里使用append函數(shù)即可。
a = append(a, x)
彈出
將切片a的最后一個(gè)元素彈出。
這里使用切片表達(dá)式完成彈出操作。
x, a = a[len(a)-1], a[:len(a)-1]
彈出切片a的第一個(gè)元素。
x, a = a[0], a[1:]
前插
將元素x前插到切片a的開始。
a = append([]T{x}, a...)
其他技巧
過濾而不分配
此技巧使用了一個(gè)事實(shí),即切片b與原始切片a共享相同的底層數(shù)組和容量,因此存儲(chǔ)已重新用于過濾的切片。當(dāng)然,原始內(nèi)容被修改了。
b := a[:0]
for _, x := range a {
if f(x) {
b = append(b, x)
}
}
對(duì)于必須被垃圾回收的元素,在完成上述操作后可以添加以下代碼:
for i := len(b); i < len(a); i++ {
a[i] = nil // 或T類型的零值
}
翻轉(zhuǎn)
將切片a的元素順序翻轉(zhuǎn)。
通過迭代兩兩互換元素完成。
for i := len(a)/2-1; i >= 0; i-- {
opp := len(a)-1-i
a[i], a[opp] = a[opp], a[i]
}
同樣的操作:
for left, right := 0, len(a)-1; left < right; left, right = left+1, right-1 {
a[left], a[right] = a[right], a[left]
}
打亂順序
打亂切片a中元素的順序。
Fisher–Yates算法:
for i := len(a) - 1; i > 0; i-- {
j := rand.Intn(i + 1)
a[i], a[j] = a[j], a[i]
}
從go1.10開始,可以使用math/rand.Shuffle。
rand.Shuffle(len(a), func(i, j int) {
a[i], a[j] = a[j], a[i]
})
使用最小分配進(jìn)行批處理
如果你想對(duì)一個(gè)大型切片a的元素分批進(jìn)行處理,這會(huì)很有用。
actions := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
batchSize := 3
batches := make([][]int, 0, (len(actions) + batchSize - 1) / batchSize)
for batchSize < len(actions) {
actions, batches = actions[batchSize:], append(batches, actions[0:batchSize:batchSize])
}
batches = append(batches, actions)
得到的效果如下:
[[0 1 2] [3 4 5] [6 7 8] [9]]
就地刪除重復(fù)元素(元素可比較)
import "sort"
in := []int{3,2,1,4,3,2,1,4,1} // 切片元素可以是任意可排序的類型
sort.Ints(in)
j := 0
for i := 1; i < len(in); i++ {
if in[j] == in[i] {
continue
}
j++
// 需要保存原始數(shù)據(jù)時(shí)
// in[i], in[j] = in[j], in[i]
// 只需要保存需要的數(shù)據(jù)時(shí)
in[j] = in[i]
}
result := in[:j+1]
fmt.Println(result) // [1 2 3 4]
元素存在就移到最前面,不存在則放在前面
// moveToFront 把needle移動(dòng)或添加到haystack的前面
func moveToFront(needle string, haystack []string) []string {
if len(haystack) != 0 && haystack[0] == needle {
return haystack
}
prev := needle
for i, elem := range haystack {
switch {
case i == 0:
haystack[0] = needle
prev = elem
case elem == needle:
haystack[i] = prev
return haystack
default:
haystack[i] = prev
prev = elem
}
}
return append(haystack, prev)
}
haystack := []string{"a", "b", "c", "d", "e"} // [a b c d e]
haystack = moveToFront("c", haystack) // [c a b d e]
haystack = moveToFront("f", haystack) // [f c a b d e]
滑動(dòng)窗口
將切片input生成size大小的滑動(dòng)窗口。
func slidingWindow(size int, input []int) [][]int {
// 返回入?yún)⒌那衅鳛榈谝粋€(gè)元素
if len(input) <= size {
return [][]int{input}
}
// 以所需的精確大小分配切片
r := make([][]int, 0, len(input)-size+1)
for i, j := 0, size; j <= len(input); i, j = i+1, j+1 {
r = append(r, input[i:j])
}
return r
}
示例:
a := []int{1, 2, 3, 4, 5}
res := slidingWindow(2, a)
fmt.Println(res)
輸出:
[[1 2] [2 3] [3 4] [4 5]]
參考資料:https://github.com/golang/go/wiki/SliceTricks
推薦閱讀
