【问题标题】:What's the best way to count occurences of positive, negative, and 0 values in an unsorted array?计算未排序数组中正数、负数和 0 值出现次数的最佳方法是什么?
【发布时间】:2020-02-11 14:02:54
【问题描述】:

以下工作,但我将如何优化它?我想随着数组的增长,循环遍历数组会变得很昂贵。 我可以创建原始数组的映射来存储每个值的出现次数,然后在另一个循环中检查这些值是否为 +/-/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)
}

【问题讨论】:

  • 我强烈建议阅读Open letter to students with homework problems。 ;) 此外,arr 不是一个数组,而是一个切片 - 一个看似很小但很有趣的区别。
  • 我不认为你可以使访问数组中所有值的操作比循环更便宜。但是您可以尝试通过同时使用多个 goroutine 处理它来提高 if 速度。 Go 会尝试利用所有内核并行运行这些 goroutine。
  • @KoalaYeung 并发!= 并行。 ;)
  • @MarkusWMahlberg:当然。而且我认为我的评论不会混淆两者。
  • @MarkusWMahlberg 这不是家庭作业问题,我只是想习惯去并且一直在hackerrank/leetcode 上做这样的问题。我真的在努力学习如何更好地做到这一点。 :)

标签: arrays algorithm performance go optimization


【解决方案1】:

如果你的输入结构是一个未排序的数组,那么 O(n) 是你能做的最好的,即遍历数组,比较每个元素一次。

如果可以的话,您可以使用两个数组和一个整数,一个数组用于负数,一个数组用于正数,以及一个整数来计算零的数量。然后,不再需要计数,您可以简单地获取数组的长度。

【讨论】:

  • 为了拆分原始切片,即使排序正确,for循环仍然是唯一的方法?
  • 如果原始切片已排序,您可以使用二进制搜索来定位 0。如果原始切片未排序,则必须使用 for 循环查看每个元素。
【解决方案2】:

您几乎处于最佳解决方案。我实施了@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

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

qed

【讨论】:

    【解决方案3】:

    最快的方法是:

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

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

    c) 在循环中使用 SIMD:

    • 将一组值加载到 SIMD 寄存器中
    • 将组复制到另一个 SIMD 寄存器
    • 对整个数字组使用“无符号右移”然后“与”,以便每个数字的最低位包含原始数字的符号位
    • 将此结果添加到不同 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 个线程/goroutine,并将数组/切片拆分为 N每个线程/goroutine 做一个片段,然后在执行“步骤 d)”之前添加每个片段的计数。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-03
      • 2012-03-05
      • 1970-01-01
      • 1970-01-01
      • 2011-11-27
      • 2013-12-03
      相关资源
      最近更新 更多