在 Go 编程语言中,这个循环的计算复杂度是多少?
var a []int
for i := 0 ; i < n ; i++ {
a = append(a, i)
}
append
操作的时间复杂度是线性的(每次追加都会重新分配内存并复制所有内容),还是摊销常数时间(类似于许多语言中实现的向量类)?
在 Go 编程语言中,这个循环的计算复杂度是多少?
var a []int
for i := 0 ; i < n ; i++ {
a = append(a, i)
}
append
操作的时间复杂度是线性的(每次追加都会重新分配内存并复制所有内容),还是摊销常数时间(类似于许多语言中实现的向量类)?
Go编程语言规范指出,如果需要,append
内置函数会重新分配空间。
如果切片s的容量不足以容纳额外的值,则
append
会分配一个新的、足够大的切片,该切片可以同时容纳现有的切片元素和额外的值。因此,返回的切片可能引用不同的基础数组。
当需要为追加操作增长目标切片时,精确的算法取决于实现。对于当前的gc
编译器算法,请参见Go runtime
包slice.go
源文件中的growslice
函数。它是摊销常数时间。
在某种程度上,增长切片的计算量如下:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}
附录
Go编程语言规范允许语言的实现者以多种方式实现append
内置函数。
例如,新分配的空间只需“足够大”。分配的数量可以是吝啬的,只分配最少必要的数量,也可以是慷慨的,分配比最少必要的数量更多的空间,以最小化多次调整大小的成本。Go gc
编译器使用慷慨的动态数组摊销常数时间算法。
以下代码示例了append
内置函数的两个合法实现。慷慨的常数函数实现了与Go gc
编译器相同的摊销常数时间算法。吝啬的变量函数,在初始分配填满后,每次都会重新分配和复制所有内容。Go append
函数和Go gccgo
编译器用作控制。
package main
import "fmt"
// Generous reallocation
func constant(s []int, x ...int) []int {
if len(s)+len(x) > cap(s) {
newcap := len(s) + len(x)
m := cap(s)
if m+m < newcap {
m = newcap
} else {
for {
if len(s) < 1024 {
m += m
} else {
m += m / 4
}
if !(m < newcap) {
break
}
}
}
tmp := make([]int, len(s), m)
copy(tmp, s)
s = tmp
}
if len(s)+len(x) > cap(s) {
panic("unreachable")
}
return append(s, x...)
}
// Parsimonious reallocation
func variable(s []int, x ...int) []int {
if len(s)+len(x) > cap(s) {
tmp := make([]int, len(s), len(s)+len(x))
copy(tmp, s)
s = tmp
}
if len(s)+len(x) > cap(s) {
panic("unreachable")
}
return append(s, x...)
}
func main() {
s := []int{0, 1, 2}
x := []int{3, 4}
fmt.Println("data ", len(s), cap(s), s, len(x), cap(x), x)
a, c, v := s, s, s
for i := 0; i < 4096; i++ {
a = append(a, x...)
c = constant(c, x...)
v = variable(v, x...)
}
fmt.Println("append ", len(a), cap(a), len(x))
fmt.Println("constant", len(c), cap(c), len(x))
fmt.Println("variable", len(v), cap(v), len(x))
}
gc:
data 3 3 [0 1 2] 2 2 [3 4]
append 8195 9152 2
constant 8195 9152 2
variable 8195 8195 2
gccgo:
data 3 3 [0 1 2] 2 2 [3 4]
append 8195 9152 2
constant 8195 9152 2
variable 8195 8195 2
append
在每次调用时可能会或可能不会重新分配内存。
参考资料:
如果s的容量不足以容纳附加值,
append
会分配一个新的、足够大的切片,使现有的切片元素和附加的值都适合其中。因此,返回的切片可能指向一个不同的底层数组。规范(在tip和1.0.3处)说明:
“如果s的容量不足以容纳附加值,
append
会分配一个新的、足够大的切片,使现有的切片元素和附加的值都适合其中。因此,返回的切片可能指向一个不同的底层数组。”这是否应该是“当且仅当”?例如,如果我知道我的切片的容量足够长,那么我能保证我不会改变底层数组吗?
是的,你可以确定。
运行时 slice.go 源文件
它不会在每次追加时重新分配内存,这在文档中已经明确说明:
如果 s 的容量不足以容纳额外的值,则 append 会分配一个新的、足够大的切片,该切片既适合现有的切片元素,也适合额外的值。因此,返回的切片可能引用不同的底层数组。
因此,摊销常数时间就是所询问的复杂度。