Как аппроксимировать подсчет различных значений в массиве за один проход через него

У меня есть несколько огромных массивов (миллионы ++ членов). Все это массивы чисел, и они не отсортированы (и я не могу этого сделать). Некоторые из них uint8_t, некоторые uint16_t/32/64. Я хотел бы аппроксимировать подсчет различных значений в этих массивах. Условия следующие:

  • скорость ОЧЕНЬ важна, мне нужно сделать это за один проход через массив, и я должен пройти через нее последовательно (не могу прыгать назад и вперед) (я делаю это на С++, если это важно)
  • Мне не нужны ТОЧНЫЕ счета. Я хочу знать, что если это массив uint32_t, если есть 10 или 20 различных чисел или есть тысячи или миллионы.
  • У меня довольно много памяти, которую я могу использовать, но чем меньше используется, тем лучше
  • Чем меньше тип данных массива, тем более точным я должен быть
  • Я не против STL, но если я смогу сделать это без него, это будет здорово (без BOOST, хотя, извините)
  • если подход можно легко распараллелить, это было бы здорово (но это не обязательное условие)

Примеры идеального вывода:

ArrayA [uint32_t, 3M members]: ~128 distinct values
ArrayB [uint32_t, 9M members]: 100000+ distinct values
ArrayC [uint8_t, 50K members]: 2-5 distinct values
ArrayD [uint8_t, 700K members]: 64+ distinct values

Я понимаю, что некоторые из ограничений могут показаться нелогичными, но так оно и есть. В качестве примечания я также хочу, чтобы верхние X (3 или 10) наиболее используемые и наименее используемые значения, но это намного проще сделать, и я могу сделать это самостоятельно. Однако, если у кого-то есть мысли для этого, не стесняйтесь делиться ими!

EDIT: немного разъяснений относительно STL. Если у вас есть решение, использующее его, отправьте его. Не использование STL было бы просто бонусом для нас, мы не слишком это себе представляем. Однако, если это хорошее решение, оно будет использоваться!

Ответы

Ответ 1

Для 8- и 16-битных значений вы можете просто составить таблицу подсчета каждого значения; каждый раз, когда вы записываете запись в таблицу, которая ранее была равна нулю, было обнаружено другое значение.

При больших значениях, если вы не заинтересованы в счетах выше 100000, std::map подходит, если это достаточно быстро. Если это слишком медленно для вас, вы можете запрограммировать свое собственное B-дерево.

Ответ 2

Я уверен, что вы можете это сделать:

  • Создайте фильтр Bloom.
  • Запустите массив, вставляя каждый элемент в фильтр (это "медленный" O (n), поскольку он требует вычисления нескольких независимых приличных хэшей каждого значения)
  • Подсчитайте, сколько бит установлено в цветовом фильтре
  • Вычислить обратно плотность плотности фильтра на оценку количества различных значений. Я не знаю вычислений с верхней части головы, но любое обращение к теории фильтров Блума идет на это, потому что это жизненно важно для вероятности того, что фильтр даст ложный результат при поиске.

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

Я считаю, что "наиболее часто используемая" проблема затруднена (ну, из-за памяти). Предположим на мгновение, что вам нужно только первое наиболее часто используемое значение. Предположим далее, что у вас есть 10 миллионов записей в массиве, и что после первых 9,9 миллионов из них ни один из номеров, которые вы видели до сих пор, оказался более 100 тысяч раз. Тогда любое из значений, которые вы видели до сих пор, может быть наиболее часто используемым значением, так как любой из них может иметь значение 100 тыс. Значений в конце. Хуже того, в конце концов, у каждого из них может быть пробег по 50 тыс., И в этом случае счет из первых 9,9 млн. Записей является тай-брейкером между ними. Поэтому, чтобы разобраться в одном проходе, который наиболее часто используется, я думаю, вам нужно знать точное количество каждого значения, которое появляется в 9,9 миллиона. Вы должны подготовиться к этому уродному делу с близкого расстояния между двумя значениями за последние 0,1 миллиона, потому что, если это произойдет, вам не разрешается перематывать назад и снова проверять два соответствующих значения. В конце концов вы можете начать отбрасывать значения - если есть значение с числом 5000 и только 4000 записей, оставшихся для проверки, вы можете отбирать что-либо со счетом 1000 или меньше. Но это не очень помогает.

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

Ответ 3

Один подход, который может работать даже при больших значениях, заключается в том, чтобы разложить их на лениво распределенные ведра.

Предположим, что вы работаете с целыми числами 32, создание массива из битов 2**32 относительно непрактично (2**29 bytes, hum). Однако мы можем предположить, что указатели 2**16 по-прежнему разумны (2**19 bytes: 500kB), поэтому мы создаем ведра 2**16 (нулевые указатели).

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

typedef std::pair<int32_t, int32_t> Pair;
typedef std::vector<Pair> Bucket;
typedef std::vector<Bucket*> Vector;

struct Comparator {
  bool operator()(Pair const& left, Pair const& right) const {
    return left.first < right.first;
  }
};

void add(Bucket& v, int32_t value) {
  Pair const pair(value, 1);
  Vector::iterator it = std::lower_bound(v.begin(), v.end(), pair, Compare());
  if (it == v.end() or it->first > value) {
    v.insert(it, pair);
    return;
  }

  it->second += 1;
}

void gather(Vector& v, int32_t const* begin, int32_t const* end) {
  for (; begin != end; ++begin) {
    uint16_t const index = *begin >> 16;

    Bucket*& bucket = v[index];

    if (bucket == 0) { bucket = new Bucket(); }

    add(*bucket, *begin);
  }
}

После того, как вы собрали свои данные, вы можете подсчитать количество различных значений или легко найти верхнюю или нижнюю.

Несколько примечаний:

  • количество ковшей полностью настраивается (таким образом, вы можете контролировать количество исходной памяти)
  • стратегия перераспределения также настраивается (это просто дешевая хеш-таблица, которую я здесь сделал).
  • можно отслеживать количество выделенных ведер и отказаться, или переключать передачу, если она начнет взорваться.
  • если каждое значение отличается, тогда оно просто не сработает, но когда вы это осознаете, вы уже собрали много счетчиков, так что вы, по крайней мере, сможете дать более низкую оценку числа разных значений, и у вас также будет начальная точка для верхнего/нижнего.

Если вам удастся собрать эти статистические данные, то работа будет вырезана для вас.

Ответ 4

Для 8 и 16 бит это довольно очевидно, вы можете отслеживать каждую возможность на каждой итерации.

Когда вы получаете 32 и 64-битные целые числа, у вас действительно нет памяти для отслеживания каждой возможности.

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

Я не понимаю, почему вы не можете сортировать массив. RadixSort - это O (n), и после сортировки это будет еще один проход, чтобы получить точную отличительность и верхнюю информацию X. В действительности это было бы 6 проходов вместе для 32-битного, если вы использовали 1-байтовый радиус (1 проход для подсчета + 1 * 4 прохода для каждого байта + 1 проход для получения значений).

В том же решении, что и выше, почему бы просто не использовать SQL. Вы можете создать хранимую процедуру, которая принимает массив в качестве параметра, заданного в таблице, и возвращает количество различных значений и верхние значения x за один раз. Эта хранимая процедура также может быть выведена параллельно.

-- number of distinct
SELECT COUNT(DISTINCT(n)) FROM @tmp
-- top x
SELECT TOP 10 n, COUNT(n) FROM @tmp GROUP BY n ORDER BY COUNT(n) DESC

Ответ 5

Я только подумал о интересном решении. Он основан на законе булевой алгебры, называемом Idempotence of Multiplication, в котором говорится, что:

X * X = X

Из него и используя коммутативное свойство булевого умножения, можно сделать вывод, что:

X * Y * X = X * X * Y = X * Y

Теперь вы видите, куда я иду? Так будет работать алгоритм (я ужасен с псевдокодом):

  • make c = element1 и element2 (двоичный И между двоичным представлением целых чисел)

  • для я = 3 до тех пор, пока я == size_of_array  make b = c и element [i];  если b!= c, то diferent_values ​​++;  с = Ь;

В первой итерации мы делаем (element1 * element2) * element3. Мы могли бы представить его как:

(X * Y) * Z

Если Z (элемент 3) равен X (element1), то:

(X * Y) * Z = X * Y * X = X * Y

И если Z равно Y (element2), то:

(X * Y) * Z = X * Y * Y = X * Y

Итак, если Z не отличается от X или Y, то X * Y не изменится, когда мы умножим его на Z

Это остается действительным для больших выражений, например:

(X * A * Z * G * T * P * S) * S = X * A * Z * G * T * P * S

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

Итак, как это пойдет. Каждый раз, когда вычисляется другое значение, умножение нашего большого множителя и этого различного значения будет отличаться от большого операнда. Таким образом, с b = c & element[i], если b!= c мы просто увеличиваем счетчик различных значений.

Думаю, я не достаточно ясен. Если это случай, пожалуйста, дайте мне знать.