Бинарные деревья против связанных списков против хэш-таблиц

Я создаю таблицу символов для проекта, над которым работаю. Мне было интересно, каковы мнения людей о преимуществах и недостатках различных методов, доступных для хранения и создания таблицы символов.

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

10 ответов


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

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

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

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

вы проходили курс по структурам данных и алгоритмам в университете?


применяются стандартные компромиссы между этими структурами данных.

  • Бинарные Деревья
    • средняя сложность для реализации (при условии, что вы не можете получить их из библиотеки)
    • вставки O (logN)
    • поиск O (logN)
  • связанные списки (несортированный)
    • низкая сложность реализации
    • вставки O (1)
    • поиск O (N)
  • хэш таблицы
    • высокая сложность реализации
    • вставки O (1) в среднем
    • поиск O (1) в среднем

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

есть знаменитый qoute из заметок пайка о программировании в C: "Правило 3. Причудливые алгоритмы медленны, когда n мало, а n обычно мало. Причудливые алгоритмы имеют большие константы. Пока вы не знаете, что n часто будет большим, не фантазируйте." http://www.lysator.liu.se/c/pikestyle.html

Я не могу сказать из вашего поста, будете ли вы иметь дело с маленьким N или нет, но всегда помните, что лучший алгоритм для больших N не обязательно хорош для маленьких Ns.


похоже, что все это может быть правдой:

  • ключи являются строками.
  • вставки выполняются один раз.
  • поиск выполняется часто.
  • количество пар ключ-значение относительно невелико (скажем, меньше, чем K или около того).

Если это так, вы можете рассмотреть отсортированный список над любой из этих других структур. Это будет работать хуже, чем другие во время вставок, так как отсортированный список O (N) на вставке против O (1) для a связанный список или хэш-таблица и O (log2N) для сбалансированного двоичного дерева. Но поиск в отсортированном списке может быть быстрее, чем любая из этих структур (я объясню это вкратце), поэтому вы можете выйти на первое место. Кроме того, если вы выполняете все вставки сразу (или иначе не требуете поиска, пока все вставки не будут завершены), вы можете упростить вставки до O(1) и сделать одну гораздо более быструю сортировку в конце. Более того, сортированный список использует меньше памяти, чем любой из этих структуры, но единственный способ, которым это может иметь значение, - это иметь много небольших списков. Если у вас есть один или несколько больших списков, то хэш-таблица, скорее всего, выполнит сортированный список.

Почему поиск может быть быстрее с отсортированный список? Ну, ясно, что это быстрее, чем связанный список, с последним временем поиска O(N). С двоичным деревом поиск остается только O (log2 N), если дерево остается идеально сбалансированной. Держать дерево сбалансированным (красно-черный, для instance) добавляет к сложности и времени вставки. Кроме того, как со связанными списками, так и с двоичными деревьями каждый элемент является отдельно выделенным1узел, что означает, что вам придется разыменовать указатели и, вероятно, перейти к потенциально широко варьирующимся адресам памяти, увеличивая шансы на промах кэша.

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

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

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

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

Мне нравится ответ Билла, но он на самом деле не синтезирует вещи.

из трех вариантов:

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

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

Это оставляет деревья. Однако у вас есть выбор: балансировать или не балансировать. Изучая эту проблему в коде C и Fortran, я обнаружил, что входные данные таблицы символов имеют тенденцию быть достаточно случайными, что вы теряете только один или два уровня дерева, не балансируя дерево. Учитывая, что сбалансированные деревья медленнее вставлять элементы и сложнее реализовать, я бы не стал займись ими. Однако, если у вас уже есть доступ к хорошим отлаженным библиотекам компонентов (например, STL C++), вы можете также пойти вперед и использовать сбалансированное дерево.


несколько вещей, чтобы следить за.

  • двоичные деревья имеют только o (log n) поиск и сложность вставки, если дерево сбалансированной. Если ваши символы вставлены довольно случайным образом, это не должно быть проблемой. Если они вставлены по порядку, вы будете создавать связанный список. (Для вашего конкретного приложения они не должны быть в каком-либо порядке, поэтому вы должны быть в порядке.) Если есть шанс, что символы будут слишком упорядоченно, Красный-Черный дерево-лучший вариант.

  • хэш-таблицы дают O (1) среднюю сложность вставки и поиска, но здесь тоже есть предостережение. Если ваша хэш-функция плоха (и я имею в виду действительно bad) вы также можете создать связанный список здесь. Однако любая разумная строковая хэш-функция должна делать это, поэтому это предупреждение действительно только для того, чтобы убедиться, что вы знаете, что это может произойти. Вы должны быть в состоянии просто проверить, что хэш-функция не имеет много столкновений над ожидаемым диапазоном входных данных, и вы будете в порядке. Еще один незначительный недостаток - использование хэш-таблицы фиксированного размера. Большинство реализаций хэш-таблицы растут, когда они достигают определенного размера (коэффициент загрузки, чтобы быть более точным, см. здесь для деталей). Это делается, чтобы избежать проблемы, которую вы получаете, когда вставляете миллион символов в десять ведер. Это просто приводит к десяти связанным спискам со средним размером 100,000.

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


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

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

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

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


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


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


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

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

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