Чому найгірший випадок для цієї функції O (n ^ 2)?


44

Я намагаюся навчити себе, як обчислити позначення BigO для довільної функції. Я знайшов цю функцію в підручнику. Книга стверджує, що функцією є O (n 2 ). Це дає пояснення, чому це відбувається, але я намагаюся слідувати. Цікаво, чи хтось міг би показати мені математику, чому це так. В принципі, я розумію, що це щось менше, ніж O (n 3 ), але я не міг самостійно приземлитися на O (n 2 )

Припустимо, нам дано три послідовності чисел, A, B і C. Будемо вважати, що жодна окрема послідовність не містить повторюваних значень, але можуть бути деякі числа, які знаходяться у двох-трьох послідовностях. Триполосна задача неперервності полягає в тому, щоб визначити, чи перетин трьох послідовностей порожній, а саме, що немає елемента x такий, що x ∈ A, x ∈ B і x ∈ C.

Між іншим, це не проблема домашнього завдання для мене - той корабель плив років тому:), просто я намагаюся бути розумнішим.

def disjoint(A, B, C):
        """Return True if there is no element common to all three lists."""  
        for a in A:
            for b in B:
                if a == b: # only check C if we found match from A and B
                   for c in C:
                       if a == c # (and thus a == b == c)
                           return False # we found a common value
        return True # if we reach this, sets are disjoint

[Редагувати] Відповідно до підручника:

У вдосконаленій версії не просто ми економимо час, якщо нам пощастить. Ми стверджуємо, що найгірший час запуску для роз'єднання - O (n 2 ).

Пояснення книги, яке я намагаюся дотримуватися, таке:

Щоб врахувати загальний час роботи, ми вивчаємо час, витрачений на виконання кожного рядка коду. Управління циклом for над A вимагає часу O (n). Управління циклом for для B становить загальний час O (n 2 ), оскільки ця петля виконується n різних разів. Тест a == b оцінюється O (n 2 ) разів. Решта часу, що витрачається, залежить від кількості пар (a, b). Як ми зазначали, існує максимум n таких пар, і тому управління циклом над C, а команди всередині цього циклу використовують не більше O (n 2 ) часу. Загальний витрачений час - O (n 2 ).

(І щоб надати належний кредит ...) Книга така: Структури даних та алгоритми в Python Майкл Т. Гудріч та ін. всі, Wiley Publishing, стор. 135

[Редагувати] Виправдання; Нижче наведено код перед оптимізацією:

def disjoint1(A, B, C):
    """Return True if there is no element common to all three lists."""
       for a in A:
           for b in B:
               for c in C:
                   if a == b == c:
                        return False # we found a common value
return True # if we reach this, sets are disjoint

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

Хоча я не можу довести, що це так (таким чином, питання), читач може погодитися, що складність спрощеного алгоритму принаймні менша, ніж оригінал.

[Редагувати] І довести, що спрощена версія є квадратичною:

if __name__ == '__main__':
    for c in [100, 200, 300, 400, 500]:
        l1, l2, l3 = get_random(c), get_random(c), get_random(c)
        start = time.time()
        disjoint1(l1, l2, l3)
        print(time.time() - start)
        start = time.time()
        disjoint2(l1, l2, l3)
        print(time.time() - start)

Врожайність:

0.02684807777404785
0.00019478797912597656
0.19134306907653809
0.0007600784301757812
0.6405444145202637
0.0018095970153808594
1.4873297214508057
0.003167390823364258
2.953308343887329
0.004908084869384766

Оскільки друга різниця дорівнює, спрощена функція дійсно є квадратичною:

введіть тут опис зображення

[Редагувати] І ще більше підтвердження:

Якщо я припускаю найгірший випадок (A = B! = C),

if __name__ == '__main__':
    for c in [10, 20, 30, 40, 50]:
        l1, l2, l3 = range(0, c), range(0,c), range(5*c, 6*c)
        its1 = disjoint1(l1, l2, l3)
        its2 = disjoint2(l1, l2, l3)
        print(f"iterations1 = {its1}")
        print(f"iterations2 = {its2}")
        disjoint2(l1, l2, l3)

врожайність:

iterations1 = 1000
iterations2 = 100
iterations1 = 8000
iterations2 = 400
iterations1 = 27000
iterations2 = 900
iterations1 = 64000
iterations2 = 1600
iterations1 = 125000
iterations2 = 2500

Використовуючи другий тест на різницю, найгірший результат - саме квадратичний.

введіть тут опис зображення


6
Або книга неправильна, або ваша транскрипція.
candied_orange

6
Ні. Неправильно помиляється незалежно від того, наскільки добре цитується. Або поясніть, чому ми не можемо просто припустити їх, якщо підемо найгіршим способом, коли вони роблять великий аналіз O, або прийміть отримані результати.
candied_orange

8
@candied_orange; Я додав ще одне обґрунтування, якнайкраще, - не мій сильний костюм. Я б просив, щоб ви знову допустили можливість того, що ви дійсно можете бути невірними. Ви зробили свою думку, належним чином.
SteveJ

8
Випадкові числа - це не ваш найгірший випадок. Це нічого не доводить.
Теластин

7
ах. добре. "Немає послідовності має повторювані значення" змінює найгірший випадок, оскільки C може спрацьовувати лише один раз на будь-який А. Вибачте за розлад - ось що я отримую за те, що пізно в суботу я перебуваю на stackexchange: D
Теластин,

Відповіді:


63

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

У книзі висловлюється аргумент, що складність становить O (n²), оскільки if a == bгілка вводиться не більше n разів. Це не очевидно, оскільки петлі все ще записуються як вкладені. Більш очевидно, якщо ми витягнемо його:

def disjoint(A, B, C):
  AB = (a
        for a in A
        for b in B
        if a == b)
  ABC = (a
         for a in AB
         for c in C
         if a == c)
  for a in ABC:
    return False
  return True

Цей варіант використовує генератори для представлення проміжних результатів.

  • У генераторі ABми матимемо не більше n елементів (через гарантію, що списки вхідних даних не містять дублікатів), а виготовлення генератора займає складність O (n²).
  • Виробництво генератора ABCпередбачає цикл над генератором ABдовжини n і більше Cдовжини n , так що його алгоритмічна складність також є O (n²).
  • Ці операції не вкладаються, а відбуваються незалежно, так що загальна складність становить O (n² + n²) = O (n²).

Оскільки пари списків введення можна перевіряти послідовно, випливає, що визначення того, чи будь-яка кількість списків неперервна, може бути здійснено за час O (n²).

Цей аналіз є неточним, оскільки передбачає, що всі списки мають однакову довжину. Ми можемо сказати точніше, що ABмає максимум довжину min (| A |, | B |), і її виробництво має складність O (| A | • | B |). Виробництво ABCмає складність O (хв (| A |, | B |) • | C |). Тоді повна складність залежить від того, як упорядковані списки введення. З | А | ≤ | B | ≤ | C | ми отримуємо загальну найгіршу складність O (| A | • | C |).

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

for a in A:
  if a in B:  # might implicitly loop
    if a in C:  # might implicitly loop
      return False
return True

або у версії на основі генератора:

AB = (a for a in A if a in B)
ABC = (a for a in AB if a in C)
for a in ABC:
  return False
return True

4
Це було б набагато зрозуміліше, якби ми просто скасували цю магічну nзмінну і поговорили про фактичні змінні в процесі гри.
Олександр

15
@code_dredd Ні, це не так, він не має прямого зв'язку з кодом. Це абстракція, яка передбачає len(a) == len(b) == len(c), що хоч і правда в контексті аналізу складності часу, але це заплутає розмову.
Олександр

10
Можливо, сказати, що код ОП має найгірший випадок складності O (| A | • | B | + min (| A |, | B |) • | C |) достатньо, щоб викликати розуміння?
Пабло Н

3
Ще одна річ у термінах тестів: як ви з’ясували, вони не допомогли вам зрозуміти, що відбувається. З іншого боку, вони, здається, надали вам додаткової впевненості в тому, щоб протистояти різним невірним, але насильно викладеним твердженням, що книга явно помилялася, тому це добре, і в цьому випадку ваше тестування перемогло інтуїтивне махання рукою .. Для розуміння, більш ефективним способом тестування було б запустити його у відладчику з точками прориву (або додати відбитки значень змінних) при введенні кожного циклу.
sdenham

4
"Зауважте, що терміни не є корисним показником алгоритмічної складності". Я думаю, що це було б більш точно, якби було сказано "суворий" або "надійний", а не "корисний".
Накопичення

7

Зауважте, що якщо всі елементи різні у кожному списку, який передбачається, ви можете повторити C лише один раз для кожного елемента A (якщо в B є елемент, який дорівнює). Отже, внутрішня петля становить O (n ^ 2)


3

Будемо вважати, що жодна окрема послідовність не містить дубліката.

є дуже важливою інформацією.

В іншому випадку найгіршим варіантом оптимізованої версії все одно буде O (n³), коли A і B рівні і містять один елемент, дубльований n разів:

i = 0
def disjoint(A, B, C):
    global i
    for a in A:
        for b in B:
            if a == b:
                for c in C:
                    i+=1
                    print(i)
                    if a == c:
                        return False 
    return True 

print(disjoint([1] * 10, [1] * 10, [2] * 10))

який виводить:

...
...
...
993
994
995
996
997
998
999
1000
True

Отже, в основному, автори припускають, що O (n³) найгіршого випадку не повинно відбуватися (чому?), І "доводять", що найгіршим випадком зараз є O (n²).

Справжньою оптимізацією було б використання наборів чи диктів для перевірки включення в O (1). У цьому випадку disjointбуде O (n) для кожного введення.


Ваш останній коментар досить цікавий, не думав про це. Ви припускаєте, що це пов'язано з можливістю робити три операції O (n) послідовно?
SteveJ

2
Якщо ви не отримаєте ідеальний хеш з принаймні одним відром на вхідний елемент, ви не можете перевірити включення в O (1). Відсортований набір зазвичай має пошук O (log n). Якщо ви не говорите про середню вартість, але це не про що йдеться. І все-таки мати збалансований двійковий набір, отримуючи важкий O (n log n), є тривіальним.
Ян Дорняк

@JanDorniak: Відмінний коментар, дякую. Зараз це трохи незручно: я ігнорував найгірший випадок key in dict, як і автори. : - / На мою захист, я думаю, що набагато важче знайти дикту з nключами та nхеш-зіткненнями, ніж просто створити список із nдублюючими значеннями. І з набором чи диктатом насправді не може бути жодного повторюваного значення. Тож найгірший-найгірший випадок - це справді O (n²). Я оновлю свою відповідь.
Ерік Думініл

2
@JanDorniak Я думаю, що набори та дикти - це хеш-таблиці в python на відміну від червоно-чорних дерев у C ++. Тож абсолютний найгірший випадок - це гірше, до 0 (n) для пошуку, але середній випадок - O (1). На відміну від O (log n) для C ++ wiki.python.org/moin/TimeComplexity . Зважаючи на те, що це питання python, і що область проблеми призводить до високої ймовірності середньої ефективності випадку, я не думаю, що твердження O (1) є поганим.
Балдрік

3
Я думаю, що тут я бачу питання: коли автори кажуть «ми припустимо, що жодна окрема послідовність не містить дублюючих значень», це не є кроком у відповіді на питання; Це, скоріше, передумова, під якою буде вирішуватися питання. Для педагогічних цілей це перетворює нецікаву проблему на проблему, яка кидає виклик інтуїції людей щодо великого O - і, здається, це було успішним, судячи з кількості людей, які наполегливо наполягали на тому, що O (n²) повинен помилятися. .. Крім того, хоча тут суперечка, підрахунок кількості кроків в одному прикладі не є поясненням.
sdenham

3

Щоб розмістити речі в термінах, якими користується ваша книга:

Я думаю, у вас немає проблем з розумінням того, що перевірка на a == bнайгірший випадок O (n 2 ).

Тепер у найгіршому випадку для третьої петлі кожен aз Aмає збіг B, тому третій цикл буде викликатися кожен раз. У випадку, коли aне існує в C, він буде проходити через весь Cнабір.

Іншими словами, це 1 раз на кожен aі 1 раз на кожен c, або n * n. O (n 2 )

Отже, є О (n 2 ) + O (n 2 ), на яке вказує Ваша книга.


0

Хитрість оптимізованого методу - вирізати кути. Тільки якщо відповіді a і b збігаються, с буде заслуговувати на оцінку. Тепер ви можете зрозуміти, що в гіршому випадку вам все одно доведеться оцінювати кожен c. Це не правда.

Ви, напевно, думаєте, що найгірший випадок полягає в тому, що кожна перевірка на a == b призводить до переходу на C, оскільки кожна перевірка на a == b повертає відповідність. Але це неможливо, оскільки умови для цього суперечливі. Для цього вам знадобляться A і B, які містять однакові значення. Вони можуть бути впорядковані по-різному, але кожне значення A повинно мати відповідне значення у B.

Тепер ось кікер. Немає можливості впорядкувати ці значення так, щоб для кожного a ви мали б оцінити всі b, перш ніж знайти свою відповідність.

A: 1 2 3 4 5
B: 1 2 3 4 5

Це було б зроблено миттєво, тому що відповідні 1 - це перший елемент в обох серіях. Що стосовно

A: 1 2 3 4 5
B: 5 4 3 2 1

Це спрацювало б для першого пробігу через A: тільки останній елемент B приніс би удар. Але наступна ітерація над A вже повинна була бути швидшою, оскільки останнє місце в B вже зайняте 1. І справді цього разу знадобиться лише чотири ітерації. І це стає трохи краще з кожною наступною ітерацією.

Зараз я не математик, тому не можу довести, що це закінчиться в O (n2), але я можу відчути це на своїх засміченнях.


1
Порядок елементів тут не грає ролі. Важливою вимогою є відсутність дублікатів; Аргумент тоді полягає в тому, що петлі можна перетворити на дві окремі O(n^2)петлі; що дає загальну O(n^2)(константи ігноруються).
AnoE

@AnoE Дійсно, порядок елементів не має значення. Саме це я і демонструю.
Мартін Маат

Я бачу, що ви намагаєтеся зробити, а що ви пишете - це не помиляється, але з точки зору ОП, ваша відповідь головним чином показує, чому певний порядок думок не має значення; це не пояснення, як дійти до фактичного рішення. ОП, схоже, не вказує на те, що він насправді вважає, що це пов’язано з замовленням. Тож мені незрозуміло, як ця відповідь допомогла б ОП.
AnoE

-1

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

Для заданого значення aв A, функція порівнює aз усіма можливими bв B, і вона робить це лише один раз. Так що для даної роботи aвона виконує a == bрівно nрази.

Bне містить жодних дублікатів (жоден із списків не робить), тому для даного aбуде щонайменше одна відповідність. (Це ключ). Там, де є відповідність, aбуде порівнюватися проти всіх можливих c, це означає, що a == cпроводиться рівно n разів. Там, де немає відповідності, a == cне буває взагалі.

Отже, для даного aє або nпорівняння, або 2nпорівняння. Це трапляється для кожного a, тому найкращий можливий випадок - (n²), а найгірший - 2n².

TLDR: кожне значення aпорівнюється з кожним значенням bі проти кожного значення c, але не проти кожної комбінації з bі c. Два випуски складаються, але вони не розмножуються.


-3

Подумайте про це таким чином, деякі числа можуть бути у двох або трьох послідовностях, але середній випадок цього полягає в тому, що для кожного елемента в множині A виконується вичерпний пошук у b. Гарантується, що кожен елемент у множині A буде повторений, але мається на увазі, що менше половини елементів у наборі b буде повторене.

Коли елементи в наборі b переносяться, ітерація відбувається, якщо є збіг. це означає, що середній випадок для цієї неперервної функції - O (n2), але абсолютним найгіршим випадком для неї може бути O (n3). Якби книга не деталізувалась, вона, ймовірно, дасть вам середній випадок як відповідь.


4
У книзі цілком зрозуміло, що O (n2) - це найгірший випадок, а не середній випадок.
SteveJ

Опис функції з точки зору великої нотації O зазвичай забезпечує лише верхню межу швидкості зростання функції. З великими позначеннями O пов'язано декілька споріднених позначень, використовуючи символи o, Ω, ω і Θ, щоб описати інші види меж на асимптотичних швидкостях росту. Вікіпедія - Big O
candied_orange

5
"Якщо книга не пішла в деталі, вона, ймовірно, дасть вам середній випадок як відповідь." - Ум, ні. Без явної кваліфікації ми зазвичай говоримо про найгірший ступінь складності в моделі оперативної пам'яті. Коли мова йде про операції по структурам даних, і це зрозуміло з контексту, то ми могли б говорити про амортизується найгірший крок складності в моделі RAM. Без явної кваліфікації ми, як правило, не будемо говорити про найкращий випадок, середній випадок, очікуваний випадок, складність у часі чи будь-яку іншу модель, крім оперативної пам'яті.
Йорг W Міттаг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.