Почему std::map
реализован как красно-черное дерево?
Существует несколько сбалансированных двоичных деревьев поиска (BST). Какие компромиссы были при выборе красно-черного дерева?
Почему std::map
реализован как красно-черное дерево?
Существует несколько сбалансированных двоичных деревьев поиска (BST). Какие компромиссы были при выборе красно-черного дерева?
Вероятно, двумя наиболее распространенными алгоритмами самобалансирующегося дерева являются красно-черные деревья и деревья AVL. Чтобы сбалансировать дерево после вставки / обновления, оба алгоритма используют понятие вращения, при котором узлы дерева поворачиваются для выполнения повторной балансировки.
В то время как в обоих алгоритмах операции вставки / удаления выполняются O (log n), в случае перебалансировки красного и черного дерева вращение выполняется как O (1) операция, в то время как с AVL это O (log n), что делает Красно-Черное дерево более эффективным в этом аспекте этапа перебалансировки и является одной из возможных причин его более широкого использования.
Красно-черные деревья используются в большинстве библиотек коллекций, включая предложения от Java и Microsoft .NET Framework.
std::map
реализаций, выследить разработчиков и спросить их, по каким критериям они приняли решение, так что это остается предположением.
- person Steve Jessop; 14.03.2011
Это действительно зависит от использования. Дерево AVL обычно имеет больше вращений для ребалансировки. Так что, если ваше приложение не имеет слишком большого количества операций вставки и удаления, но сильно нагружает поиск, то дерево AVL, вероятно, будет хорошим выбором.
std::map
использует красно-черное дерево, поскольку оно обеспечивает разумный компромисс между скоростью вставки / удаления узла и поиском.
Деревья AVL имеют максимальную высоту 1,44 логна, в то время как деревья RB имеют максимальную высоту 2логна. Вставка элемента в AVL может означать перебалансировку в одной точке дерева. Перебалансировка завершает вставку. После вставки нового листа обновление предков этого листа должно выполняться до корня или до точки, в которой два поддерева имеют одинаковую глубину. Вероятность обновления k узлов составляет 1/3 ^ k. Ребалансировка - O (1). Удаление элемента может потребовать более одной перебалансировки (до половины глубины дерева).
RB-деревья - это B-деревья 4-го порядка, представленные как деревья двоичного поиска. Четыре узла в B-дереве приводят к двум уровням эквивалентного BST. В худшем случае все узлы дерева являются 2-узлами, и только одна цепочка из 3-узлов вниз до листа. Этот лист будет на расстоянии 2логн от корня.
Спускаясь от корня к точке вставки, нужно преобразовать 4 узла в 2 узла, чтобы убедиться, что любая вставка не приведет к насыщению листа. Вернувшись после вставки, все эти узлы необходимо проанализировать, чтобы убедиться, что они правильно представляют 4-узлы. Это также можно сделать, спустившись по дереву. Глобальная стоимость будет такой же. Бесплатных обедов нет! Удаление элемента из дерева происходит в том же порядке.
Все эти деревья требуют, чтобы узлы несли информацию о росте, весе, цвете и т. Д. Только деревья Splay не содержат такой дополнительной информации. Но большинство людей боятся Splay-деревьев из-за неуклюжести их структуры!
Наконец, деревья могут также нести информацию о весе в узлах, что позволяет балансировать вес. Могут применяться различные схемы. Следует перебалансировать, когда поддерево содержит более чем в 3 раза больше элементов, чем другое поддерево. Повторная балансировка снова выполняется посредством одинарного или двойного вращения. Это означает наихудший случай 2,4логн. Можно обойтись 2 раза вместо 3, это гораздо лучшее соотношение, но это может означать, что здесь и там будет разбалансировано чуть меньше 1% поддеревьев. Сложный!
Какой тип дерева лучше? AVL точно. Их проще всего кодировать, а их худшая высота ближе всего к входу в систему. Для дерева из 1000000 элементов AVL будет иметь максимальную высоту 29, RB 40 и сбалансированный вес 36 или 50 в зависимости от соотношения.
Есть много других переменных: случайность, соотношение добавлений, удалений, поисков и т. Д.
Предыдущие ответы касались только альтернатив дерева, а красный черный, вероятно, остался только по историческим причинам.
Почему не хеш-таблица?
Тип требует только использования оператора <
(сравнения) в качестве ключа в дереве. Однако хеш-таблицы требуют, чтобы для каждого типа ключа была определена hash
функция. Сведение к минимуму требований к типу очень важно для универсального программирования, поэтому вы можете использовать его с широким спектром типов и алгоритмов.
Создание хорошей хеш-таблицы требует глубокого знания контекста, в котором она будет использоваться. Следует ли использовать открытую адресацию или связанную цепочку? Какие уровни нагрузки он должен принять перед изменением размера? Должен ли он использовать дорогой хэш, избегающий коллизий, или грубый и быстрый?
Поскольку STL не может предвидеть, какой вариант лучше всего подходит для вашего приложения, значение по умолчанию должно быть более гибким. Деревья «просто работают» и хорошо масштабируются.
(В C ++ 11 были добавлены хеш-таблицы с unordered_map
. Вы можете видеть из документации для настройки многих из этих параметров требуется настройка политик.)
А как насчет других деревьев?
Красно-черные деревья предлагают быстрый поиск и самобалансируются, в отличие от BST. Другой пользователь указал на его преимущества перед самобалансирующимся деревом AVL.
Александр Степанов (создатель STL) сказал, что он будет использовать B * Tree вместо красно-черного дерева, если он снова напишет std::map
, потому что это более дружелюбно для современных кешей памяти.
С тех пор одним из самых больших изменений стал рост кешей. Промахи кеша обходятся очень дорого, поэтому теперь гораздо важнее местоположение ссылки. Структуры данных на основе узлов, которые имеют низкую локальность ссылок, имеют гораздо меньший смысл. Если бы я проектировал STL сегодня, у меня был бы другой набор контейнеров. Например, B * -дерево в памяти - гораздо лучший выбор, чем красно-черное дерево для реализации ассоциативного контейнера. - Александр Степанов
Должны ли карты всегда использовать деревья?
Другая возможная реализация карт - это сортированный вектор (сортировка вставкой) и двоичный поиск. Это будет хорошо работать для контейнеров, которые не часто модифицируются, но часто запрашиваются. Я часто делаю это в C, поскольку qsort
и bsearch
встроены.
Мне вообще нужно использовать карту?
Соображения о кешировании означают, что редко имеет смысл использовать std::list
или std::deque
вместо std:vector
даже в тех ситуациях, которым нас учили в школе (например, удаление элемента из середины списка). Применяя те же рассуждения, использование цикла for для линейного поиска по списку часто более эффективно и чище, чем построение карты для нескольких поисков.
Конечно, выбор читаемого контейнера обычно важнее производительности.
Обновление 2017-06-14: webbertiger редактирует свой ответ после того, как я прокомментировал. Я должен отметить, что его ответ теперь намного лучше для моих глаз. Но я сохранил свой ответ как дополнительную информацию ...
Из-за того, что я думаю, что первый ответ неверен (исправление: больше не оба), а третий имеет неправильное утверждение. Я чувствую, что мне нужно кое-что прояснить ...
Два самых популярных дерева - это AVL и Red Black (RB). Основное отличие заключается в использовании:
Основное отличие заключается в расцветке. У вас меньше действий по перебалансировке в дереве RB, чем у AVL, потому что окраска позволяет вам иногда пропускать или сокращать действия по перебалансировке, которые имеют относительную высокую стоимость. Из-за раскраски дерево RB также имеет более высокий уровень узлов, потому что оно может принимать красные узлы между черными (имея возможности ~ в 2 раза больше уровней), что делает поиск (чтение) немного менее эффективным ... но потому что это constant (2x), он остается в O (log n).
Если вы рассматриваете снижение производительности для модификации дерева (значимое) VS снижение производительности при просмотре дерева (почти незначительное), становится естественным предпочесть RB над AVL для общего случая.
Это просто выбор вашей реализации - они могут быть реализованы как любое сбалансированное дерево. Различные варианты сопоставимы с небольшими различиями. Следовательно, любой ничем не хуже любого другого.
O(logn)
, а неO(1)
, но значения всегда будут отсортированы. Начиная с C ++ 11 (я думаю), естьunordered_map
иunordered_set
, которые реализованы с использованием хэш-функций, и хотя они не отсортированы, большинство запросов и операций возможно вO(1)
(в среднем) - person SomethingSomething   schedule 15.03.2017std::map
итераторы, указывающие на другие элементы, не становятся недействительными. Это делает очень трудным, если не полностью невозможным, хранение более одного элемента на динамически выделяемый узел, при этом выполняя обычные гарантии временной сложности. (Запросы и обновленияstd::map
должны занимать в худшем случае логарифмическое время.) Итак, на практике реализацииstd::map
должны быть своего рода самобалансирующимися двоичными деревьями. - person pyon   schedule 04.08.2020