Двоичная куча против двоичного дерева С++

У меня возникла некоторая путаница во время выполнения операции find_min в двоичном дереве поиска и двоичной куче. Я понимаю, что возврат min в двоичной куче - это операция O (1). Я также понимаю, почему теоретически возврат минимального элемента в двоичном дереве поиска - это операция O (log (N)). К моему большому удивлению, когда я прочитал о структуре данных в C++ STL, в документации говорится, что возврат итератора к первому элементу в карте (что то же самое, что возврат минимального элемента) происходит за постоянное время! Разве это не должно возвращаться в логарифмическом времени? Мне нужен кто-то, кто поможет мне понять, что C++ делает под капотом, чтобы вернуть это за постоянное время. Поскольку тогда нет смысла использовать двоичную кучу в C++, тогда структура данных карты будет поддерживать извлечение минимума и максимума как в постоянное время, удаление и поиск в O(log(N)) и сохраняет все отсортированным. Это означает, что структура данных имеет преимущества как BST, так и двоичной кучи, связанных в одном!

У меня был спор об этом с интервьюером (на самом деле не спор), но я пытался объяснить ему, что в C++ возврат min и max из карты в C++ (которая представляет собой самобалансирующееся двоичное дерево поиска) происходит за постоянное время. Он был сбит с толку и продолжал говорить, что я ошибаюсь и что бинарная куча — это правильный путь. Разъяснение было бы очень признательно


person AyBayBay    schedule 07.01.2014    source источник
comment
Двоичная куча кажется мне полезной только в том случае, если вы потребляете/удаляете элементы во время итерации. Вспучивание следующего элемента как бы зависит от дыры, вызванной этим удалением.   -  person cHao    schedule 07.01.2014
comment
Интервьюер явно сбит с толку. Надеюсь, вы не устроились на работу в его компанию :-)   -  person Sergey Kalinichenko    schedule 07.01.2014
comment
ну, из-за спора он разозлился, и интервью не прошло хорошо, поэтому я предполагаю, что мне не перезвонят. Я думаю, что это и хорошо, и плохо: / Я не был очень конкретным в своем объяснении ему, поэтому я разместил этот вопрос, чтобы убедиться, что я прав, и почему это так.   -  person AyBayBay    schedule 07.01.2014


Ответы (3)


Поиск минимума и максимума за постоянное время достигается за счет сохранения ссылок на крайний левый и крайний правый узлы RB-дерева в структуре заголовка карты. Вот комментарий fиз исходного кода RB- tree, шаблон, из которого берутся реализации std::set, std::map и std::multimap:

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

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

person Sergey Kalinichenko    schedule 07.01.2014
comment
Итак, если бы меня интересовало только асимптотическое время выполнения, структура данных карты C++ имеет преимущества как двоичной кучи, так и двоичного дерева поиска в отношении времени выполнения следующих операций (findmax, findmin, удаление, вставка, поиск) из-за способ его реализации и учет/состояние, которое он отслеживает в своей реализации заголовка - person AyBayBay; 07.01.2014
comment
@AyBayBay Это правильно: асимптотическая сложность будет такой же, хотя куча может быть реализована более экономичным способом (т. Е. Постоянный множитель, учитываемый при вычислении большого O, может быть ниже для кучи, чем для дерева). - person Sergey Kalinichenko; 07.01.2014
comment
вау, тогда я был прав, мне нужно научиться быть более уверенным, когда я прав, и ясно объяснить это, все, что вы, ребята, сказали мне, это именно то, что я подозревал. Спасибо! - person AyBayBay; 07.01.2014

По крайней мере, в типичной реализации std::setstd::map) будут реализованы как многопоточное двоичное дерево1. Другими словами, каждый узел содержит не только указатель на его (до) двух дочерних узлов, но также на предыдущий и следующий по порядку узел. Тогда сам класс set имеет указатели не только на корень дерева, но также на начало и конец многопоточного списка узлов.

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

Это имеет ряд недостатков по сравнению с двоичной кучей. Наиболее очевидным является то, что он хранит четыре указателя для каждого элемента данных, тогда как двоичная куча может хранить только данные без указателей (отношения между узлами неявно определяются позициями данных). В крайнем случае (например, std::set<char>) это может привести к тому, что для хранения указателей потребуется намного больше места, чем для данных, которые вам действительно нужны (например, в 64-разрядной системе вы можете получить 4 указателя по 64 бита каждый для хранения каждого 8-битного символа). Это может привести к плохому использованию кеша, что (в свою очередь) снижает скорость.

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

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


1. С применением некоторого алгоритма балансировки — чаще всего красно-черного, но иногда AVL-деревьев или B-деревьев. Можно использовать любое количество других сбалансированных деревьев (например, альфа-сбалансированные деревья, деревья k-соседей, бинарные b-деревья, общие сбалансированные деревья).

person Jerry Coffin    schedule 07.01.2014
comment
Это было очень подробно, спасибо за ответ. Я буду иметь это в виду в будущем. - person AyBayBay; 07.01.2014

Я не эксперт по картам, но возврат первого элемента карты будет считаться своего рода «корнем». всегда есть указатель на него, поэтому время поиска будет мгновенным. То же самое можно сказать и о BSTree, поскольку у него явно есть корневой узел, затем 2 узла от него и так далее (что, кстати, я бы рассмотрел с помощью дерева AVL, поскольку время поиска для наихудшего сценария намного лучше, чем BSTree ).

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

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

person user3164339    schedule 07.01.2014
comment
map в C++ — красно-черное дерево - person AyBayBay; 07.01.2014
comment
Я предполагаю, что это была проблема терминологии с моей стороны, я извиняюсь. Однако, когда я учился в школе (это было давно), именно так мы реализовали карту cplusplus. com/reference/map/map и, насколько мне известно, это не RBTree, если я только что не запутался. Но если я правильно помню, RBTree было деревом, в котором каждый узел имел 3 подузла (опять же, моя память может быть неправильной, и в этом случае я удалю этот комментарий) - person user3164339; 07.01.2014