Мы решаем задачу сортировки: нам нужно упорядочить ключи K1,K2,…,Kn по возрастанию относительного какого-то линейного порядка.
Метод простых вставок
Допустим, мы уже отсортировали j−1 записей так, что
K1⩽K2⩽⋯⩽Kj−2⩽Kj−1
Теперь нам нужно вставить следующий ключ Kj.
Будем сравнивать его по очереди с ключами Kj−1,Kj−2,…K1, пока не обнаружим,
что его нужно вставить между ключами Ki и Ki+1.
Сдвинем все ключи, начиная с Ki+1, на одну позицию и вставим запись Kj на место i+1.
Другими словами, мы каждый раз берем новый ключ и вставляем его в правильное место.
functioninsertion_sort(mutablearray a[n]):forint j =1; j < n; j++:int i = j -1key= a[j]while i >=0and a[i]>key:
a[i+1]= a[i]
i -=1
a[i+1]=key
Количество сравнений в этом алгоритме равно числу инверсий перестановки (K1,K2,…,Kn) плюс количество выходных сравнений в цикле while — n−1
Количество записей (присваиваний элементов a) совпадает с количеством сравнений.
Бинарные и двухпутевые вставки
В алгоритме простых вставок новый j-й элемент сравнивался
в среднем с (j−1)/2 уже отсортированными элементами по порядку.
В итоге общее количество сравнений в среднем равно j=2∑n(j−1)/2=(n2−n)/4=O(n2).
Можно использовать то, что элементы, в которые мы хотим вставить новый ключ, уже отсортированы.
Будем искать место вставки с помощью бинарного поиска.
До конца это проблему не решит, ведь кроме поиска места вставки нужно её произвести.
Но даже так это неплохой буст к производительности
Итого мы для каждого элемента с номером j+1 проводим бинарный поиск
в подмассиве размера j (все, что находится спереди).
В итоге максимальное количество сравнений
Мы получили, что количество сравнений близко к оптимальному, и это не может не радовать.
Однако, наша сортировка всё равно выполняет O(n2) записей,
что сводит на нет все наши старания.
Проблема в том, что вставка в массив выполняется за O(n) записей.
Вообще, не существует линейной структуры данных,
обеспечивающей одновременно поиск и вставку быстрее, чем за O(n) операций.
Можно использовать нелинейные структуры, например сбалансированные деревья поиска или список с пропусками,
но тогда у нас будет нехилый временной оверхед на саму структуру.
Смягчить проблему можно, рассмотрев особый метод двухпутевых вставок.
Двухпутевые вставки
Выделим под массив участок памяти размером 3n, а сам массив поместим в середину.
Будем поддерживать указатели на начало массива и его длину, чтобы не потерять массив в аллоцированном участке.
При вставке элемента в массив определяем, к какому краю ближе место вставки j,
и расширяем массив в сторону этого края, копируя min(n−j,j) элементов вместо n−j.
Таким образом мы в 2 раза уменьшили ожидаемое количество записей,
получив алгоритм со средним количество записей n2/8+O(n) вместо n2/4+O(n).
Блочные вставки
Есть способ немного сгладить проблему линейных структур данных.
Нужно использовать блочный список.
Массив представляем в виде блочного списка с размером блока k.
Поиск в блочном списке длины n занимает O(n/k+logk) операций,
а вставка занимает O(k) операций, если мы знаем, в какой блок вставлять (имеем на него ссылку).
Получается, что общее время работы алгоритма сортировки блочными вставками равно
j=1∑n(O(j/k+logk)+O(k))=O(kn2+nlogk+nk)
Минимум достигается при k=n,
общее количество операций при сортировке блочными вставками
получается равным O(n3/2).
Метод экзотический, имеет какой-то оверхед на структуру и не является широко универсальным,
так как приходится следить за размерами блоков, совершать реаллокации и слияния.
Сортировка Шелла
Если в процессе сортировки мы перемещаем элементы на 1 позицию за операцию,
время выполнение будет в любом случае Ω(n2).
Пусть σ∈Sn — случайная перестановка n элементов массива.
Рассмотрим величину δj(σ)=defσ(j)−j —
расстояние от элемента исходном массиве до его правильного положения в отсортированном массиве.
Предположив равномерное распределение перестановок σ, найдём среднее
Это сумма расстояний, на которые элементы уехали от своих правильных позиций при перестановке σ.
Величина D(σ) служит оценкой сложности предстоящего процесса сортировки.
Минимум считается просто: σ∈SnminD(σ)=0 при σ=1.
Максимум достигается на перестановке, которая максимально далеко перемещает каждый элемент.
При чётном n это полный разворот,
а при нечётном n надо центральный элемент оставить на месте.
Итого σ∈SnmaxD(σ)=⌊n2/2⌋.
Для подсчёта среднего значения воспользуемся результатом для величины δj(σ):
E(D(σ))=j=1∑nE(δj(σ))=n2j=1∑n(2j)=3n2−1
Для вычисления дисперсии необходимо сначала найти дисперсию δj(σ) и ковариации расстояний для разных элементов, то есть для величин δi(σ) и δj(σ).
Давайте введем величину T(i,j), характеризующую совместную удалённость элементов массива от двух
фиксированных элементов i,j.
T(i,j)=ℓ=1∑n∣ℓ−i∣⋅∣ℓ−j∣
T(i,j)=T(j,i), поэтому можно предположить, что i>j,
разбить отрезок суммирования на 3 подотрезка,
раскрыть модули в каждом подотрезке и найти представление T(i,j):
Многочлен получен подставлением асимптотических оценок для всех компонент суммы.
Если очень надо, можно подставить все формулы и посчитать точное значение дисперсии.
В O(n), кстати, скрыто что-то около 0.4n+0.3.
Можем заключить, что полное расстояние перемещения D(σ) распределено в соответствии с
min=0,ave=3n2−1,max=⌊2n2⌋,dev=3n2−36013n+O(1)
Анализ величины D(σ) показывает,
что для эффективного метода сортировки вставками необходим механизм,
который позволил бы перемещать элементы большими скачками.
Такой механизм предложил Дональд Льюис Шелл в 1959 году.
На основе этого механизма построена сортировка Шелла.
Основная идея — разбивать элементы на пары так,
чтобы расстояние между элементами в одной паре было равно степени двойки.
inputmutablearray[int] a[n]inputarray[int] h[t]for gap in h :forint j = gap; j < n; j++:
current = a[j]
i = j - gap
while i >=0and current < a[i]:
a[i + gap]= a[i]
i -= gap
a[i + gap]= current