Nano Hash - криптовалюты, майнинг, программирование

Есть ли какая-то техническая причина, почему std::lower_bound не специализирован для итераторов красно-черного дерева?

Я всегда предполагал, что std::lower_bound() выполняется за логарифмическое время, если я передаю ему пару красно-черных итераторов дерева (set::iterator или map::iterator). Мне пришлось дважды обжечься, чтобы заметить, что std::lower_bound() в этом случае выполняется за время O(n), по крайней мере, с реализацией libstdc++. Я понимаю, что в стандарте нет концепции красно-черных итераторов дерева; std::lower_bound() будет рассматривать их как двунаправленные итераторы и advance за линейное время. Я до сих пор не вижу причин, по которым реализация не могла бы создать специальный тег итератора для итераторов красно-черного дерева и вызвать специализированный lower_bound(), если переданные итераторы оказались красно-черным деревом итераторы.

Есть ли какая-то техническая причина, по которой std::lower_bound() не специализируется на итераторах красно-черного дерева?


ОБНОВЛЕНИЕ: Да, я знаю о функциях-членах find, но это не главное. (В шаблонном коде у меня может не быть доступа к контейнеру или работать только на части контейнера.)


После истечения срока действия вознаграждения: я считаю ответы Мердада и Якка наиболее убедительными. Я не мог выбрать между слишком; Я отдаю награду Мехрдаду и принимаю ответ Якка.


  • Вместо этого пробовали контейнерный метод? 05.01.2014
  • Не суть. Кроме того, в шаблонном коде вы не можете работать с доступом к контейнеру / работать со всем контейнером. 05.01.2014
  • Я думаю, что можно указать предикат, который не эквивалентен предикату, предоставленному для std::set, и при этом выполнить требование частичной сортировки (для специальных set). Таким образом, вы можете заменить алгоритм lower_bound специальной красно-черной версией только в том случае, если предикат эквивалентен порядку std::set. 05.01.2014
  • @dyp технически нужно просто согласовать элементы в заданном диапазоне. 05.01.2014
  • @Yakk Действительно, но я думаю, это гораздо сложнее проверить. 05.01.2014
  • Вы можете добавить тип итератора — скажем, пропускаемый — который при заданных двух итераторах может перейти к одному примерно на полпути между ними за амортизированное постоянное время. 05.01.2014
  • +1 Вы набрали достаточно голосов, поэтому мы одобряем вас: идите и исправьте библиотеку! :-) Потому что единственная реальная причина в том, что этого еще никто не сделал. 07.01.2014
  • Это и обратная совместимость, которая была важной целью libstdc++. 09.01.2014
  • Это можно было бы сделать, но не с помощью итераторов SCARY. 09.01.2014

Ответы:


1

Нет технической причины, по которой это нельзя было бы реализовать.

Чтобы продемонстрировать, я набросаю способ реализовать это.

Мы добавляем новую категорию Iterator, SkipableIterator. Это подтип BiDirectionalIterator и надтип RandomAccessIterator.

SkipableIterators гарантируют, что функция between, выполненная в контексте, где виден std::between, работает.

template<typeanme SkipableIterator>
SkipableIterator between( SkipableIterator begin, SkipableIterator end )

between возвращает итератор между begin и end. Он возвращает end тогда и только тогда, когда ++begin == end (end идет сразу после begin).

Концептуально between должен эффективно находить элемент «примерно посередине между» begin и end, но мы должны быть осторожны, чтобы позволить работать как рандомизированному списку пропуска, так и сбалансированному красно-черному дереву.

Итераторы произвольного доступа имеют очень простую реализацию between -- return (begin + ((end-begin)+1)/2;

Добавить новый тег также легко. Деривация позволяет существующему коду работать хорошо, пока он правильно использует диспетчеризацию тегов (и не делает явную специализацию), но здесь есть небольшая проблема с поломкой. У нас могли бы быть "теговые версии", где iterator_category_2 является усовершенствованием iterator_category (или что-то менее хакерское), или мы могли бы использовать совершенно другой механизм, чтобы говорить о пропускаемых итераторах (независимая черта итератора?).

Как только у нас появится эта возможность, мы сможем написать алгоритм быстрого упорядоченного поиска, который работает на map/set и multi. Это также будет работать с контейнером списка пропуска, например QList. Это может быть даже та же реализация, что и в версии с произвольным доступом!

06.01.2014
  • Какое асимптотическое время выполнения имеет between для итераторов дерева? (И как он используется для реализации бинарного поиска? Это просто замена distance+advance?) 06.01.2014
  • @DyP: Вероятно, амортизировано или ожидается O (log(distance(begin, end))). 06.01.2014
  • @DyP Все, что имеет значение, это то, что between быстрее, чем distance+advance амортизированное, что должно быть легко в шкале нотации O для сбалансированного двоичного двунаправленного итерируемого дерева. В идеале вы должны имитировать порядок поиска set::lower_bound. Дело в том, что иногда between намного быстрее, чем advance, так как advance говорит, что я хочу продвинуться ровно столько, когда все, что мы хотим сделать, это продвинуться примерно на полпути к end. (Обратите внимание, что если бы дерево было быстро индексируемым (скажем, каждый узел отслеживал количество дочерних элементов), можно было бы реализовать логарифмическую скорость advance) 06.01.2014
  • Хм... Для меня это звучит так, будто lower_bound с SkipableIterators все еще асимптотически медленнее, чем set::find, что-то вроде O(logN * logN), так как каждый шаг в двоичном поиске требует between, что составляет O(logN) (что N уменьшается вдвое). для каждого шага, но асимптотически это не имеет значения). Это лучше, чем lower_bound с O(N) advance, но все равно не сделает функцию-член устаревшей. Или вы имели в виду другой алгоритм? 07.01.2014
  • @DyP, возможно, вы сможете достичь амортизированного постоянного времени при рекурсивных вызовах до нулевого размера: отследите, как вы будете ходить по (несколько) сбалансированному двоичному дереву. Может быть постоянный фактор, который элемент может реализовать (потому что он знает, что начинается со всего дерева). (Примечание о логарифмической скорости advance было побочным, я не ожидал, что between будет реализовано: between явно не волнует, где именно он заканчивается, в то время как advance, возможно, придется спуститься вниз по дереву, чтобы найти точную место оно хочет быть) 07.01.2014

  • 2

    Причин несколько:

    1. При использовании версии, не являющейся членом, можно использовать другой предикат. На самом деле, при использовании, например, std::map<K, V>, должен использоваться другой предикат должен, так как предикат карты работает с K, а диапазон работает с парами K и V.
    2. Даже если предикат совместим, функция имеет интерфейс, использующий пару узлов где-то в дереве, а не корневой узел, который был бы необходим для эффективного поиска. Хотя вполне вероятно, что есть родительские указатели, требование об этом для дерева кажется неуместным.
    3. Итераторы, предоставляемые алгоритму, не обязательно должны быть t.begin() и t.end() дерева. Они могут находиться где-то в дереве, что делает использование древовидной структуры потенциально неэффективным.
    4. В стандартной библиотеке нет абстракции дерева или алгоритмов, работающих с деревьями. Хотя ассоциативно упорядоченные контейнеры [вероятно] реализованы с использованием деревьев, соответствующие алгоритмы не доступны для общего использования.

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

    05.01.2014
  • Часть, которую я считаю сомнительной — что еще хуже, std::advance использует то же имя для алгоритма, который имеет линейную сложность с двунаправленными итераторами и постоянную сложность с произвольным доступом ;-p 05.01.2014
  • для карты V является младшим oartner в порядке: и поскольку K должен быть уникальным, они обычно совместимы, если V сопоставим. 05.01.2014
  • @SteveJessop std::advance() меня не смущает: это всегда максимально быстро. std::lower_bound() отличается тем, что его можно реализовать за логарифмическое время с помощью итераторов красно-черного дерева, но он работает за линейное время. Ой! 05.01.2014
  • @Ali: Не совсем так, std::advance не всегда работает настолько быстро, насколько это возможно. Если вы использовали ADL для его вызова (т. е. using std::advance; advance(i, d);), тогда это могло быть. Но на данный момент std::advance не использует operator+=, если только итератор не является произвольным доступом. Это неэффективно: рассмотрим concat_iterator, который итерирует объединение нескольких диапазонов. operator++ будет намного медленнее, чем operator+=, потому что последний может пропускать целые диапазоны, но итератор не может гарантировать произвольный доступ, поэтому std::advance будет неэффективным. 06.01.2014
  • @Ali: лично я считаю, что ADL следует использовать для вызова всех алгоритмов C++ по причинам, подобным этой, но я чувствую, что я единственный, кто считает, что это необходимо или хорошая идея, поэтому Я на самом деле тоже не делаю этого на практике. :( Я думаю, что C++ предоставил бы гораздо лучшие абстракции, если бы ADL правильно использовался для алгоритмов. 06.01.2014
  • @Mehrdad Хм. Интересно. Что это за таинственный concat_iterator? Вы имеете в виду, что есть другие полезные типы итераторов, о которых стандартная библиотека не знает, и, как следствие, алгоритмы не работают с ними? В любом случае, я имел в виду, что std::advance() работает максимально быстро на итераторах в стандартной библиотеке. 06.01.2014
  • @Ali: Да, это то, что я имею в виду. Вот пример одного (это диапазон, но это та же проблема). 06.01.2014
  • @Mehrdad: Интересно. concat_iterator — это итератор, попадающий между категориями. Итак, для производительности вы хотите настроить advance. Но это желание противоречит основанному на STL принципу, согласно которому алгоритмы являются общими и их не нужно настраивать для каждого итератора. Алгоритмы не отделены от данных, если вы повторно реализуете их для каждого типа итератора. Теперь swap настолько явно не является общим, что все знают, что это часть объекта и его нужно вызывать из ADL. Я думаю, менее очевидно, что то же самое относится (или должно быть) к advance, lower_bound, copy, sort... 06.01.2014
  • @SteveJessop: Для меня advance, swap, iter_swap и т. д. вообще не являются алгоритмами! :) Честно говоря, advance даже не должно было существовать -- operator+= было бы достаточно, если бы только его требовалось определить для всех типов итераторов. То же самое и с swap -- это должен был быть оператор, что, конечно же, является предложением. 06.01.2014
  • @SteveJessop: Что касается остальных алгоритмов - просто нет способа сохранить алгоритмы на 100% ортогональными итераторам. Весь пункт структур данных заключается в том, чтобы воздействовать на связанные с ними алгоритмы. Абстракция очень дырявая; просто наивно со стороны C++ думать, что алгоритмы одинаково применимы ко всем структурам данных, потому что это подразумевает, что структуры данных бессмысленны! Например, предположим, что std::sort должно быть невыполнимым для бинарного дерева поиска, использующего предикат по умолчанию - конечно, вы должны определить это как особую ситуацию. Алгоритмы просто не ортогональны структурам данных. 06.01.2014
  • @SteveJessop: Кстати, именно поэтому диапазоны обычно лучше итераторов. Если бы lower_bound принял диапазон, то пользователь мог бы просто передать сам контейнер, и у нас не было бы проблем с попыткой найти предикат... 06.01.2014
  • @Mehrdad: правильно, если то, что вы говорите, общепринято, то переход от пар итераторов к диапазонам был бы хорошей возможностью изменить (то, что я назвал) принципы алгоритмов. Скажем, все стандартные алгоритмы могут быть реализованы с помощью ADL с помощью диапазонов (включая контейнеры), а реализации в std:: — это просто настройки по умолчанию для контейнеров, которым они подходят. Это большинство, но не обязательно все комбинации стандартных алгоритмов со стандартными контейнерами. Кстати, я не думаю, что sort в бинарном дереве поиска должно быть недопустимым, это логическая ошибка. Хотя это не умаляет ваших очков. 07.01.2014
  • @SteveJessop: я не думаю, что рассмотрение точек настройки алгоритмов - правильный подход. Есть пара указателей настройки, например, операторы, swap(), advance(), distance() и, возможно, несколько других. Однако не все, что является шаблоном функции, является подходящей точкой настройки. На самом деле, я предпочел бы пойти противоположным путем и сделать некоторые алгоритмы не кандидатами для поиска через ADL, сделав их функциональными объектами (что дало бы дополнительное преимущество, состоящее в том, что вы могли бы легко связать их, даже если ты еще аргументов не знаешь). 07.01.2014
  • @DietmarKühl: Хорошо, мы можем дать ответ, если то, что говорит Мехрдад, общепринято. Например, насчет твоего трупа все согласны ;-) Я как бы держусь в стороне, потому что у меня нет твердого мнения о том, какие вещи в настоящее время <algorithm> действительно выигрывают от настройки для реальных нестандартных типов диапазонов в дикий. swap был безотказным примером в C++03, но, честно говоря, в C++11 он немного маргинален. 07.01.2014
  • @SteveJessop: нет, я бы не стал стоять на пути заблудшей толпы! Времена, когда я жертвовал собой на благо общества, давно прошли: толпа будет делать то, что толпа считает правильным, что не обязательно является лучшим решением. Это не помешает мне правильно использовать мою собственную библиотеку (если она когда-нибудь появится...). 07.01.2014
  • лол, я не могу сказать, кто с кем согласен, но это интересная дискуссия. 07.01.2014
  • @DietmarKühl Срок действия награды скоро истекает. Хотя я нахожу ответ Мердада и Якка на данный момент более убедительным (вероятно, потому, что они говорят то, что я хотел услышать), я хотел бы попросить вас как-то переместить свой комментарий Я не думаю, что рассмотрение точек настройки алгоритмов является правильный подход. [...] в свой ответ и, пожалуйста, дополните его примерами. Может быть, ваш подход лучше/менее закручен, и я хотел бы понять, что вы имеете в виду, желательно с примерами. Большое спасибо! 12.01.2014

  • 3

    Отличный вопрос. Я искренне считаю, что для этого нет хороших/убедительных/объективных причин.

    Почти все причины, которые я вижу здесь (например, требование предиката), не имеют для меня значения. Их может быть неудобно решать, но они вполне решаемы (например, просто требуется определение типа для различения предикатов).

    Самая убедительная причина, которую я вижу в самом верхнем ответе:

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

    Однако я считаю вполне разумным предположить, что родительские указатели реализованы.

    Почему? Поскольку временная сложность set::insert(iterator, value) гарантированно равна амортизированному постоянному времени, если итератор указывает на правильное местоположение.

    Считают, что:

    1. Дерево должно оставаться самобалансирующимся.
    2. Чтобы дерево было сбалансированным, необходимо смотреть на родительский узел при каждом изменении.

    Как вы можете избежать хранения родительских указателей здесь?

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

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


    Дополнительное замечание, но учтите, что мы просто не могли частично специализировать функции (например, lower_bound) в C++03.
    Но это не проблема, потому что мы могли бы просто специализировать type вместо этого и переадресовал вызов функции-члену этого типа.

    05.01.2014
  • Быстрый поиск в Google показывает, что существуют деревья RB без родительского указателя, но их превосходство сомнительно, см., например, Красно-черные деревья с уклоном влево считаются вредными. Таким образом, предположение о родительских указателях вполне разумно. Итак, единственная реальная проблема, которую я вижу, это случай, когда у предиката есть элементы данных: DyP говорит, что в этом случае нам нужен доступ к контейнеру. Есть мысли по этому поводу? 06.01.2014
  • @Ali: О, я, конечно, не имел в виду, что все деревья RB, которые я видел, используют родительские указатели! Это, конечно, не так - на самом деле, я видел эту ссылку раньше. Я имел в виду, что все реализации std::map или std::set, которые я видел, используют родительские указатели, потому что они должны обеспечивать амортизированную временную сложность O(1), когда дается итератор, и я не думаю, что они могут поступить иначе. Общее дерево RB не обязательно должно удовлетворять этому. Что касается предиката - да, это требует, чтобы узлы удерживают указатели на свои контейнеры, но ничто (?) не мешает этому? 06.01.2014
  • @Ali: в качестве альтернативы компаратор может храниться в другом месте в куче, чтобы избежать проблем, когда контейнеры swapped, если это необходимо. 06.01.2014
  • Можно поступить иначе; сам итератор может поддерживать хранилище узлов, через которые он уже прошел, подобно наивному рекурсивному обходу дерева. Я видел, как этот метод использовался несколько раз, когда дерево большое, а количество итераторов невелико. Я не удивлюсь, если увижу такую ​​реализацию в высокопроизводительной STL-подобной библиотеке, поскольку она упрощает реализации без блокировок и оптимизируется для обычного случая низких итераторов. 09.01.2014
  • @Alice: Подожди, что ты имеешь в виду? Разве это не требует хранения O (n) внутри каждого итератора, что довольно дорого? Кроме того, не означает ли это, что итераторы станут недействительными после удаления родителя правого дочернего узла? 10.01.2014
  • O(log n), глубина текущей ветви дерева. И нет, если узлы хранятся через слабый указатель, то пока хотя бы один узел еще действителен он может пропускать стертые. Эта тактика в основном используется на картах без блокировки; итераторы используются повторно, поэтому стоимость не высока, а стоимость дополнительных родительских указателей заключается в том, что узлы могут быть удалены только лениво. Переместив все представления в структуру данных в тяжелые итераторы, можно сделать вывод, кто на что указывает, и можно выполнить настоящее освобождение памяти узла. 11.01.2014
  • @Alice: Интересно ... Хотя это подразумевает стоимость log n для создания и уничтожения итераторов. Я ожидал, что стандарт будет давать ограничения по времени для итераторов, но я думаю, что это не так? 11.01.2014
  • Дерево с резьбой также может предоставлять итераторы постоянного размера со следующими и предыдущими запросами с постоянным временем, и не не требуют родительских указателей (они повторно используют пространство дочерних указателей NIL). Я думаю, что они также могут предоставить временные гарантии insert с подсказкой. 14.01.2014
  • @larsmans: Они действительно могут? Чтобы перебалансировать дерево, вам нужен доступ к родителю, не так ли? Вы не можете получить родительский доступ в постоянное время... 15.01.2014
  • Вы правы, процедура вставки, которую я имел в виду, разбалансирует дерево. Возможно, в расширенном дереве это было бы приемлемо, но остальная часть спецификации set/map, похоже, исключает это. 15.01.2014
  • @Mehrdad Создание и уничтожение итератора будет иметь постоянное время (если предположить, что распределитель равен O (1), что, хотя и не всегда или даже обычно верно, но обычно предполагается), но константа будет большой. Высота дерева предсказуемо ограничена (если у вас есть X узлов в дереве, и оно организовано с разветвлением Y, размер - это некоторая функция log Y от X), поэтому итераторы log n не имеют значения. 15.01.2014
  • @Alice: я запуталась, что такое n в твоем объяснении? 15.01.2014
  • @Mehrdad То же, что и в любой структуре данных; все элементы, вставленные в структуру данных. 16.01.2014
  • @Alice: Но я думал, вы только что сказали, что в вашей структуре данных X узлов, а не n? 16.01.2014
  • Это был просто пример. Х = п. Дело в том, что вы можете предсказать глубину дерева или сделать наихудшие предположения о нем, используя данные с постоянным временем, такие как известное количество узлов в дереве. 16.01.2014
  • @Алиса: А что?? это довольно глупо... Вся точка big-O состоит в том, чтобы проанализировать, что происходит, когда n увеличивается. Если вы ограничиваете n, то все в мире работает за постоянное время, и это совершенно бесполезное утверждение. 16.01.2014
  • О чем ты говоришь? я вообще ничего не привязывал. Дело в том, что создание каждого итератора — это постоянное время; он делает единственный вызов распределителя, выделяя достаточно памяти для хранения узлов по мере продвижения по дереву. Ему нужно хранить столько узлов, сколько высоко дерево, что является расчетом за постоянное время. Ничто из этого не является обязательным; необходимое количество изменяется по мере того, как дерево становится больше. Если ваш аллокатор O(1) (вероятно, с некоторой высокой константой C), то операции итератора все еще находятся в пределах границ STL. 16.01.2014
  • @Alice: я не говорю о распределителе, я говорю о вашем утверждении, что ему нужно хранить столько узлов, сколько высоко дерево. В дереве n узлов. Хранение этих узлов занимает log(n) времени и места. Как это постоянное время!? 16.01.2014
  • @Mehrdad он может выделить все пространство для хранения для узлов за постоянное время, поскольку это известно. Это O(1) времени, O(log n) памяти. Он использует это как стек. Каждый раз, когда он выходит из строя, он подталкивает к нему родительский узел, что является операцией O (1). Он никогда не вызывает операцию O(log n); это просто O (1) поверх традиционных операций обхода. Вы путаете, когда он делает хранилище с выделением для хранилища. 16.01.2014
  • @Alice: Нет, я не путаю их, я говорю, что нет причин, по которым обход должен начинаться с корня дерева. Он вполне может начаться где-то еще, и в этом случае ему нужно будет поместить в стек log n узлов. 16.01.2014

  • 4

    (Разработка комментария)

    Я думаю, что можно указать предикат, который не эквивалентен предикату, предоставленному std::set, и при этом выполнить требование частичной сортировки (для специальных наборов). Таким образом, вы можете заменить алгоритм lower_bound специальной красно-черной версией только в том случае, если предикат эквивалентен порядку std::set.

    Пример:

    #include <utility>
    #include <algorithm>
    #include <set>
    #include <iostream>
    
    struct ipair : std::pair<int,int>
    {
        using pair::pair;
    };
    
    bool operator<(ipair const& l, ipair const& r)
    {  return l.first < r.first;  }
    
    struct comp2nd
    {
        bool operator()(ipair const& l, ipair const& r) const
        {  return l.second > r.second; /* note the > */ }
    };
    
    std::ostream& operator<<(std::ostream& o, ipair const& e)
    {  return o << "[" << e.first << "," << e.second << "]";  }
    
    int main()
    {
        std::set<ipair, comp2nd> my_set = {{0,4}, {1,3}, {2,2}, {3,1}, {4,0}};
        for(auto const& e : my_set) std::cout << e << ", ";
    
        std::cout << "\n\n";
    
        // my_set is sorted wrt ::operator<(ipair const&, ipair const&)
        //        and       wrt comp2nd
        std::cout << std::is_sorted(my_set.cbegin(), my_set.cend()) << "\n";
        std::cout << std::is_sorted(my_set.cbegin(), my_set.cend(),
                                    comp2nd()) << "\n";
    
        std::cout << "\n\n";
    
        // implicitly using operator<
        auto res = std::lower_bound(my_set.cbegin(), my_set.cend(), ipair{3, -1});
        std::cout << *res;
    
        std::cout << "\n\n";
    
        auto res2 = std::lower_bound(my_set.cbegin(), my_set.cend(), ipair{-1, 3},
                                     comp2nd());
        std::cout << *res2;
    }
    

    Выход:

    [0,4], [1,3], [2,2], [3,1], [4,0], 
    
    1
    1
    
    [3,1]
    
    [1,3]
    05.01.2014
  • Конечно, вы все равно можете предоставить какой-то механизм для отправки lower_bound в красно-черную версию, если используемый предикат эквивалентен std::set::key_comp() 05.01.2014
  • Спасибо. Я должен был четко указать в вопросе, что я предполагаю, что lower_bound() и set используют один и тот же предикат. 05.01.2014
  • @Ali Думаю, тебе все равно нужно было это проверить. Таким образом, вы не можете просто использовать общую отправку тегов для красно-черных итераторов (в моем отредактированном примере вы увидите, что предикат по умолчанию, выбранный lower_bound, также может отличаться от предиката набора; даже если вы не не используйте предикат не по умолчанию для set, поиск имени может отличаться). 05.01.2014
  • Я думаю, что пользователь должен предоставить lower_bound правильный предикат. Если пользователь ошибется, мы ничего не сможем сделать. Я просто не могу решить, не является ли ваш пример примером ошибки пользователя. 05.01.2014
  • Я не уверен, что полностью понимаю ваш пример. Можно ли заставить lower_bound работать за логарифмическое время, если set и lower_bound используют один и тот же предикат? Можно ли решить, совпадают ли предикаты, если у вас нет доступа к самому контейнеру? 06.01.2014
  • @Ali Как говорит Дитмар Кюль, возможно получить время выполнения O (logN), если есть родительские указатели. Проверка эквивалентности предиката требует доступа к контейнеру, если сравнение нетривиально (представьте себе объект функции предиката с элементами данных). Однако тип предиката может быть встроен в тип итератора (этого должно быть достаточно для std::less и других предикатов без сохранения состояния). Также может быть невозможно проверить эквивалентность (предикат должен быть EqualityComparable). 06.01.2014
  • Мердад говорит, что все известные ему реализации используют родительские указатели. Так что нет, это не реальная проблема. Быстрый поиск в Google показывает, что существуют деревья RB без родительского указателя, но их превосходство сомнительно, см., например, Красно-черные деревья с уклоном влево считаются вредными. Итак, единственная реальная проблема, которую я вижу, это случай, когда предикат имеет элементы данных. 06.01.2014
  • Красно-черные деревья — не единственная структура данных, удовлетворяющая требованиям set и map; многие структуры данных, и многие из них широко используются, удовлетворяют ограничениям. Списки пропуска использовались во многих проектах из-за того, что в среднем они требовали меньше места. Были использованы различные другие формы BST, в частности, дерево AA, которое широко используется в личных реализациях, потому что оно проще, чем дерево RB, и дерево AVL, потому что оно более строгое и о нем легче рассуждать. Не принимайте красное черное дерево; в стандарте нет. 09.01.2014
  • @Alice Хотя вы правы в том, что красно-черные деревья не обязательны, аргумент, который я хочу указать в своем ответе, связан с порядком, используемым std::set, который не зависит от используемой структуры данных. реализовать set. Али указывает, что libstdc++ может предоставить специальную реализацию lower_bound для итераторов красно-черных деревьев (через диспетчеризацию), поскольку libstdc++ использует красно-черные деревья для своего std::set (т. е. оптимизация для конкретной реализации). Именно об этом говорят комментарии, касающиеся красно-черных деревьев. 09.01.2014

  • 5

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

    Вернем время назад к началу 2000-х, во время перехода между GCC и GCC 3, а затем, во время незначительных изменений GCC 3. Многие из проектов, над которыми я работал, должны были быть бинарно-совместимыми; мы не могли потребовать от пользователя перекомпилировать наши программы или плагины, а также мы не могли быть уверены в версии GCC, на которой они были скомпилированы, или версии STL, с которой они были скомпилированы.

    Решение: не используйте STL. У нас были собственные строки, векторы и попытки, а не использование STL. Решение ада зависимостей, созданное якобы стандартной частью языка, было настолько отличным, что мы отказались от него. И не только в одном или двух проектах.

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

    Но ваше изменение повлечет за собой новую, невысказанную зависимость

    Предположим, что завтра появится новая структура данных, которая существенно превосходит красно-черные деревья, но не гарантирует наличие некоторых специализированных итераторов. Одной из таких реализаций, которая была очень популярна всего несколько лет назад, была список пропусков, который предлагал те же гарантии при, возможно, значительно меньшем объеме памяти. Список пропусков, похоже, не сработал, но другая структура данных вполне могла это сделать. Лично я предпочитаю использовать попытки, которые предлагают значительно лучшую производительность кэша и более надежную алгоритмическую производительность; их итераторы будут существенно отличаться от красно-черных деревьев, если кто-то в libstdc++ решит, что эти структуры обеспечивают лучшую общую производительность для большинства применений.

    Строго следуя стандарту, обратная бинарная совместимость может поддерживаться даже при изменении структуры данных. Это хорошая вещь (TM) для библиотеки, предназначенной для динамического использования. Я бы и глазом не моргнул, если бы такая оптимизация была хорошо реализована и хорошо использовалась, если бы она использовалась статически, например, библиотека Boost Container.

    Но для такой динамической библиотеки, как libstdc++, гораздо важнее обратная бинарная совместимость.

    08.01.2014
  • Может ли изменение алгоритма (шаблона) сделать двоичные файлы несовместимыми? 09.01.2014
  • Это зависит от реализации динамической ссылки. Некоторые части библиотеки (например, шаблон) будут в программе компоновки, а другие (реализация базовой структуры) — нет. Предположим, что шаблон представляет собой оболочку, которая оборачивает общие вызовы базовой структуры данных, использующей void* в разделяемой библиотеке (распространенный способ уменьшить раздувание, разрешенное стандартом). Если скомпилированный шаблон ссылается на итераторы, которые сейчас не используются в разделяемой библиотеке, у нас критическое изменение. Именно по этой причине стандарт — это политика, а не реализация. 09.01.2014
  • Я должен отметить, что это фундаментальный конфликт между дженериками и объектами, который разрешается с помощью примитивной формы стирания типов; либо vector‹int› и vector‹string› — это фундаментально разные классы, и поэтому между ними не может быть общего кода, либо должна происходить некоторая форма стирания типа, и вектор любого экземпляра может совместно использовать код. Первое — это явно то, что происходит в заголовке, включающем только статическую библиотеку (например, контейнер Boost), а второе — это то, что мы предпочли бы в динамической библиотеке, такой как libstdc++. 09.01.2014
  • Новые материалы

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

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

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

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..