假设我有一个slice
type 切片int
。在声明时,我将第三个参数设置为size
,我相信可以size
通过设置cap
slice 的参数为至少int 保留内存。
slice:=make([]int,0,size)
现在,假设我有一个整数变量value
。要将其添加到切片的末尾,我使用
slice=append(slice,value)
如果当前切片中的元素数少于size
,则无需添加整个基础数组到新位置即可添加新元素。
另外,如果我想在前面加上value
到slice
,作为建议在这里和这里,我用
slice=append([]int{value},slice...)
我的问题是,在这种情况下会发生什么?如果元素数仍少于size
,则如何将元素存储在内存中?假设make()
在调用函数时分配是连续的,是否所有现有元素都右移以释放第一个值空间?还是重新分配内存并复制所有元素?
询问的原因是我希望我的程序尽可能快,并且想知道这是否可能导致其速度降低。如果是这样,是否有其他替代方式可以使时间效率更高?
内置函数append()
始终将元素附加到切片。您不能单独使用它来添加元素。
话虽如此,如果您要在其前添加元素的切片容量大于长度(在其元素后有“自由”空间),则可以重新切片原始切片,将所有元素复制到更高的索引以腾出空间对于新元素,然后将元素设置为第0 个索引。这将不需要新的分配。它看起来像这样:
func prepend(dest []int, value int) []int {
if cap(dest) > len(dest) {
dest = dest[:len(dest)+1]
copy(dest[1:], dest)
dest[0] = value
return dest
}
// No room, new slice need to be allocated:
// Use some extra space for future:
res := make([]int, len(dest)+1, len(dest)+5)
res[0] = value
copy(res[1:], dest)
return res
}
测试它:
s := make([]int, 0, 5)
s = append(s, 1, 2, 3, 4)
fmt.Println(s)
s = prepend(s, 9)
fmt.Println(s)
s = prepend(s, 8)
fmt.Println(s)
输出(在Go Playground上尝试):
[1 2 3 4]
[9 1 2 3 4]
[8 9 1 2 3 4]
注意:如果没有新元素的余地,由于性能现在很重要,我们不仅会这样做:
res := append([]int{value}, dest...)
因为它执行的分配和复制操作超出了所需的数量:因此为文字([]int{value}
)分配了一个分片,然后append()
在附加dest
到其后分配了一个新的分片。
取而代之的是,我们的解决方案仅分配一个新数组(by make()
,甚至为将来的增长保留一些空间),然后将其设置value
为第一个元素并进行复制dest
(前面的元素)。
如果需要多次处理,则正常切片可能不是正确的选择。更快的替代方法是使用链表,在链表前添加元素无需分配切片/数组和复制,您只需创建一个新的node元素,然后将其指向旧根即可将其指定为根(第一个元素)。
标准库在container/list
包中提供了常规实现。
坚持正常的切片和阵列,还有另一种解决方案。
如果您愿意自己管理更大的支持阵列(或片),可以通过在使用片之前保留可用空间来实现。前置时,您可以从支持的较大数组或片中创建一个新的分片值,该值从索引开始,该索引为要保留的1个元素留出空间。
没有完整性,仅用于演示:
var backing = make([]int, 15) // 15 elements
var start int
func prepend(dest []int, value int) []int {
if start == 0 {
// No more room for new value, must allocate bigger backing array:
newbacking := make([]int, len(backing)+5)
start = 5
copy(newbacking[5:], backing)
backing = newbacking
}
start--
dest = backing[start : start+len(dest)+1]
dest[0] = value
return dest
}
测试/使用它:
start = 5
s := backing[start:start] // empty slice, starting at idx=5
s = append(s, 1, 2, 3, 4)
fmt.Println(s)
s = prepend(s, 9)
fmt.Println(s)
s = prepend(s, 8)
fmt.Println(s)
// Prepend more to test reallocation:
for i := 10; i < 15; i++ {
s = prepend(s, i)
}
fmt.Println(s)
输出(在Go Playground上尝试):
[1 2 3 4]
[9 1 2 3 4]
[8 9 1 2 3 4]
[14 13 12 11 10 8 9 1 2 3 4]
分析:当backing
切片中有空间可以放置值时,此解决方案不进行分配,也不进行复制!发生的所有事情是,它从该backing
切片中创建一个新切片,该切片覆盖目标+1空间中要添加的值,并对其进行设置并返回此切片值。你真的不能比这更好。
如果没有空间,那么它将分配更大的backing
切片,复制旧内容的内容,然后执行“正常”前缀。
想法:假设您总是将元素按向后的顺序存储在切片中。
将您的元素以向后的顺序存储在切片中意味着预装成为追加!
因此,要“预填充”元素,只需使用即可append(s, value)
。就这样。
是的,这有其有限的用途(例如,将其追加到以相反顺序存储的切片中,其问题和复杂性与“常规”切片和预装操作相同),并且您失去了许多便利(使用for range
仅举一例列出元素的能力)但是,从性能角度来看,仅使用即可胜任预取值append()
。
注意:迭代以向后顺序存储元素的元素必须使用向下循环,例如:
for i := len(s) - 1; i >= 0; i-- {
// do something with s[i]
}
最后说明:所有这些解决方案都可以轻松扩展为在切片前面加上一个值。一般来说reslicing当额外的空间不是+1
,但是+len(values)
,并没有简单的设置dst[0] = value
,而是调用copy(dst, values)
。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句