Сортировка вставками

    Всем привет. Сегодня продолжаем серию статей, которые я написал специально к запуску курса «Алгоритмы и структуры данных» от OTUS.




    Введение


    Сортировка массива является одной из первых серьезных задач, изучаемых в классическом курсе «Алгоритмы и структуры данных» дисциплины computer science. В связи с этим задачи на написание сортировок и соответствующие вопросы часто встречаются на собеседованиях на позиции стажера или junior разработчика.

    Постановка задачи


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

    Сортировка вставками


    В прошлый раз мы поговорили об одной из простейших сортировок — сортировке выбором. Сегодня речь пойдет о несколько более сложном алгоритме — сортировке вставками.

    Описание алгоритма


    Сортировка массива вставками осуществляется так: также как и в случае сортировки выбором массив делится на две части. Одна из частей называется отсортированной, а другая неотсортированной. Алгоритм предполагает проход по всему массиву с тем, чтобы длина отсортированной части стала равна длине всего массива. В рамках каждой итерации мы берем первый элемент неотсортированной части массива и осуществляем с ним следующую операцию: пока наш элемент строго меньше чем предыдущий меняем их местами. После чего увеличиваем длину отсортированной части массива на единицу. Таким образом путем последовательного перемещения изучаемого элемента мы добиваемся того, чтобы он встал на свое место. Пример осуществления одной итерации представлен ниже:
    1 3 5 | 2 9 6 -> 1 3 2 5 9 6 -> 1 2 3 5 9 6 -> 1 2 3 5 | 9 6

    Реализация


    Предлагаю посмотреть на реализацию данного алгоритма на языке C:

    void insertionSortFunction(double array[], int size) {
        int i, j, temp;
        // i представляет длину отсортированной части массива, начинаем с 1, потому что один элемент сам по себе считается упорядоченным
        for (i = 1; i < size; i++) {
            temp = array[i];
            for (j = i - 1; j >= 0; j--) {
                if (array[j] < temp) {
                    break;
                }
      
                array[j + 1] = array[j];
                array[j] = temp;
            }
        }
    }
    


    Анализ


    Предлагаю проанализировать данный алгоритм.

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

    $M(n) = O(1)$

    .

    Со временем все несколько интереснее. Тело внутреннего цикла само по себе выполняется за O(1), то есть не зависит от размера сортируемого массива. Это означает, что для понимания асимптотики алгоритма необходимо посчитать сколько раз выполняется это тело. Но количество итераций внутреннего цикла зависит от того, насколько хорошо упорядочены (или не упорядочены) элементы сортируемого массива. Для осуществления анализа необходимо посмотреть несколько случаев.

    Минимальное количество итераций достигается в том случае, если сортируемый массив уже отсортирован. Действительно, для каждой итерации внешнего цикла for происходит ровно одна итерация внутреннего цикла. Это так называемый лучший случай.

    $T(n) = (n - 1) * O(1) = O(n)$

    Таким образом, сортировка осуществляется за линейное время.

    В худшем случае число итераций предполагается наибольшим, то есть break никогда не срабатывает. На первой итерации внешнего цикла осуществляется одна итерация внутреннего цикла. На второй итерации внешнего цикла осуществляется 2 итерации внутреннего цикла. Продолжая рассуждение дальше, можно прийти к тому, что на последней ((n — 1) — ой) итерации внешнего цикла выполниться (n — 1) итерация внутреннего цикла. Получаем:

    $T(n) = O(1) + 2 * O(1) + 3 * O(1) + ... + (n - 1) * O(1) = O(1 + 2 + 3 + ... + (n - 1)) = O(n * (n - 1) / 2) = O(n ^ 2)$

    Для осуществления вычислений мы воспользовались свойствами О — нотации и формулой суммы арифметической прогрессии.

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

    Если подводить итоги, то асимптотика алгоритма по памяти —

    $O(1)$

    по времени в лучшем случае

    $O(n)$

    и в среднем и в худшем случаях

    $O(n^2)$

    Поэтому данную сортировку относят к классу квадратичных сортировок.

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

    Итоги


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

    $O(n)$

    упорядоченность объема данных.



    Узнать о курсе подробнее.


    OTUS. Онлайн-образование
    Цифровые навыки от ведущих экспертов

    Похожие публикации

    Комментарии 1

      0
      Есть более точный анализ сортировки вставками основанный на инверсиях. Инверсия в перестановке — это такая пара элементов, что больший из них стоит раньше. Единственная перестановка с нулевым количеством инверсий — это тождественная, она же является единственной отсортированной. Утверждается, что если сортировка вставками делает своп, то число инверсий уменьшается ровно на единицу. Отсюда количество свопов — это количество инверсий. Количество сравнений — это количества свопов плюс не больше, чем n холостых проверок, после которых срабатывает break.

      Среднее число инверсий в перестановке — n(n-1)/4. Это легко увидеть, если разбить перестановки на пары: перестановка и её перевернутый вариант (элементы идут в обратном порядке); каждая пара элементов образует инверсию либо в исходной перестановке, либо в её перевертыше, поэтому на две перестановки имеем n(n-1)/2 инверсий. Указанный анализ годится также и для произвольного массива, у которого все элементы различны

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

      Еще интересным моментом является то, что можно за nlogn (а может и быстрее, если есть знатоки RMQ подскажите пожалуйста) посчитать количество инверсий и, если оно окажется достаточно мало, то использовать сортировку вставками, а если велико — что-нибудь другое

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

      Самое читаемое