Каков наилучший способ подсчета вхождений положительных, отрицательных и 0 значений в несортированном массиве? - PullRequest
0 голосов
/ 15 октября 2019

Ниже работает, но как бы это оптимизировать? Я полагаю, что цикл по массиву станет дорогим по мере роста. Я мог бы создать карту исходного массива для хранения количества вхождений для каждого значения, а затем проверить эти значения на + / - / 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 ]

1 голос
/ 15 октября 2019

Если ваша входная структура является несортированным массивом, то O (n) - лучшее, что вы можете сделать, то есть пройти массив, сравнив каждый элемент по одному разу.

Вы можете, если этоопция, работать с двумя массивами и целым числом, одним массивом для негативов, одним массивом для позитивов и целым числом для подсчета количества нулей. Тогда подсчет больше не нужен, и вы можете просто определить длину массивов.

0 голосов
/ 15 октября 2019

Самый быстрый способ будет:

a) убедиться, что массив / срез использует наименьший возможный тип данных (чтобы уменьшить объем ОЗУ и количество затронутых строк кэша; чтобы упаковать больше значений водин SIMD-регистр, и чтобы уменьшить величину сдвига, я собираюсь предложить позже) - например, для значений, которые вы указали в вопросе, вы могли / должны использовать int8 (а не int).

b) добавить нули в конец, чтобы заполнить массив / фрагмент несколькими из множества элементов, которые ЦП может сделать одновременно с использованием SIMD (например, 32 элемента, если вы используете int8 на ЦП 80x86, которыйподдерживает AVX2). Это в основном просто позволяет избежать беспорядочных хлопот, когда вы приближаетесь к концу массива / слайса.

c) использование SIMD в цикле:

  • загрузка группы значений в SIMDрегистр
  • копировать группу в другой регистр SIMD
  • использовать «беззнаковое смещение вправо», а затем «И» во всей группе чисел, чтобы младший бит в каждом номере содержал знаковый бит исходного номера
  • добавить результат этого к "группе счетчиков отрицательных чисел" в другом регистре SIMD
  • использовать последовательность "shift" и "OR", чтобы объединить все битычисло в один бит, чтобы получить «1, если исходное число было ненулевым, 0, если исходное число было нулевым»
  • добавить результат этого к «группе счетчиков ненулевых чисел» вдругой регистр SIMD

d) После всего этого (вне цикла):

  • рассчитать количество отрицательных чисел, выполнив "горизонтальное сложение"«группа счетчиков отрицательных чисел»

  • рассчитать количество положительных чисел, выполнив «горизонтальное сложение» «группы счетчиков ненулевых чисел», затем вычтя количество отрицательных чисел

  • вычислитьподсчет нулей, выполнив "zeros = all_numbers - positive_numbers - positive_numbers - padding_zeros"

Конечно, чтобы что-то делать хорошо, вам нужна встроенная сборка, что означает, что вам нужно что-то вроде https://godoc.org/github.com/slimsag/rand/simd (что делает встроенную сборку для вас удобным переносимым способом).

Примечание 1: для больших массивов / фрагментов (но не для небольших массивов / фрагментов) вы также захотите использовать несколькоПараллельные процессоры (например, если N процессоров имеют N потоков / программ, и разбить массив / фрагмент на N частей, где каждый поток / программа выполняет один фрагмент, а затем добавить подсчет из каждого фрагмента перед выполнением «шага d)»).

Примечание 2: для больших объемов данных;мой алгоритм "O (n)", и поскольку ваш оригинальный алгоритм только "O (n)", я бы ожидал, что мой алгоритм будет работать до 100 раз быстрее на современном оборудовании. Однако для очень небольших объемов данных, поскольку "O (n)" не является линейным, я ожидаю, что ваш алгоритм будет быстрее, чем мой.

0 голосов
/ 15 октября 2019

Вы в значительной степени в оптимальном решении. Сначала я применил предложение @ 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, простое подсчет кажется наиболее эффективным.

qed

...