`追加'复杂性

时间:2013-03-29 11:37:09

标签: go

Go编程语言中这个循环的计算复杂度是多少?

var a []int
for i := 0 ; i < n ; i++ {
  a = append(a, i)
}

append是否在线性时间内运行(重新分配内存并复制每个附加内容的所有内容),或者是在分摊的常量时间内(就像实现多种语言中的矢量类一样)?

2 个答案:

答案 0 :(得分:19)

The Go Programming Language Specification表示append内置函数会在必要时重新分配。

  

Appending to and copying slices

     

如果s的容量不足以容纳附加值,   append分配一个适合两者的新的足够大的切片   现有切片元素和附加值。于是,归来了   slice可以引用不同的底层数组。

必要时,为追加增长目标切片的精确算法取决于实现。对于当前的gc编译器算法,请参阅Go growsliceslice.go源文件中的runtime函数。它是固定时间的摊销。

部分地,增长量计算切片计算为:

    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 Programming Language Specification允许该语言的实现者以多种方式实现append内置函数。

例如,新分配只需要“足够大”。分配的金额可以是parsimonius,分配最小必要金额或慷慨,分配超过最小必要金额,以最大限度地减少调整大小的成本。 Go gc编译器使用大量动态数组摊销的恒定时间算法。

以下代码说明了append内置函数的两个合法实现。慷慨的常量函数实现与Go gc编译器相同的分摊常数时间算法。一旦初始分配被填满,parsimonius变量函数每次都重新分配和复制所有内容。 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内置函数可能会或可能不会在每次调用时重新分配。

参考文献:

Dynamic array

Amortized analysis

  

Appending to and copying slices

     

如果s的容量不足以容纳附加值,   append分配一个适合两者的新的足够大的切片   现有切片元素和附加值。于是,归来了   slice可以引用不同的底层数组。

     

Append to a slice specification discussion

     

规范(提示和1.0.3)说明:

     

“如果s的容量不足以容纳额外的容量   值append分配一个适合的新的足够大的切片   现有的切片元素和附加值。就这样   返回的切片可能引用不同的底层数组。“

     

这应该是“当且是否”?例如,如果我知道   我的切片的容量足够长,我保证我会   不改变底层数组?

     

Rob Pike

     

是的,你是如此放心。

运行时slice.go源文件

Arrays, slices (and strings): The mechanics of 'append'

答案 1 :(得分:-1)

它不会在每个附加内容上重新分配,并且在docs中明确说明:

  

如果s的容量不足以容纳附加值,则append会分配一个足够大的新切片,该切片适合现有切片元素和其他值。因此,返回的切片可以引用不同的底层数组。

因此,摊销的常数时间是复杂的。