Компактная структура данных для хранения и поиска через большой набор (равномерно распределенных) целых чисел - PullRequest
3 голосов
/ 18 марта 2012

От меня требуется хранить в памяти и просматривать один миллион равномерно распределенных целых чисел. Моя рабочая нагрузка чрезвычайно интенсивна.
Моя текущая реализация использует HashSet (Java). Я вижу хорошую производительность при поиске, но использование памяти не идеально (десятки МБ).
Не могли бы вы придумать более эффективную (память) структуру данных?
Редактировать: решение должно будет поддерживать небольшое количество дополнений к структуре данных.

Справочная информация:
Вышеуказанная проблема целых чисел является упрощением следующей проблемы:
У меня есть набор из миллиона строк (мой «Словарь»), и я хочу сказать, содержит ли словарь заданную строку или нет.
Словарь слишком велик, чтобы поместиться в памяти, поэтому я готов пожертвовать чуть-чуть точностью, чтобы уменьшить объем памяти. Я сделаю это, переключившись на словарь, содержащий значение хеш-кода каждой строки (целое число) вместо действительных символов. Я предполагаю, что вероятность столкновения для каждой строки составляет всего 1M/2^32.

Ответы [ 7 ]

12 голосов
/ 18 марта 2012

Хотя ответ Джона Скита дает хорошую экономию при небольших инвестициях, я думаю, вы можете добиться большего.Поскольку ваши числа довольно равномерно распределены, вы можете использовать интерполяционный поиск для более быстрого поиска (примерно O (log log N) вместо O (log N)).Для миллиона предметов вы, вероятно, можете запланировать около 4 сравнений вместо 20.

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

enter image description here

Вы бы разбили свое (предположительно) 32-разрядное целое число на две 16-разрядные части,Вы использовали бы первые 16 битов в качестве индекса первого уровня таблицы поиска.На этом уровне у вас будет 65536 указателей, по одному на каждое возможное 16-битное значение для этой части вашего целого числа.Это приведет вас ко второму уровню таблицы.Для этой части мы выполнили бы бинарный или интерполяционный поиск между выбранным указателем и следующим повышением, т. Е. Всеми значениями второго уровня, которые имели такое же значение в первых 16 битах.

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

Это означает, что вместо второго уровня, занимающего 4 мегабайта, мы сократили его до 2 мегабайт.Наряду с этим нам нужна таблица первого уровня, но она составляет всего 65536x4 = 256 Кбайт.

Это почти наверняка улучшит скорость по сравнению с бинарным поиском всего набора данных.В худшем случае (используя бинарный поиск для второго уровня) у нас может быть целых 17 сравнений (1 + log 2 65536).Среднее значение будет лучше, чем это, хотя - поскольку у нас есть только миллион элементов, в каждом «разделе» второго уровня может быть в среднем только 1_000_000 / 65536 = ~ 15 элементов, что дает примерно 1 + log 2 (16) = 5 сравнений.Использование интерполяционного поиска на втором уровне может немного уменьшить это, но когда вы только начинаете с 5 сравнений, у вас не остается много места для действительно существенных улучшений.Учитывая в среднем всего ~ 15 элементов на втором уровне, тип поиска, который вы используете, не будет иметь большого значения - даже линейный поиск будет довольно быстрым.

Конечно, если вы хотите, вы можете пойти еще дальше и использовать вместо этого четырехуровневую таблицу (по одному на каждый байт в целом числе).Однако может возникнуть вопрос, сможет ли это сэкономить вам достаточно денег, чтобы стоить того, что стоит.По крайней мере, сразу, я сразу догадываюсь, что вы проделали бы довольно много дополнительной работы для довольно минимальной экономии (простое хранение последних байтов миллиона целых чисел, очевидно, занимает 1 мегабайт, и три уровня таблицы, ведущие к этому, явноЗанимайте приличную сумму больше, так что вы удвоите количество уровней, чтобы сэкономить что-то наполовину мегабайт. Если вы находитесь в ситуации, когда сохранение немного больше будет иметь большое значение, пойти на это - но в противном случае,Я сомневаюсь, оправдывает ли возврат дополнительные инвестиции.

4 голосов
/ 18 марта 2012

Звучит так, как будто вы можете просто сохранить отсортированный int[] и затем выполнить бинарный поиск.С миллионами значений это ~ 20 сравнений, чтобы получить любое значение - достаточно ли этого будет быстро?

3 голосов
/ 18 марта 2012

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

Фильтр Блума состоит из k хеш-функций и таблицы из n битов, изначально пустой. Чтобы добавить элемент в таблицу, введите его в каждую из хеш-функций k (получая число от 0 до n -1) и установите соответствующий бит. Чтобы проверить, есть ли элемент в таблице, передайте его каждой хэш-функции k и посмотрите, установлены ли все соответствующие биты k .

Фильтр Блума с частотой ложных срабатываний 1% требует около 10 бит на элемент; частота ложных срабатываний быстро уменьшается, когда вы добавляете больше битов на элемент.

Вот реализация с открытым исходным кодом в Java.

1 голос
/ 28 апреля 2018

В проекте Github есть некоторая реализация Java наборов для целых чисел с уменьшенным потреблением памяти. LargeIntegerSet .

1 голос
/ 18 марта 2012

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

0 голосов
/ 30 июля 2016

Я думаю, что вы могли бы пересмотреть исходную проблему (имея эффективный список слов), вместо того, чтобы пытаться оптимизировать «оптимизацию».

Я бы предложил рассмотреть дерево Radix / Trie.

https://en.wikipedia.org/wiki/Radix_tree или https://en.wikipedia.org/wiki/Trie

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

Radix tree example

Некоторые примеры реализаций:

https://lucene.apache.org/core/4_0_0/analyzers-stempel/org/egothor/stemmer/Trie.html

https://github.com/rkapsi/patricia-trie

https://github.com/npgall/concurrent-trees

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

http://bhavin.directi.com/to-trie-or-not-to-trie-a-comparison-of-efficient-data-structures/

0 голосов
/ 18 марта 2012

Существует несколько реализаций IntHashSet для примитивов.

Быстрое приближение ко мне дало мне это . Существует также апачская [open source] реализация IntHashSet . Я бы предпочел реализацию apache, хотя она имеет некоторые издержки [она реализована как IntToIntMap ]

...