Сравнить Хэш с двоичным деревом поиска
Мы все знаем, что хэш-таблица имеет O (1) время для обеих вставок и look-ups, если хеш-функция была выбрана хорошо. Итак, в чем причина, по которой мы хотим использовать двоичное дерево поиска? Просто потому, что идеальную хеш-функцию сложно проектировать?
Вот как я придумал этот вопрос?
Я заметил, что Стандартная С++ STL имеет set
и map
, которые реализованы с деревом двоичного поиска, но не имеют хеша (не говоря уже о нестандарте hash_set
, hash_map
). Хотя, Ruby имеет только Hash
. Я хочу понять рациональность этой разницы.
Ответы
Ответ 1
Деревья допускают прерывание в порядке.
В худшем случае для хэш-таблицы используется O (N) (линейный поиск по одному ведру), двоичный поиск связан O (log N).
NB: для этого требуется, чтобы дерево было сбалансировано - поэтому типичная реализация использует дерево с балансировкой, suhc как красно-черное дерево.
В то время как такое ухудшение маловероятно, это не невозможно и сильно зависит от способности выбирать подходящую хеш-функцию и распределение фактических данных.
Реализация дерева также увеличивается тривиально до требуемого размера, тогда как хэш-карта начинает ухудшаться, когда она заполняется (для большинства реализаций она заявила, что около 70% заполненных ведер). Вам нужно либо перефразировать всю таблицу (опять-таки, плохие приложения реального времени), либо постепенно перейти к новой таблице, что не является простой реализацией.
В конце концов, STL, вероятно, просто отправился с одним "базовым" шаблоном контейнера - деревом, чтобы избежать дополнительной сложности реализации.
Ответ 2
Чтобы добавить в ответ peterchen, структуры хэша, хотя теоретически быстрее при вставке и удалении существенно зависят от фактических данных, выбранной хэш-функции и количества данных.
- Идеальная хэш-функция зависит от количества и распределения данных.
Наличие больших вариаций производительности между лучшими и худшими случаями делает их непригодными для структур общего назначения. Бинарные деревья, с другой стороны, более предсказуемы независимо от количества/типа используемых данных, хотя и менее эффективны в наилучшем случае.
Ответ 3
STL первоначально не включал хэш-таблицу среди контейнеров, поскольку хэш-таблицы более сложны - вам нужно выбирать между открытой и закрытой адресацией, не говоря уже о хеш-функции и т.д. В то время Степанов и Страуструп были пытаясь ускорить прогресс на нем, чтобы он был быстро принят в стандарт.
Деревья, с другой стороны, относительно просты. Уже было известно, что поскольку это структуры данных в памяти, мы можем просто использовать двоичное дерево вместо B-дерева. Тогда это был выбор между деревьями AVL и RB. Деревья RB, как правило, выбираются из-за лучших характеристик производительности, о которых я не могу комментировать, но статьи в Википедии по обоим структурам (AVL и RB) расскажет вам более подробно.
В противном случае деревья и хэш-таблицы хороши для разных вещей. Если вам нужны быстрые вставки или поиск, и они не могут заботиться о заказе, в котором они хранятся, хеш-таблицы хороши. Если вам нужны характеристики заказа и надежные гарантии при вставках и извлечении, то бинарные деревья хороши. Другим хорошим правилом является профиль. Так как большинство применений либо совместимы с интерфейсом, но также помогает профилирование, которое дает вам лучшую производительность.
Ответ 4
Вы можете получить доступ к данным в двоичном дереве поиска по порядку.
Ответ 5
Ну, деревья поиска поиска упорядочены, хешей нет.
Ответ 6
Чтобы использовать дерево, вам нужно заказать предметы в дереве. Для использования хэш-таблицы вам нужна функция для вычисления хэш-значения элемента в хеш-таблице.
Интересно, что .NET framework требует, чтобы каждый класс реализовал (или наследовал) функцию GetHashCode
, позволяющую хранить каждый объект в хеш-таблице. Однако это также добавляет дополнительную нагрузку на разработчиков, которые необходимы для реализации семантически правильных хеш-функций, даже если они не предполагают хэширование класса. Одним из решений является возвращение постоянного значения из GetHashCode
, которое семантически корректно, но не очень эффективно, если функция когда-либо используется для хеширования.
Ответ 7
Если вам это удастся, вы всегда должны предпочесть хэш над двоичным деревом поиска. Хэши имеют более высокие издержки памяти, чем деревья, но вся память, которую они используют, может быть выделена в одном большом блоке. Для деревьев каждый добавленный node требует отдельного выделения, которое вызывает высокую фрагментацию и плохо для производительности. Подобно тому, как вы скорее прочитали бы 1000 байт из 1 файла, чем 1 байт из 1000 разных файлов.
Случай, когда хеши не работают, - это упорядочение вопросов. Например, предположим, что вы пишете распределитель памяти и вы храните свободные блоки памяти в структуре данных. Ключи - это размеры блоков, а значения - указатели на них.
Запрос на память влечет за собой просмотр этой структуры данных и поиск самого маленького (подразумевает порядок!) блока, удовлетворяющего запросу. Например, если у вас есть блоки с ключами 10, 20, 30 и запрос на 20 байтов памяти, вы выбираете второй блок. Хешмап может сделать это легко.
Но что, если запрос на 22 байта? Поскольку ключ с значением 20 отсутствует, вам нужно выполнить итерацию всей хэш-карты, чтобы найти правую клавишу (30), которая является операцией O (n). Но если вы использовали дерево, то "найти наименьший ключ, превышающий заданный ключ", является операцией O (log n).
Ответ 8
Во времена С++ люди по-прежнему были поклонниками жесткого академического подхода к структурам данных и алгоритмам, поэтому они предпочли структуры с меньшим объемом памяти и хорошо понятным поведением лучшего и худшего случая.
К тому времени, когда появился Ruby, и для целей сценариев люди поняли, что они предпочитают простоту в отношении сырой производительности, а поскольку hashtables позволяют семантику обоих массивов (если вы используете последовательный индекс как ключ) И словарей (если вы используете естественный ключ), они считались более универсальной структурой данных.