Хэш-таблица против сбалансированного двоичного дерева

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


person peoro    schedule 31.01.2011    source источник
comment
stackoverflow.com/ вопросы/4128546/   -  person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 25.09.2017


Ответы (11)


Боюсь, на этот вопрос нельзя ответить вообще.

Проблема в том, что существует много типов хеш-таблиц и сбалансированных двоичных деревьев, и их производительность сильно различается.

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

Для более подробного ответа давайте рассмотрим некоторые альтернативы.

Хэш-таблица (см. статью в Википедии, где приведены некоторые основы)

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

Бинарное дерево

  • Повторная балансировка обходится дорого, вы можете рассмотреть Skip-List (также лучше для многопоточного доступа) или Splay Tree.
  • Хороший распределитель может «упаковывать» узлы вместе в памяти (лучшее поведение при кэшировании), даже если это не решает проблему поиска указателя.
  • B-Tree и варианты также предлагают «упаковку»

Не будем забывать, что O(1) — это асимптотическая сложность. Для нескольких элементов коэффициент обычно более важен (с точки зрения производительности). Что особенно верно, если ваша хеш-функция медленная...

Наконец, для наборов вы также можете рассмотреть вероятностные структуры данных, такие как фильтры Блума.

person Matthieu M.    schedule 31.01.2011
comment
@ProfVersaggi: На самом деле это даже не так; некоторые хеш-таблицы плохо обрабатывают дубликаты, но некоторые хорошо. Я советую вам прочитать записи Хоакина М. Лопеса Муньоса по теме< /а>. Он создал и поддерживает Boost MultiIndex. - person Matthieu M.; 17.01.2014

Хэш-таблицы, как правило, лучше, если нет необходимости хранить данные в какой-либо последовательности. Двоичные деревья лучше, если данные должны быть отсортированы.

person supercat    schedule 31.01.2011
comment
Не поддерживая сортировку, хэш-таблицы, которые могут поддерживать (вставлять) порядок, несколько тривиальны. - person ; 31.01.2011
comment
Это не так просто. Я боюсь нескольких вещей: 1. хеш-таблицы имеют плохую производительность (O(n)) в худшем случае 2. чтобы изменить размер хеш-таблицы, мне нужно что-то перехешировать, это довольно дорого. Этот вопрос заключается в том, чтобы узнать, как я могу избежать таких моментов, и получить информацию о других проблемах, которые мне не хватает. - person peoro; 31.01.2011
comment
pst: поддержание порядка вставки возможно практически с любой коллекцией «черных ящиков»; в какой степени можно поддерживать порядок сортировки с хеш-таблицей лучше, чем с «черным ящиком»? - person supercat; 31.01.2011
comment
@peoro: O (n) практически невозможно, если только кто-то не знает детали вашей реализации и просто не хочет вас сломать. Даже учитывая операцию изменения размера (происходит с разумным интервалом), хэш стоит намного меньше, чем сбалансированное дерево. - person Haozhun; 31.01.2011
comment
@peoro: Чтобы усилить точку зрения Джина, если каждое изменение размера в хеш-таблице удваивает размер (довольно типично), то сразу после изменения размера половина элементов будет повторно хеширована один раз, четверть из них дважды, восьмая из них три раза. , 1/16 из них четыре раза и т. д., поэтому независимо от того, насколько велика таблица, среднее количество перефразировок будет меньше двух. Тем не менее, мысль о вырожденных ситуациях хеширования хороша. Например, если создать Dictionary типа struct без переопределения GetHashCode, а многие ключи имеют одинаковое значение в первом поле, производительность будет низкой. - person supercat; 31.01.2011
comment
@peoro: повторное хеширование обычно не требуется (хэш может храниться вместе с данными), увеличение хеш-таблицы может быть выполнено в режиме онлайн (этап миграции), а наихудший случай O (n) может быть смягчен структурой хеш-таблицы (и есть из чего выбрать). - person Matthieu M.; 01.02.2011
comment
@MatthieuM.: Наихудший случай O(n) для хеш-таблицы неизбежен, если только количество различных значений, которые может принимать элемент, не ограничено). В противном случае ни одна хеш-таблица не может достичь поведения лучше, чем O(n), если все элементы, которые ей заданы, неразличимы, за исключением операции проверки на равенство (т. доступны битовые сканирования или другие подобные операции). - person supercat; 11.06.2012
comment
@supercat: ты прав, конечно. Однако простая хэш-таблица — это всего лишь массив связанных списков, а связанные списки имеют тенденцию к быстрому вырождению, в то время как, если вы используете массив хэш-таблиц с открытыми адресами, у вас меньше шансов добраться до этого наихудшего случая. Это то, что я имел в виду под облегченным; не то, чтобы это было совершенно неуместно, но то, что, выбрав другую структуру, вы могли значительно уменьшить шансы на то, что это произойдет. - person Matthieu M.; 12.06.2012
comment
@MatthieuM.: Некоторые хеш-таблицы склонны к линейной производительности, если количество элементов приближается к размеру таблицы, независимо от того, насколько хороша хэш-функция. Эффективная разработка алгоритма может уменьшить эту опасность. Хеш-таблица обречена на производительность O(n), если хэш-функция плохая, независимо от того, какой алгоритм таблица пытается использовать. - person supercat; 12.06.2012
comment
В C++ std::map — красно-черное дерево, а std::unordered_map — хеш-таблица. Обычно я предпочитаю std::map, так как я уверен, что никогда не столкнусь с проблемами повторного хэширования, которое может, например, разрушать системы жесткого реального времени. - person Erik Alapää; 01.04.2015

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

В следующем двоичном дереве предполагается, что оно является самобалансирующимся, например, красно-черное дерево, дерево AVL или дерево.

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

Бинарные деревья проще реализовать на чисто функциональных языках.

Двоичные деревья имеют естественный порядок сортировки и естественный способ обхода дерева для всех элементов.

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

Хеш-таблицы составляют почти O (1) (в зависимости от того, как вы обрабатываете коэффициент загрузки) по сравнению с деревьями бинов O (lg n).

Деревья, как правило, являются «средним исполнителем». Нет ничего, что они делают особенно хорошо, но и ничего особенно плохого они не делают.

person I GIVE CRAP ANSWERS    schedule 31.01.2011

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

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

Сложность вставки в наихудшем случае для хеш-таблицы можно оставить равной O(1)/O(log K) (где K — количество элементов с одним и тем же хэшем), если допустимо увеличить сложность поиска в наихудшем случае до O( K) или O(log K), если элементы можно отсортировать.

Инварианты как для деревьев, так и для хеш-таблиц дорого восстанавливаются при изменении ключей, но меньше, чем O (n log N) для отсортированных массивов.

Вот факторы, которые следует учитывать при принятии решения о том, какую реализацию использовать:

  1. Наличие отношений общего порядка.
  2. Наличие хорошей хэш-функции для отношения эквивалентности.
  3. Априорное знание количества элементов.
  4. Знание скорости вставки, удаления и поиска.
  5. Относительная сложность функций сравнения и хеширования.
person Apalala    schedule 31.01.2011
comment
Двоичное дерево поиска требует отношения полного порядка между ключами. Для хеш-таблицы требуется только отношение эквивалентности или идентичности с согласованной хэш-функцией. Это заблуждение. Двоичное дерево поиска всегда может использовать те же ключи, что и хеш-таблица: хэш-значения. Это не ограничение на случаи использования деревьев по сравнению с хеш-таблицами. - person rlibby; 10.02.2011
comment
@rlibby Хотя в большинстве реализаций хэш-ключей по умолчанию используются типы, для которых определен общий порядок (целые числа или указатели), требуется только эквивалентность, если вы предоставляете свои собственные хэши. Таким образом, в общем случае вы не можете использовать бинарное дерево поиска по хеш-ключам, потому что вы не знаете, что такое хэши, откуда они взялись, или, тем более, если они поддерживают отношение полного порядка. - person Apalala; 18.02.2011
comment
но если я правильно понимаю ваше предложение, то такое хэш-значение также нельзя использовать в хеш-таблице. Конечно, если его можно использовать в хэш-таблице, то его можно также использовать в наборе деревьев. Если его можно использовать в таблице, то он должен сопоставляться с некоторым индексом в таблице. Можно использовать функцию, которая генерирует этот индекс, для генерации ключей для набора деревьев. - person rlibby; 21.02.2011
comment
@rlibby Хэш-таблица требует, чтобы одинаковые элементы имели одинаковый хэш, но не требует, чтобы разные элементы имели разные хэши. Если разные элементы имеют один и тот же хэш, то отношения полного порядка не существует. - person Apalala; 12.11.2012
comment
Если разные элементы часто имеют одинаковые хэши, хеш-таблица все равно будет очень медленной. Действительно, можно хранить связанный список в каждом узле двоичного дерева точно так же, как связанный список в каждой записи хеш-таблицы. - person SOFe; 08.03.2019

Хэш-таблицы обеспечивают более быстрый поиск:

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

Бинарные деревья:

  • Детерминированный. O(log n) Я думаю...
  • Не нужно дополнительное пространство, как хэш-таблицы
  • Должен содержаться в порядке. Добавление элемента в середине означает перемещение остальных.
person whitey04    schedule 31.01.2011
comment
Что вы имеете в виду, когда говорите, что бинарные деревья детерминированы? Хеш-таблицы также являются детерминированными. Кроме того, операции с бинарными деревьями выполняются за O(h), где h — высота. Если это сбалансированное бинарное дерево, то h=O(log(n)). - person Daniel Egeberg; 31.01.2011
comment
Не правда! Хэш-таблицы могут отсутствовать. Например, если у вас есть массив из 10 и вы используете номер телефона для его индексации (например, используйте модуль), вы можете получить хэш, который указывает вам на первый элемент массива. Однако, если при построении массива сначала использовалось 9 других чисел с таким же хешем; вам действительно нужно пройти весь путь до последнего элемента. В бинарном поиске вы гарантированно получите BigO(log n), несмотря ни на что. !ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ! Все зависит от того, как вы строите свою сортировку/поиск по хешу. Есть много способов... - person whitey04; 31.01.2011
comment
Добавление элемента в середине не означает перемещение остальных элементов. Это связанная структура данных, а не массив (возможно, вы путаете двоичное дерево поиска с двоичным поиском, которые являются двумя очень разными вещами. Все операции - O (log (n)), если добавление/удаление в середину означало перемещение остальных было бы O (n). - person MAK; 31.01.2011
comment
Все зависит от того, как вы это реализуете... Использование связанного дерева - хороший способ обойти проблему вставки бинарного поиска. Однако бинарный поиск (с деревом под ним или без него) всегда будет возвращать результат в O (log n). Хэш не может, если входной ключ не равен 1: 1 с сгенерированным хешем. - person whitey04; 31.01.2011

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

person biziclop    schedule 31.01.2011

Чтобы добавить к другим замечательным ответам выше, я бы сказал:

Используйте хеш-таблицу, если объем данных не изменится (например, хранение констант); но, если количество данных изменится, используйте дерево. Это связано с тем, что в хеш-таблице после достижения коэффициента загрузки размер хеш-таблицы должен измениться. Операция изменения размера может быть очень медленной.

person David Weiser    schedule 31.01.2011
comment
В наихудшем случае время добавления элемента в хеш-таблицу составляет O(n) из-за изменения размера, но если размер хеш-таблицы каждый раз удваивается, доля добавлений, требующих повторного хеширования, будет уменьшаться по мере увеличения размера таблицы. Среднее количество операций повторного хэширования на элемент никогда не превысит двух, независимо от того, насколько большой будет таблица. - person supercat; 31.01.2011
comment
Если размер хэш-таблицы удваивается, то я был бы удивлен, если бы количество коллизий уменьшилось, потому что хеш-таблицы работают лучше всего (т.е. меньшее количество коллизий), когда размер таблицы является простым. Кроме того, если вы просите систему предоставить вам в два раза больше памяти при каждом изменении размера, у вас быстро закончится память (или замедлится работа системы, если система переупорядочивает свою память, чтобы предоставить вам объем непрерывной памяти, который вы Просишь). - person David Weiser; 31.01.2011
comment
удвоение является общей стратегией, но это не требуется. Нужен экспоненциальный рост. Вы можете выбрать меньшую экспоненту, если хотите, это просто будет означать, что среднее количество операций перехеширования будет выше. В любом случае амортизированная стоимость n вставок в таблицу с экспоненциальным ростом составляет O(n), в то время как самобалансирующиеся бинарные деревья поиска стоят O(n*log(n)). - person rlibby; 10.02.2011

Один момент, который, я думаю, не был рассмотрен, заключается в том, что деревья намного лучше подходят для постоянных структур данных. То есть неизменяемые структуры. Стандартную хеш-таблицу (т. е. такую, которая использует один массив связанных списков) нельзя изменить без изменения всей таблицы. Одна из ситуаций, в которой это имеет значение, — это когда две параллельные функции имеют копию хэш-таблицы, и одна из них изменяет таблицу (если таблица является изменяемой, это изменение будет видно и другой). Другая ситуация может быть примерно такой:

def bar(table):
    # some intern stuck this line of code in
    table["hello"] = "world"
    return table["the answer"]

def foo(x, y, table):
    z = bar(table)
    if "hello" in table:
        raise Exception("failed catastrophically!")
    return x + y + z

important_result = foo(1, 2, {
    "the answer": 5,
    "this table": "doesn't contain hello", 
    "so it should": "be ok"
})
# catastrophic failure occurs

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

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

Это означает, что эффективное дерево может быть очень удобным, когда желательны неизменяемые карты.

person limp_chimp    schedule 15.11.2013

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

person Darius Bacon    schedule 31.01.2011

По моему опыту, hastable всегда быстрее, потому что деревья слишком сильно страдают от эффектов кеша.

Чтобы увидеть некоторые реальные данные, вы можете проверить страницу тестов моей библиотеки TommyDS http://tommyds.sourceforge.net/

Здесь вы можете увидеть сравнение производительности наиболее распространенных доступных хеш-таблиц, деревьев и библиотек.

person amadvance    schedule 05.02.2011

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

person Yogesh Umesh Vaity    schedule 31.05.2016