Почему std::map
реализован в виде красно-черного дерева?
Существует несколько сбалансированных бинарных поисковых деревьев (BST). Каковы были дизайнерские компромиссы при выборе красно-черного дерева?
Почему std::map
реализован в виде красно-черного дерева?
Существует несколько сбалансированных бинарных поисковых деревьев (BST). Каковы были дизайнерские компромиссы при выборе красно-черного дерева?
Вероятно, два наиболее общих алгоритма дерева балансировки: Красно-черные деревья и Деревья AVL. Чтобы сбалансировать дерево после вставки/обновления, оба алгоритма используют понятие поворота, где узлы дерева вращаются для выполнения повторной балансировки.
В обоих алгоритмах операции вставки/удаления являются O (log n), в случае повторного балансировки дерева Red-Black это O ( 1), в то время как с AVL это O (log n), что делает дерево Red-Black более эффективным в этом аспекте стадии повторной балансировки и одной из возможных причин, по которой он более широко используется.
Деревья Red-Black используются в большинстве библиотек коллекции, включая предложения от Java и Microsoft.NET Framework.
Это действительно зависит от использования. Дерево AVL обычно имеет больше поворотов перебалансировки. Поэтому, если ваше приложение не имеет слишком большого количества операций вставки и удаления, но сильно зависит от поиска, то, вероятно, AVL-дерево является хорошим выбором.
std::map
использует дерево Red-Black, поскольку оно получает разумный компромисс между скоростью node вставки/удаления и поиска.
Деревья AVL имеют максимальную высоту 1,44 лога, а деревья RB - максимум 2 лога. Вставка элемента в AVL может привести к перебалансированию в одной точке дерева. Перебалансировка завершает вставку. После вставки нового листа, обновление предков этого листа должно быть выполнено вплоть до корня или до точки, где два поддерева имеют равную глубину. Вероятность обновления k узлов составляет 1/3 ^ k. Перебалансировка - это O (1). Удаление элемента может подразумевать более одного перебалансирования (до половины глубины дерева).
RB-деревья - это B-деревья порядка 4, представленные в виде двоичных деревьев поиска. 4-узел в B-дереве приводит к двум уровням в эквивалентном BST. В худшем случае все узлы дерева являются 2-узлами, и только одна цепочка из 3-х узлов доходит до листа. Этот лист будет на расстоянии 2 лога от корня.
Спускаясь от корня к точке вставки, нужно изменить 4-узлы на 2-узлы, чтобы любая вставка не насытила лист. Возвращаясь из вставки, все эти узлы должны быть проанализированы, чтобы убедиться, что они правильно представляют 4-узлы. Это также можно сделать, спустившись на дерево. Глобальная стоимость будет такой же. Там нет бесплатного обеда! Удаление элемента из дерева того же порядка.
Все эти деревья требуют, чтобы узлы содержали информацию о росте, весе, цвете и т.д. Только деревья Splay свободны от такой дополнительной информации. Но большинство людей боятся Splay-деревьев из-за неуклюжести их структуры!
Наконец, деревья также могут нести информацию о весе в узлах, что позволяет балансировать вес. Различные схемы могут быть применены. Необходимо сбалансировать, когда поддерево содержит более чем в 3 раза больше элементов другого поддерева. Повторная балансировка выполняется либо через одно, либо через двойное вращение. Это означает худший случай 2,4 лога. Можно обойтись 2 раза вместо 3, что намного лучше, но это может означать, что чуть-чуть менее 1% поддеревьев будет неуравновешенным. Tricky!
Какой тип дерева лучше? AVL точно. Их проще всего кодировать, а их худшая высота ближе всего к logn. Для дерева из 1000000 элементов AVL будет иметь максимальную высоту 29, RB 40 и весовой баланс 36 или 50 в зависимости от соотношения.
Есть много других переменных: случайность, соотношение добавлений, удалений, поисков и т.д.
Предыдущие ответы касаются только альтернатив дерева, и красный черный цвет, вероятно, остается только по историческим причинам.
Почему не хеш-таблица?
Для типа требуется только <
сравнение для использования в качестве ключа в дереве. Однако для хеш-таблиц требуется, чтобы для каждого типа ключей была определена hash
функция. Соблюдение минимальных требований к типу очень важно для общего программирования.
Создание хорошей хеш-таблицы требует глубокого знания контекста, в котором она будет использоваться. Должен ли он использовать открытую адресацию или связанную цепочку? Какие уровни нагрузки следует принять перед изменением размера? Следует ли использовать дорогой хеш, который избегает столкновений, или грубый и быстрый?
Поскольку STL не может предугадать, какой вариант является лучшим для вашего приложения, значение по умолчанию должно быть более гибким. Деревья "просто работают" и хорошо масштабируются.
(С++ 11 действительно добавил хеш-таблицы с помощью unordered_map
. Из документации видно, что для настройки многих из этих параметров требуется настройка политик.)
А как насчет других деревьев?
Красное Черное дерево предлагает быстрый поиск и является самобалансирующимся, в отличие от BST. Другой пользователь указал на свои преимущества перед самобалансирующимся деревом AVL.
Александр Степанов (создатель STL) сказал, что он будет использовать дерево B * вместо красно-черного дерева, если он снова напишет std::map
, потому что это более удобно для современных кэшей памяти.
Одним из самых больших изменений с тех пор стал рост кешей. Промахи в кеше очень дороги, так что местность ссылок сейчас гораздо важнее. Структуры данных на основе узлов, которые имеют низкую локальность ссылок, имеют гораздо меньше смысла. Если бы я проектировал STL сегодня, у меня был бы другой набор контейнеров. Например, B * -tree в памяти является гораздо лучшим выбором, чем красно-черное дерево для реализации ассоциативного контейнера. - Александр Степанов
Вы должны всегда использовать красное черное дерево или дерево B *?
В других случаях Алекс заявлял, что std::vector
почти всегда является лучшим контейнером списка по аналогичным причинам. Редко имеет смысл использовать std::list
или std::deque
даже для тех ситуаций, которым нас учили в школе (таких как удаление элемента из середины списка). std::vector
настолько быстр, что превосходит эти структуры для всего, кроме большого N
Применяя это рассуждение, если у вас есть только небольшое количество элементов (сотни?), Использование std::vector
и линейный поиск могут быть более эффективными, чем реализация дерева std::map
. В зависимости от частоты вставки сортированный std::vector
сочетании с std::binary_search
может быть самым быстрым выбором.
Обновление 2017-06-14: webbertiger измените свой ответ после того, как я прокомментировал. Я должен отметить, что его ответ теперь намного лучше для моих глаз. Но я сохранил свой ответ как дополнительную информацию...
Из-за того, что я думаю, что первый ответ неправильный (исправление: больше не то и другое), а третий имеет неправильное подтверждение. Я чувствую, что должен был уточнить вещи...
2 самых популярных дерева - AVL и Red Black (RB). Основное отличие заключается в использовании:
Основное отличие заключается в окраске. У вас меньше действий по перебалансировке в дереве RB, чем у AVL, потому что раскраска позволяет вам иногда пропустить или сократить действия по перебалансировке, которые имеют относительную высокую стоимость. Из-за раскраски дерево RB также имеет более высокий уровень узлов, потому что оно может принимать красные узлы между черными (имея возможности в ~ 2 раза больше уровней), что делает поиск (чтение) немного менее эффективным... но потому что это постоянная (2x), она остается в O (log n).
Если вы считаете, что снижение производительности для модификации дерева (значимое) против повышения производительности консультации с деревом (почти незначительное), становится естественным предпочесть RB над AVL для общего случая.
Это просто выбор вашей реализации - они могут быть реализованы как любое сбалансированное дерево. Различные варианты сопоставимы с незначительными различиями. Поэтому любой такой же хороший, как любой.