title: 【轉載】關於切片操作的技巧
date: 2021-08-11 10:47:54
comment: false
toc: true
category:
- Golang
tags: - 轉載
- Go
- 切片
- 操作
本文轉載自:[譯] 關於切片操作的技巧 | 李文周的博客
本文翻譯自官方 wiki,整理了 Go 語言中關於切片操作的一些技巧。
備註:由於行文需要,一些細節與原文存在些許出入。
切片操作常用技巧#
複製#
將切片 a 中的元素複製到切片 b 中。
最簡單的、最常用的方法就是使用內置的copy
函數。
b = make([]T, len(a)) // 一次將內存申請到位
copy(b, a)
除了使用內置的copy
函數外,還有下面兩種使用append
函數複製切片的方法。
b = append([]T(nil), a...)
b = append(a[:0:0], a...)
這兩種方法通常比使用copy
函數複製的方法要慢一些,但是如果在複製之後有更多的元素要添加到 b 中,那麼它們的效率會更高。
剪切#
將切片 a 中索引 i~j 位置的元素剪切掉。
可以按照下面的方式,使用append
函數完成。
a = append(a[:i], a[j:]...)
刪除#
將切片 a 中索引位置為 i 的元素刪除。
同樣可以按照上面剪切的方式使用append
函數完成刪除操作。
a = append(a[:i], a[i+1:]...)
或者搭配copy
函數使用切片表達式完成刪除操作。
a = a[:i+copy(a[i:], a[i+1:])]
此外,如果只需要刪除掉索引為 i 的元素,無需保留切片元素原有的順序,那麼還可以使用下面這種簡單的方式進行刪除。
a[i] = a[len(a)-1] // 將最後一個元素移到索引i處
a = a[:len(a)-1] // 截掉最後一個元素
剪切或刪除操作可能引起的內存泄露#
需要特別注意的是。如果切片 a 中的元素是一個指針類型或包含指針字段的結構體類型(需要被垃圾回收),上面剪切和刪除的示例代碼會存在一個潛在的內存泄漏問題:一些具有值的元素仍被切片 a 引用,因此無法被垃圾回收機制回收掉。下面的代碼可以解決這個問題。
剪切#
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]
內部擴張#
在切片 a 的索引 i 之後擴張 j 個元素。
使用兩個append
函數完成,即先將索引 i 之後的元素追加到一個長度為 j 的切片後,再將這個切片中的所有元素追加到切片 a 的索引 i 之後。
a = append(a[:i], append(make([]T, j), a[i:]...)...)
擴張的這一部分元素為 T 類型的零值。
尾部擴張#
將切片 a 的尾部擴張 j 個元素的空間。
a = append(a, make([]T, j)...)
擴張的這一部分元素同樣為 T 類型的零值。
過濾#
按照一定的規則將切片 a 中的元素進行就地過濾。
這裡假設過濾的條件已封裝為keep
函數,使用for range
遍歷切片 a 的所有元素逐一調用keep
函數進行過濾。
n := 0
for _, x := range a {
if keep(x) {
a[n] = x // 保留該元素
n++
}
}
a = a[:n] // 截取切片中需保留的元素
插入#
將元素 x 插入切片 a 的索引 i 處。
還是使用兩個append
函數完成插入 x 的操作。
a = append(a[:i], append([]T{x}, a[i:]...)...)
第二個append
函數創建了一個具有自己底層數組的新切片,並將a[i:]
中的元素複製到該切片,然後由第一個append
函數將這些元素複製回切片 a。
我們可以通過使用另一種方法來避免新切片的創建(以及由此產生的內存垃圾)和第二個副本:
a = append(a, 0 /* 這裡應使用元素類型的零值 */)
copy(a[i+1:], a[i:])
a[i] = x
追加#
將元素 x 追加到切片 a 的最後。
這裡使用append
函數即可。
a = append(a, x)
彈出#
將切片 a 的最後一個元素彈出。
這裡使用切片表達式完成彈出操作。
x, a = a[len(a)-1], a[:len(a)-1]
彈出切片 a 的第一個元素。
x, a = a[0], a[1:]
前插#
將元素 x 前插到切片 a 的開始。
a = append([]T{x}, a...)
其他技巧#
過濾而不分配內存#
此技巧使用了一個事實,即切片 b 與原始切片 a 共享相同的底層數組和容量,因此原存儲空間已重新用於過濾後的切片。當然原始切片的內容被修改了。
b := a[:0]
for _, x := range a {
if f(x) {
b = append(b, x)
}
}
對於必須被垃圾回收的元素,在完成上述操作後可以添加以下代碼:
for i := len(b); i < len(a); i++ {
a[i] = nil // 或T類型的零值
}
翻轉#
將切片 a 的元素順序翻轉。
通過迭代兩兩互換元素完成。
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]
})
使用最小分配進行批處理#
如果你想對一個大型切片 a 的元素分批進行處理,這會很有用。
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]]
原地刪除重複元素(元素可比較)#
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++
// 需要保存原始數據時
// in[i], in[j] = in[j], in[i]
// 只需要保存需要的數據時
in[j] = in[i]
}
result := in[:j+1]
fmt.Println(result) // [1 2 3 4]
存在就移到前面,不存在就插入到前面#
如果給定的元素在切片中存在則把該元素移到切片的頭部,如果不存在則將該元素插入到切片的頭部。
// moveToFront 把needle移動或添加到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]
滑動窗口#
將切片 input 生成 size 大小的滑動窗口。
func slidingWindow(size int, input []int) [][]int {
// 返回入參的切片作為第一個元素
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]]