计算未排序数组中正值,负值和0值的出现的最佳方法是什么?

时间:2019-10-14 23:45:43

标签: arrays algorithm performance go optimization

以下内容有效,但我将如何对其进行优化?我想遍历数组会变得越来越昂贵。 我可以创建原始数组的映射表来存储每个值的出现次数,然后在另一个循环中检查这些值的+ /-/ 0,但这甚至更糟。

package main
import (
    "fmt"
)

func main() {
    arr := []int{2, 5, 6, 7, 8, 2, 4, 1, 1, 1, 2, -2, -2, 2, 2, 3, -1, 0, 0, 0, 0, 2, 5, 4, 9, 8, 7, 2, -3, -7}
    var p, n, z int = 0, 0, 0
    for _, v := range arr {
        if v > 0 {
            p++
        } else if v < 0 {
            n++
        } else if v == 0 {
            z++
        }
    }
    fmt.Println(p, n, z)
}

3 个答案:

答案 0 :(得分:1)

如果您的输入结构是未排序的数组,那么O(n)是您可以做的最好的事情,也就是说,遍历该数组,对每个元素进行一次比较。

如果可以选择,则可以使用两个数组和一个整数,一个数组用于负数,一个数组用于正数,以及一个整数以计算零的数量。然后,不再需要计数,您只需获取数组的长度即可。

答案 1 :(得分:0)

您几乎可以找到最佳解决方案。我实现了@bserdar的建议“首先进行排序”并针对它进行了基准测试。

  

注意:这是一个非常粗糙的实现。将其与一磅盐一起食用。

为了便于阅读,省略了打包和导入。

var slice = []int{2, 5, 6, 7, 8, 2, 4, 1, 1, 1, 2, -2, -2, 2, 2, 3, -1, 0, 0, 0, 0, 2, 5, 4, 9, 8, 7, 2, -3, -7}

func orig(s []int) (negative, zero, positive int) {
    for _, v := range s {
        if v > 0 {
            positive++
        } else if v < 0 {
            negative++
        } else if v == 0 {
            zero++
        }
    }
    return
}

func sorted(s []int) (negative, zero, positive int) {
    // We do not want to modify the input slice,
    // so we need to create a copy of it
    sortedSlice := make([]int, len(s))
    copy(sortedSlice, s)
    sort.Ints(sortedSlice)
    return preSorted(sortedSlice)
}

func preSorted(s []int) (int, int, int) {
    var z, p int
    var zfound bool
    for i := 0; i < len(s); i++ {
        if s[i] < 0 {
            continue
        } else if !zfound && s[i] == 0 {
            zfound = true
            z = i
        } else if s[i] > 0 {
            p = i
            break
        }
    }
    return z, p - z, len(s) - p
}

测试代码:

func BenchmarkOrig(b *testing.B) {
    for i := 0; i < b.N; i++ {
        orig(slice)
    }
}

func BenchmarkLongOrig(b *testing.B) {
    var slice = make([]int, 10000000)
    for i := 0; i < 10000000; i++ {
        slice[i] = rand.Intn(10)
        if rand.Intn(2) == 0 {
            slice[i] = slice[i] * -1
        }
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        orig(slice)
    }
}
func BenchmarkSorted(b *testing.B) {
    for i := 0; i < b.N; i++ {
        sorted(slice)
    }
}

func BenchmarkLongSorted(b *testing.B) {
    var slice = make([]int, 10000000)
    for i := 0; i < 10000000; i++ {
        slice[i] = rand.Intn(10)
        if rand.Intn(2) == 0 {
            slice[i] = slice[i] * -1
        }
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sorted(slice)
    }
}

func BenchmarkPresorted(b *testing.B) {
    cp := make([]int, len(slice))
    copy(cp, slice)
    sort.Ints(cp)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        preSorted(cp)
    }
}

func BenchmarkLongPresorted(b *testing.B) {
    var slice = make([]int, 10000000)
    for i := 0; i < 10000000; i++ {
        slice[i] = rand.Intn(10)
        if rand.Intn(2) == 0 {
            slice[i] = slice[i] * -1
        }
    }
    sort.Ints(slice)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sorted(slice)
    }
}

根据基准:

goos: darwin
goarch: amd64
BenchmarkOrig-4             27271665            38.4 ns/op         0 B/op          0 allocs/op
BenchmarkLongOrig-4               21      50343196 ns/op           0 B/op          0 allocs/op
BenchmarkSorted-4            1405150           852 ns/op         272 B/op          2 allocs/op
BenchmarkLongSorted-4              2     536973066 ns/op    80003104 B/op          2 allocs/op
BenchmarkPresorted-4        100000000           10.9 ns/op         0 B/op          0 allocs/op
BenchmarkLongPresorted-4           5     248698010 ns/op    80003104 B/op          2 allocs/op

编辑发现了一种更有效的返回计数的方法。我们无需创建新的切片,而是计算每个子切片的长度。当切片较小时,这使得预排序非常有效。但是在10M时,简单计数似乎是最有效的。

已确认

答案 2 :(得分:0)

最快的方法是:

a)确保阵列/切片正在使用最小的数据类型(以减少RAM的数量和触及的缓存行的数量;将更多的值打包到单个SIMD寄存器中,并减少移位量I稍后再建议)-例如对于您在问题中显示的值,您可以/应该使用int8(而不是int)。

b)在末尾添加零,以将数组/切片填充到CPU使用SIMD可以一次完成的许多元素的倍数(例如,如果在80x86 CPU上使用int8,则可以使用32个元素)支持AVX2)。当您接近阵列/切片的末尾时,这基本上只是避免了麻烦的麻烦。

c)循环使用SIMD:

  • 将一组值加载到SIMD寄存器中
  • 将组复制到另一个SIMD寄存器
  • 在整个数字组上使用“无符号右移”,然后使用“ AND”,以便每个数字的最低位包含原始数字的符号位
  • 将此结果添加到另一个SIMD寄存器中的“负数计数器组”中
  • 使用“移位”和“或”的序列,将一个数字的所有位合并为一个位,得到“如果原始数字不为零,则为1,如果原始数字为零,则为0”
  • 将此结果添加到另一个SIMD寄存器中的“非零数字计数器组”中

d)之后(在循环之外):

  • 通过对“负数计数器组”进行“水平加法”来计算负数的计数

  • 通过对“非零数字的计数器组”进行“水平加法”,然后减去负数的计数来计算正数的计数

  • 通过执行“ zeros = all_numbers-negative_numbers-positive_numbers-padding_zeros”来计算零的计数

当然,要想做好任何事情,您都需要内联汇编,这意味着您需要使用https://godoc.org/github.com/slimsag/rand/simd之类的东西(以一种很好的可移植方式为您进行内联汇编)。

注1:对于大型阵列/切片(但不是小型阵列/切片),您还希望并行使用多个CPU(例如,如果有N个CPU,则具有N个线程/ goroutines,然后将阵列/切片分成N个每个线程/ goroutine做一件事情,然后在执行“步骤d)”之前添加每件事情的计数。

注2:用于大量数据;我的算法是“ O(n)”,并且因为您的原始算法只有“ O(n)”,所以我希望我的算法在现代硬件上的速度快100倍。但是,对于少量数据,由于“ O(n)”不是线性的,所以我希望您的算法比我的算法更快。