При изучении алгоритмов сортировок, возник вопрос об общепринятой оценке сложности, а так же к примерам реализации. И эти вопросы возникли сразу на первой сортировке Пузырьком. Заговор? Невнимательность? Небрежность? Шутка?

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

Далее на просторах интернета можно встретить разные реализации данной сортировки, например (JS):

function BubbleSort(array)
{
  for(i = 0; i < array.length; i++)
  {
    for(j = 0; j < array.length - 1; j++)
    {
      if(array[j] > array[j + 1])
      {
        var temp = array[j]
        array[j] = array[j + 1]
        array[j + 1] = temp
      }
    }
  }
}

И тут возникают несостыковки:

  1. В каком случае тут может быть худший и лучший варианты, что должно прервать цикл?

  2. По описанию сортировки, в каждой новой итерации не нужно учитывать элементы которые уже на своем месте, а тут это игнорируется.

  3. Если придет уже отсортированный массив, то зачем в принципе делать следующие итерации

Если покопаться в других примерах, или немного подумать логически то это решается следующим примером (и снова JS):

function BubbleSort2(array)
{
  var result = [...array] //Делаем копию массива, чтобы не мутировать данные
  var isSorted = false //Утверждение что массив не отсортирован
  var temp /*Переменная для операции обмена, 
            вынесена сюда чтобы сразу зарезервировать ячеку памяти, 
            а не создавать каждый раз при операции обмена*/

  function Swap(a, b) //Вынесли операцию обмена в отдельную функцию
  {
    temp = result[a]
    result[a] = result[b]
    result[b] = temp
  } 
  /*Можно сделать и без 3ей переменной, но думаю это избыточно, 
    т.к. памяти много не занимает*/
    
  for(i = 0; i < result.length && !isSorted; i++) //Цикл прервется если массив отсортирован
  {
    isSorted = true //Вначале каждой итерации утверждаем что массив отсортирован
    for(j = 0; j < result.length - (1 + i); j++) //Уменьшаем с каждой новой итерацией
    {
      if(result[j] > result[j + 1])
      {
        Swap(j, j+1)
        isSorted = false /*Если была произведена операция обмена, 
                           значит массив еще не отсортирован*/
      }
    }
  }

  return result
}

Теперь реализация подходит под условия сортировки и достаточно логична, но есть одно НО!. Если добавить счетчик количества проходов по массиву, то получится следующая картина (возьмём наихудший сценарий, массив из 5ти элементов отсортированный в обратном порядке):

Подсчет количества произведенных действий
Подсчет количества произведенных действий

Как видно из изображения выше, последняя итерация лишняя, поэтому для первого цикла for можно взять длину (N - 1), но это совершенно не критично.
Но вот то что счетчик показал 15 вместо положенных худшему сценарию 5^2=25, уже странно, куда делось еще 10 операций. Или возможно все таки сложность другая? Но какая?

Ответ: Сложность сортировки пузырьком в худшем сценарии НЕ квадратичная, а равна сумме арифметической прогрессии с разностью в единицу, или n*(n+1)/2 или

Под a1 и an следует понимать не само значение, а порядковый номер
Под a1 и an следует понимать не само значение, а порядковый номер

Я понимаю что там производится несколько операций, но это K которую как правило отбрасывают, и что сложность называют приблизительно n^2.

Но все же странно, что в такой сфере, где бывают возникают споры и из-за более мелких причин, вдруг так пренебрежительно оценивают алгоритм, хотя на выходе мы получаем разницу практически в 2 раза! Например если элементов 100, то фактически 5050 против 10000, а это почти 50%.

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

Вывод

У сортировки пузырьком сложность O(n*(n+1)/2), но это не делает её быстрее, это по прежнему медленная сортировка. Однако если Ваш алгоритм действительно имеет сложность O(n^2), то это практически в 2 раза хуже чем алгоритм Bubble Sort в худшем сценарии!

Комментарии (39)


  1. middle
    00.00.0000 00:00
    +41

    Освежите в памяти определение символа O.


    1. s_f1
      00.00.0000 00:00
      +12

      Думаю, в данном случае не «освежите в памяти», а «узнайте».
      Возможно ещё, что это очередная курсовая такая.


      1. middle
        00.00.0000 00:00
        +5

        Я старался не думать о человеке плохо.


  1. GrigorGri
    00.00.0000 00:00
    +3

    Насколько я помню, сортировка пузырьком имеет оба цикла 0..length; 0..length. То что вы описали с 0..length, i..length обычно называется "оптимизированная сортировка пузырьком". Так что никакого противоречия нет, у классической сортировки пузырьком как раз N^2 итераций.

    https://en.m.wikipedia.org/wiki/Bubble_sort можно посмотреть вот тут секцию Optimizing bubble sort.


    1. GrigorGri
      00.00.0000 00:00
      +5

      Ну и да, как выше написали O(n*(n+1)/2) раскрывается в O(n^2).


  1. volatilization
    00.00.0000 00:00
    +19

    Именно это и значит O(n^2)


  1. JekaMas
    00.00.0000 00:00
    +7

    Я бы, убрал эту статью подальше, на вашем месте. Может неплохо подпортить резюме.


    1. sci_nov
      00.00.0000 00:00
      +1

      Это нормальный этап в начале пути познания. Поверьте, я делал подобные вещи, когда начинал работать преподавателем, и только на 7-й 8-й год работы вышел на твёрдый хороший уровень.


      1. molnij
        00.00.0000 00:00

        Начало пути познания это школа. Не разобраться в школе с o/O нотацией -нормально. Не разобраться во время обучения в универе - уже, имхо, вопросики возникают. Не разобраться будучи "фулстек-разработчиком" - это уже прям тревожный признак, и именно то что написали в стартовом комменте.


        1. sci_nov
          00.00.0000 00:00
          +1

          Всё относительно :).


      1. GospodinKolhoznik
        00.00.0000 00:00

        Определение О большое даётся на первом курсе матана. Хорошие нынче препы пошли, не знают программу по математике за 1й курс...


  1. xi-tauw
    00.00.0000 00:00
    +20

    Эх. Ожидал статью уровня Почти во всех реализациях двоичного поиска и сортировки слиянием есть ошибка, а оказалось, что автор просто не понимает О-нотацию.


  1. Myclass
    00.00.0000 00:00
    +3

    Даже, если бы вы вышли на n² + n×5000 + 5000, и у вас в массиве будут или 10 млн. или 4 элемента или как в вашем случае - n×(n +1)/2, всё равно у вас сложность будет n²...


  1. Rusrst
    00.00.0000 00:00

    Тут издательство Питер рекомендовал книгу -

    Алгоритмы на практике, Даниэль Зингару, там есть рассказ про нотацию О, что это значит, и почему так.


  1. LeshaRB
    00.00.0000 00:00
    +1

    Рекомендую книгу

    Вильям Спрингер

    Гид по Computer Science, расширенное издание


  1. Fil
    00.00.0000 00:00
    +1

    Более того, оценка O(n100) для пузырьковой сортировки — тоже корректна. Можно оценить точнее как Θ(n2) для худшего случая


  1. sci_nov
    00.00.0000 00:00
    +2

    Резервировать ячейку памяти за пределами функции нет необходимости, лучше резервировать прямо в функции. Swap без третьей переменной? Вряд ли, доп. регистр процессора всё равно будет задействован. Сейчас компиляторы многое умеют оптимизировать, и лучшим стилем программирования является стиль без излишеств: от простого к чуть более сложному с осторожностью.


    1. klimkinMD
      00.00.0000 00:00

      Swap без третьей переменной?

      a = a XOR b;

      b = b XOR a;

      a = a XOR b;


      1. sci_nov
        00.00.0000 00:00

        Так это с дополнительной арифметикой :).


      1. sci_nov
        00.00.0000 00:00

        А как на это отреагирует компилятор - большой вопрос. Я как-то делал байтовый swap для целого числа без знака. Использовал шаблон и возможность С++20 - concept (unsigned integral). Честный код (с циклом и т.п.) компилируется в одну инструкцию ЦПУ byteswap().


      1. kchhay
        00.00.0000 00:00

        В том, что это работает с числами - я уверен. А если там строки/кастомные объекты? Если побитово будет ковырять, то тоже, но я чето не уверен.
        Для чисел ещё можно через сумму. Типа,
        a=a+b
        b=a-b
        a=a-b


        1. klimkinMD
          00.00.0000 00:00

          a = a + b -- переполнения не боитесь?


          1. sci_nov
            00.00.0000 00:00

            Для unsigned нормально.


  1. dcc0
    00.00.0000 00:00

    Какую функцию выполняет запятая после слова сортировок?


  1. mbait
    00.00.0000 00:00
    -7

    Автор молодец, провёл хороший анализ. Между прочим, можно рассказать об этом на собеседовании и получить преимущество перед другими кандидатами. Но мне кажется, что можно углубиться в тему ещё сильнее - посчитать сколько реальных опреаций (тактов, инструкций) тратится на сортировку пузырьком. Ведь очевидно, что для Си и Python сложность алгоритма (если считать её по методике из статьи) будет очень сильно различаться. Так почему на собеседованиях спрашивают сложность алгоритма без привязки к конкретному языку? С такой темой уже можно выходить на диплом.


    1. zede
      00.00.0000 00:00
      +1

      Еще бы автор поинтересовался что такое O для которого он и сделал разоблачение. Так как то что мы увидели является обычным поверхностным пониманием темы. И перед "разгромной" статьей лучше бы спросил у более опытного коллеги: "а мои рассуждения верны?"


    1. lamerok
      00.00.0000 00:00
      +4

      Как сложность алгоритма может зависеть от языка?

      Скорость работы зависит, но сложность точно нет. Сложность, она же у алгоритма.


      1. GospodinKolhoznik
        00.00.0000 00:00
        +1

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


    1. klimkinMD
      00.00.0000 00:00
      +2

      С такой темой уже можно выходить на диплом.

      А там и на премию Дарвина, не грех, замахнуться


    1. playermet
      00.00.0000 00:00
      +1

      Так почему на собеседованиях спрашивают сложность алгоритма без привязки к конкретному языку?

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

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


      1. DenisPantushev
        00.00.0000 00:00

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

        При этом в C# Dictionnary, цитирую

        https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2?redirectedfrom=MSDN&view=net-7.0

        Универсальный класс Dictionary<TKey,TValue> обеспечивает сопоставление набора ключей с набором значений. Каждое дополнение к словарю состоит из значения и связанного с ним ключа. Получение значения с помощью его ключа происходит очень быстро, близко к O(1), поскольку класс Dictionary<TKey,TValue> реализован в виде хэш-таблицы.

        Как реально в коде реализовано, я не знаю.

        Поэтому, если вы на собесе по java скажете, что поиск в hashMap осуществляется за O(1), вы его провалите.


        1. sci_nov
          00.00.0000 00:00

          Нигде не описано, а вы тогда откуда знаете :)?


          1. DenisPantushev
            00.00.0000 00:00

            Имею в виду, что O(n), есть, O(log(n)) есть, а вот O(sqrt(n)) нет, хотя у hashMap в java именно такая сложность.


            1. sci_nov
              00.00.0000 00:00

              Странно, конечно. Ничего особенного в O(n^0.5) нет. Должно быть в документации, если это так, а иначе откуда мы должны знать?


            1. playermet
              00.00.0000 00:00

              А откуда вообще инфа, что у него такая сложность? Не спец по java, но во всех источниках до которых дотянулся говорится что до какой-то версии был связный список с O(n), а потом хешмап улучшили, и теперь при возможности сравнения элементов используется дерево с O(log(n)).


        1. middle
          00.00.0000 00:00

          This implementation provides constant-time performance for the basic
          operations (get and put), assuming the hash function
          disperses the elements properly among the buckets.

          https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html

          Ну, а если не равномерно распределяет -- ССЗБ. Логарифм -- это ещё легко отделались.


        1. Ritan
          00.00.0000 00:00

          И откуда это тайное знание? В коде стандартной библиотеки я вижу совершенно обычную версию поиска с закрытой адресацией. То что поиск в словаре это "амортизированное" О(1) - не меняет ничего. Тот факт, что ноды при переполнении превращаются в деревья - тоже


        1. playermet
          00.00.0000 00:00

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


  1. DenisPantushev
    00.00.0000 00:00

    Ох, школота... при расчете о большое, выполняются правила:

    1. константные слагаемые выбрасываются, n+1 => n

    2. умножение на константу выбрасываются n*1/2 => n

    3. члены с меньшей степенью выбрасываются, n3+n2+n => n3.

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

    Справедливости ради, при прослушивании курсов по java почти все преподаватели, описывая алгоритмы и контейнеры, "описывали" сложность (сложно это называть это О большое) как "при поиске элемента в хешмапе, содержащей 10000 элементов, будет производится проход по ста элементам массива и еще по ста элементам массива". Вот какая это сложность? Насколько увеличится количество итерации при увеличении исходных данных в десять раз?