Хеширование неупорядоченной последовательности малых целых чисел

Фон

У меня есть большая коллекция (~ тысяч) последовательностей целых чисел. Каждая последовательность имеет следующие свойства:

  • имеет длину 12;
  • порядок элементов последовательности не имеет значения;
  • ни один элемент не появляется дважды в той же последовательности;
  • все элементы меньше 300.

Обратите внимание, что свойства 2. и 3. подразумевают, что последовательности фактически являются наборами, но они сохраняются как массивы C, чтобы максимизировать скорость доступа.

Я ищу хороший алгоритм на С++, чтобы проверить, присутствует ли новая коллекция в коллекции. Если нет, новая последовательность добавляется в коллекцию. Я думал об использовании хеш-таблицы (обратите внимание, однако, что я не могу использовать любые конструкции С++ 11 или внешние библиотеки, например Boost). Хеширование последовательностей и сохранение значений в std::set также является опцией, так как столкновениями можно просто пренебречь, если они достаточно редки. Любое другое предложение также приветствуется.

Вопрос

Мне нужна коммутативная хеш-функция, т.е. функция, не зависящая от порядка элементов в последовательности. Я думал о первом сокращении последовательностей до некоторой канонической формы (например, сортировке), а затем с использованием стандартных хеш-функций (см. Ссылки ниже), но я предпочел бы избежать накладных расходов, связанных с копированием (я не могу изменить исходные последовательности) и сортировка. Насколько я могу судить, ни одна из функций, упомянутых ниже, не является коммутативной. В идеале хэш-функция также должна использовать тот факт, что элементы никогда не повторяются. Скорость имеет решающее значение.

Любые предложения?

Ответ 1

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

Если вы хотите использовать хеш-таблицу, а не trie, то вы все равно можете отсортировать элементы, а затем применить одну из этих некоммутативных хеш-функций. Вам нужно отсортировать элементы, чтобы сравнить последовательности, которые вы должны выполнить, потому что у вас будут столкновения с хэш-таблицами. Если вам не нужно сортировать, то вы можете умножить каждый элемент на постоянный коэффициент, который бы размазал их по битам int (существует теория для нахождения такого фактора, но вы можете найти его экспериментально), а затем XOR Результаты. Или вы можете искать свои значения ~ 300 в таблице, сопоставляя их с уникальными значениями, которые хорошо смешиваются с помощью XOR (каждый из них может быть случайным значением, выбранным так, чтобы он имел равное количество 0 и 1 бит - каждый XOR переворачивает случайная половина битов, что является оптимальным).

Ответ 2

Вот основная идея; не стесняйтесь изменять его по своему усмотрению.

  • Хеширование целого числа - это только тождество.

  • Мы используем формулу из boost::hash_combine для получения хешей комбайнов.

  • Мы сортируем массив для получения уникального представителя.

код:

#include <algorithm>

std::size_t array_hash(int (&array)[12])
{
    int a[12];
    std::copy(array, array + 12, a);
    std::sort(a, a + 12);

    std::size_t result = 0;

    for (int * p = a; p != a + 12; ++p)
    {
        std::size_t const h = *p; // the "identity hash"

        result ^= h + 0x9e3779b9 + (result << 6) + (result >> 2);
    }

    return result;
}

Обновление: поцарапать. Вы просто отредактировали вопрос, чтобы быть чем-то совершенно другим.

Если каждое число не превышает 300, вы можете сжать отсортированный массив по 9 бит каждый, т.е. 108 бит. "Неупорядоченное" свойство сохраняет только дополнительные 12!, что составляет около 29 бит, поэтому это действительно не имеет значения.

Вы можете искать 128-битный неподписанный интегральный тип и хранить в нем отсортированный, упакованный набор целых чисел. Или вы можете разбить этот диапазон на два 64-битных целых числа и вычислить хэш, как указано выше:

uint64_t hash = lower_part + 0x9e3779b9 + (upper_part << 6) + (upper_part >> 2);

(Или, возможно, используйте 0x9E3779B97F4A7C15 как магическое число, которое является 64-разрядной версией.)

Ответ 3

Я бы просто использовал функцию sum как хэш и посмотрел, как далеко вы с этим справитесь. Это не использует преимущества не повторяющегося свойства данных, ни того факта, что все они являются < 300. С другой стороны, его невероятно быстро.

std::size_t hash(int (&arr)[12]) {
    return std::accumulate(arr, arr + 12, 0);
}

Поскольку функция должна быть не осведомлена о заказе, я не вижу разумного способа использовать ограниченный диапазон входных значений без их первой сортировки. Если это абсолютно необходимо, столкновение, Id hard-code a сортировка сети (т.е. Число операторов if... else)) для сортировки 12 значений на месте (но я понятия не имею, как будет выглядеть сортировочная сеть для 12 значений или даже если это будет практично).

РЕДАКТИРОВАТЬ После обсуждения в комментариях, это очень хороший способ уменьшения коллизий: поднять каждое значение в массиве до некоторой целочисленной мощности перед суммированием. Самый простой способ сделать это - через transform. Это создает копию, но, вероятно, все еще очень быстро:

struct pow2 {
    int operator ()(int n) const { return n * n; }
};

std::size_t hash(int (&arr)[12]) {
    int raised[12];
    std::transform(arr, arr + 12, raised, pow2());
    return std::accumulate(raised, raised + 12, 0);
}

Ответ 4

Вы можете переключать биты, соответствующие каждому из 12 целых чисел, в битете размером 300. Затем используйте формулу boost:: hash_combine для объединения десяти 32-битных целых чисел, реализующих этот битовый набор.

Это дает коммутативную хеш-функцию, не использует сортировку и использует тот факт, что элементы никогда не повторяются.


Этот подход может быть обобщен, если мы выберем произвольный размер битета, и если мы установим или переключим произвольное количество бит для каждого из 12 целых чисел (какие биты для установки/переключения для каждого из 300 значений определяются либо хеш-функцией или с использованием предварительно вычисленной таблицы поиска). В результате получается Bloom filter или связанные структуры.

Мы можем выбрать фильтр Bloom размером 32 или 64 бит. В этом случае нет необходимости комбинировать куски большого битового вектора в одно значение хэш-функции. В случае классической реализации фильтра Блума с размером 32, оптимальное количество хэш-функций (или ненулевых битов для каждого значения таблицы поиска) равно 2.

Если вместо "или" работы классического фильтра Блума мы выбираем "xor" и используем половину ненулевых битов для каждого значения таблицы поиска, мы получаем решение, упомянутое Джим Балтером.

Если вместо операции "или" мы выбираем "+" и используем примерно половину ненулевых битов для каждого значения таблицы поиска, мы получаем решение, подобное одному, предложенное Конрадом Рудольфом.

Ответ 5

Я принял ответ Джима Балтера, потому что он тот, кто ближе всего к тому, что я в конечном итоге закодировал, но все ответы получили мой +1 за их полезность.

Вот алгоритм, с которым я закончил. Я написал небольшой Python script, который генерирует 300 64-битных целых чисел, так что их двоичное представление содержит ровно 32 истинных и 32 ложных бита. Позиции истинных битов распределяются случайным образом.

import itertools
import random
import sys

def random_combination(iterable, r):
    "Random selection from itertools.combinations(iterable, r)"
    pool = tuple(iterable)
    n = len(pool)
    indices = sorted(random.sample(xrange(n), r))
    return tuple(pool[i] for i in indices)

mask_size = 64
mask_size_over_2 = mask_size/2

nmasks = 300

suffix='UL'

print 'HashType mask[' + str(nmasks) + '] = {'
for i in range(nmasks):
    combo = random_combination(xrange(mask_size),mask_size_over_2)
    mask = 0;
    for j in combo:
        mask |= (1<<j);
    if(i<nmasks-1):
        print '\t' + str(mask) + suffix + ','
    else:
        print '\t' + str(mask) + suffix + ' };'

С++-массив, созданный с помощью script, используется следующим образом:

typedef int_least64_t HashType;

const int maxTableSize = 300;

HashType mask[maxTableSize] = {
  // generated array goes here
};

inline HashType xorrer(HashType const &l, HashType const &r) {
  return l^mask[r];
}

HashType hashConfig(HashType *sequence, int n) {
  return std::accumulate(sequence, sequence+n, (HashType)0, xorrer);
}

Этот алгоритм, безусловно, самый быстрый из тех, что я пробовал (this, this с кубами и этот с битрейтом размером 300). Для моих "типичных" последовательностей целых чисел скорости столкновений меньше 1E-7, что вполне приемлемо для моей цели.