Чому std :: list :: reverse має складність O (n)?


192

Чому функція звороту для std::listкласу в стандартній бібліотеці C ++ має лінійне виконання? Я думаю, що для подвійно пов'язаних списків зворотна функція повинна була бути O (1).

Повернення подвійно пов'язаного списку повинно включати перемикання вказівників голови та хвоста.


18
Я не розумію, чому люди заперечують це питання. Це абсолютно розумне питання. Повернення подвійно пов'язаного списку повинно зайняти O (1) час.
Цікаво,

46
на жаль, деякі люди плутають поняття "питання добре" з "питання має гарне уявлення". Я люблю такі питання, де в основному "моє розуміння здається іншим, ніж загальноприйнята практика, будь ласка, допоможіть вирішити цей конфлікт", тому що розширення, як ви думаєте, допомагає вирішити набагато більше проблем у дорозі! Здається, інші застосовують підхід "це марно обробка в 99,9999% випадків, навіть не думай про це". Якщо це будь-яке втіху, я був прихильний до набагато, набагато менше!
corsiKa

4
Так, це запитання отримало непомірну кількість посилань на якість. Напевно, це те саме, що підтримало відповідь Блінді. Справедливо кажучи, що "повернення подвійно пов'язаного списку повинно включати перемикання покажчиків на голову та хвіст", як правило, не вірно для стандартного пов'язаного списку impl, який навчаються всі в середній школі, або для багатьох реалізацій, якими користуються люди. Багато разів у безпосередній реакції кишечника людей на запитання чи відповідь призводить до прийняття рішення. Якби ви були більш чіткими в цьому реченні або пропустили його, я думаю, ви б отримали менше зворотних запитів.
Кріс Бек

3
Або дозвольте мені покласти тягар доказування з вами, @Curious: Я розпочав тут реалізацію подвійного списку: ideone.com/c1HebO . Чи можете ви вказати, як ви очікуєте, що Reverseфункція буде реалізована в O (1)?
CompuChip

2
@CompuChip: Насправді, залежно від реалізації, це може не статися. Вам не потрібен додатковий логічний сигнал, щоб знати, який вказівник використовувати: просто використовуйте той, який не вказує до вас…, який цілком може бути автоматичним із пов'язаним списком XOR'ed. Так, так, це залежить від того, як буде здійснено список, і заява про ОП може бути уточнена.
Матьє М.

Відповіді:


187

Гіпотетично reverseмогло бути O (1) . Там (знову гіпотетично) міг бути логічний член списку, який вказує, чи напрямок пов'язаного списку в даний час такий же або протилежний початковому, де цей список створений.

На жаль, це призведе до зниження продуктивності в основному будь-якої іншої операції (хоча і не змінюючи асимптотичну тривалість виконання). Під час кожної операції потрібно було б проконсультуватися із булевим елементом, щоб вирішити, чи слід слідувати за наступним або попереднім вказівником посилання.

Оскільки це, ймовірно, вважалося відносно нечастою операцією, стандарт (який не диктує реалізації, а лише складність), визначав, що складність може бути лінійною. Це дозволяє «наступним» покажчикам завжди однозначно означати той самий напрямок, прискорюючи загальновиробничі операції.


29
@MooseBoys: Я не згоден з вашою аналогією. Різниця полягає в тому, що, в разі списку, реалізація може забезпечити reverseз O(1)складністю , не зачіпаючи великий-о-який - або іншої операції , за допомогою цього булева трюк прапора. Але на практиці додаткова гілка в кожній операції коштує дорого, навіть якщо це технічно O (1). Навпаки, ви не можете скласти структуру списку, в якій sortє O (1), а всі інші операції мають однакову вартість. Сенс питання полягає в тому, що, здавалося б, ви можете отримати O(1)зворотний зворот безкоштовно, якщо дбаєте лише про великий O, то чому вони цього не зробили.
Кріс Бек

9
Якщо ви використовували список, пов’язаний з XOR, реверсування стане постійним часом. Ітератор, однак, був би більшим, і збільшення / зменшення його було б трохи дорожче обчислювальним. Це може бути карликовим через неминучий доступ до пам'яті, хоча для будь- якого зв'язаного списку.
Дедуплікатор

3
@IlyaPopov: Чи справді потрібен кожен вузол? Користувач ніколи не задає ніяких запитань щодо самого вузла списку, а лише основного органу списку. Таким чином, доступ до булевого простого для будь-якого методу, який викликає користувач. Ви можете встановити правило про те, що ітератори недійсні, якщо список перетворений, та / або зберігати копію булевого тексту, наприклад, з ітератором. Тому я думаю, що це не вплине на велику О, обов'язково. Я визнаю, я не проходив по черзі через специфікацію. :)
Кріс Бек

4
@Kevin: Гм, що? Ви не можете скористатися двома вказівниками безпосередньо, вам потрібно спершу перетворити їх у цілі числа (очевидно, типу std::uintptr_t. Потім ви можете скопіювати їх.
Deduplicator

3
@Kevin, ви, безумовно, могли скласти список, пов'язаний з XOR, на C ++, це практично мова, що відповідає дитині для цього типу. Ніщо не говорить про те, що вам доведеться використовувати std::uintptr_t, ви можете передавати charмасив, а потім XOR компоненти. Це було б повільніше, але на 100% портативне. Ймовірно, ви можете змусити його вибирати між цими двома реалізаціями, а використовувати другий лише як резервний, якщо uintptr_tйого немає. Деякі, якщо це описано у цій відповіді: stackoverflow.com/questions/14243971/…
Кріс Бек,

61

Це може бути O (1), якщо в списку буде зберігатися прапор, який дозволяє міняти значення покажчиків " prev" та " next", які має кожен вузол. Якщо повернення списку буде частою операцією, таке доповнення може бути насправді корисним, і я не знаю жодної причини, чому його застосування було б заборонено чинним стандартом. Однак наявність такого прапора зробить звичайне прокручування списку дорожче (якщо тільки постійним фактором), тому що замість цього

current = current->next;

в operator++ітераторі списку ви отримаєте

if (reversed)
  current = current->prev;
else
  current = current->next;

що ви не вирішили легко додати. Зважаючи на те, що списки, як правило, проходять набагато частіше, ніж вони перевертаються, було б дуже нерозумно, щоб стандарт надавав цю методику. Тому зворотній операції дозволяється мати лінійну складність. Зауважте, однак, що tO (1) ⇒ tO ( n ), тому, як було сказано раніше, технічна реалізація вашої "оптимізації" буде дозволена.

Якщо ви родом з Java або подібного фону, вам може бути цікаво, чому ітератор повинен перевіряти прапор кожен раз. Чи не могли б ми замість цього мати два різних ітераторних типи, обидва похідні від загального базового типу, і мати std::list::beginі std::list::rbeginполіморфно повернути відповідний ітератор? Хоча це можливо, це зробить все ще гірше, тому що просування ітератора було б непрямим (важко вбудованим) викликом функції. У Java ви все одно платите за цю ціну регулярно, але знову ж таки, це одна з причин, коли багато людей досягають C ++, коли продуктивність критична.

Як вказував у коментарях Бенджамін Ліндлі , оскільки reverseітераторів не можна визнати недійсними, єдиним підходом, дозволеним стандартом, є збереження вказівника назад до списку всередині ітератора, що викликає подвійний непрямий доступ до пам'яті.


7
@galinette: std::list::reverseне визнає ітераторами недійсними.
Бенджамін Ліндлі

1
@galinette Вибачте, я неправильно прочитав ваш попередній коментар як "прапор за ітератор ", а не "прапор на вузол ", як ви його написали. Звичайно, прапор на вузол був би контрпродуктивним, як вам, знову ж таки, доведеться робити лінійний обхід, щоб перевернути їх усі.
5gon12eder

2
@ 5gon12eder: Ви можете усунути розгалуження дуже втраченою вартістю: зберігайте nextта prevвказівники у масиві та зберігайте напрямок у вигляді 0або 1. Для повторення вперед ви повинні слідувати pointers[direction]та повторювати в зворотному напрямку pointers[1-direction](або навпаки). Це все ще додасть крихітний накладний наклад, але, мабуть, менше, ніж гілка.
Джеррі Труну

4
Напевно, ви не можете зберегти вказівник до списку всередині ітераторів. swap()вказується на постійний час і не визнає недійсним жодних ітераторів.
Тавіан Барнс

1
@TavianBarnes Чорт забирай! Ну, потрійне непряме, тоді… (я маю на увазі, насправді не потрійне. Вам потрібно буде зберігати прапор у динамічно виділеному об’єкті, але вказівник в ітераторі може, звичайно, вказувати безпосередньо на цей об’єкт, а не посилатися на список.)
5gon12eder

37

Звичайно, оскільки всі контейнери, які підтримують двонаправлені ітератори, мають поняття rbegin () та rend (), це питання суперечить?

Тривіально створити проксі-сервер, який перевертає ітератори та отримує доступ до контейнера через нього.

Ця робота не є дійсно O (1).

як от:

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

очікуваний вихід:

mat
the
on
sat
cat
the

З огляду на це, мені здається, що комітет з питань стандартизації не встиг надати мандат O (1) зворотне впорядкування контейнера, оскільки це не потрібно, а стандартна бібліотека побудована в основному за принципом мандатування лише того, що вкрай необхідно. уникаючи дублювання.

Просто мій 2с.


18

Тому що він повинен пройти кожен вузол ( nусього) та оновити їх дані (дійсно є крок оновлення O(1)). Це робить всю операцію O(n*1) = O(n).


27
Тому що вам також потрібно оновити зв’язки між кожним елементом. Вийміть аркуш паперу і витягніть його, а не схиляючи.
Сліпий

11
Навіщо запитати, якщо ти так впевнений? Ви витрачаєте обидва наш час.
Сліпий

28
@Curious Двозначно пов'язані вузли списку мають відчуття напрямку. Причина звідти.
взуття

11
@Blindy Гарна відповідь повинна бути повною. "Вийміть аркуш паперу і витягніть його", таким чином, не повинно бути необхідним компонентом хорошої відповіді. Відповіді, які не є гарними відповідями, підлягають зворотній коментар.
RM

7
@Shoe: Вони повинні? Будь ласка, досліджуйте список, пов’язаний із XOR тощо.
Дедупликатор

2

Він також підміняє попередній та наступний вказівники для кожного вузла. Ось чому це потрібно лінійним. Хоча це може бути зроблено в O (1), якщо функція, що використовує цю LL, також приймає інформацію про LL в якості вхідного сигналу, як, наприклад, чи він звертається нормально або зворотно.


1

Тільки пояснення алгоритму. Уявіть, у вас є масив з елементами, тоді вам потрібно його перевернути. Основна ідея полягає в перегляді кожного елемента, змінюючи елемент на першому положенні на останнє, елемент на другому положенні на передостаннє положення тощо. Коли ви досягнете середини масиву, ви зміните всі елементи, таким чином, в (n / 2) ітераціях, що вважається O (n).


1

Це O (n) просто тому, що йому потрібно скопіювати список у зворотному порядку. Кожна окрема операція з пунктом - O (1), але їх у всьому списку є n.

Звичайно, існують деякі операції постійного часу, що займаються налаштуванням місця для нового списку, а потім зміною покажчиків тощо. Позначення O не враховує окремих констант, коли ви включаєте n-коефіцієнт першого порядку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.