Анализ алгоритмов
Сортировка вставками
Сортировка вставками (insertion sort) удобна для сортировки коротких последовательностей. Именно таким способом обычно сортируют карты: держа в левой руке уже упорядоченные карты и взяв правой рукой очередную карту, мы вставляем её в нужное место, сравнивая с имеющимися и идя справа налево (рис. 1.1).
Запишем этот алгоритм в виде процедуры Insertion-Sort, параметром которой является массив А[1..п] (последовательность длины n, подлежащая сортировке). Мы обозначаем число элементов в массиве А через length[A]. Последовательность сортируется «на
Рис. 1. Работа процедуры Insertion-Sort для входа А = (5,2,4,6,1,3). Позиция j показана жирными числами.
месте», без дополнительной памяти (in place): помимо массива мы используем лишь фиксированное число ячеек памяти. После выполнения процедуры Insertion-Sort массив А упорядочен по возрастанию.
1 for (j=2;j<=n;j++){
2 key=A[j];
3 i=j-1;
4 while (i>0 && A[i]>key){
5 A[i+1]=A[i];
6 i=i-1;
7 }
8 A[i+1]=key;
9 }
На рис. 1.2 показана работа алгоритма при А = (5,2,4,6,1,3). Участок A[1.. j-1] составляют уже отсортированные карты, a A[j + 1..n] — ещё не просмотренные. В цикле for индекс j пробегает массив слева направо. Мы берём элемент A[j] (строка 2 алгоритма) и сдвигаем идущие перед ним и большие его по величине элементы (начиная с (j-1)-го) вправо, освобождая место для взятого элемента (строки 3-6). В строке 8 элемент A[j] помещается на освобождённое место.
Анализ алгоритмов
Рассматривая различные алгоритмы решения одной и той же задачи, полезно проанализировать, сколько вычислительных ресурсов они требуют (время работы, память), и выбрать наиболее эффективный. Конечно, надо договориться о том, какая модель вычислений используется. В качестве модели используется обычная однопроцессорная машина с произвольным доступом (random-access machine, RAM), не предусматривающая параллельного выполнения операций.
Сортировка вставками: анализ.Время сортировки вставками зависит от размера сортируемого массива: чем больше массив, тем больше может потребоваться времени. Обычно изучают зависимости времени работы от размера входа. (Впрочем, для алгоритма сортировки вставками важен не только размер массива, но и порядок его элементов: если массив почти упорядочен, то времени требуется меньше.)
Как измерять размер входа (input size)? Это зависит от конкретной задачи. В одних случаях размером разумно считать число элементов на входе (сортировка, преобразование Фурье). В других более естественно считать размером общее число битов, необходимое для представления всех входных данных. Иногда размер входа измеряется не одним числом, а несколькими (например, число вершин и число рёбер графа).
Временем работы (running time) алгоритма мы называем число элементарных шагов, которые он выполняет — вопрос только в том, что считать элементарным шагом. Мы будем полагать, что одна строка псевдокода требует не более чем фиксированного числа операций (если только это не словесное описание каких-то сложных действий — типа «отсортировать все точки по x-координате»). Мы будем различать также вызов (call) процедуры (на который уходит фиксированное число операций) и её исполнение (execution), которое может быть долгим.
Итак, вернёмся к процедуре Insertion-Sort и отметим около каждой строки её стоимость (число операций) и число раз, которое эта строка исполняется. Для каждого j от 2 до n (здесь n = length[A] — размер массива) подсчитаем, сколько раз будет исполнена строка 4, и обозначим это число через tj. (Заметим, что строки внутри цикла выполняются на один раз меньше, чем проверка, поскольку последняя проверка выводит из цикла.)
Insertion-Sort(A) стоимость число раз
1 for(j=2;j<=n;j++){ c1 n
2 key=A[j]; c2 n-1
3 i=j-1; c3 n-1
4 while (i>0 && A[i]>key){ c4 ∑j=2ntj
5 A[i+1]=A[i]; c5 ∑j=2n(tj-1)
6 i=i-1; c6 ∑j=2n(tj-1)
7 }
8 A[i+1]=key; c8 n-1
9 }
Строка стоимостью с, повторённая m раз, даёт вклад cm в общее число операций. (Для количества использованной памяти этого сказать нельзя!) Сложив вклады всех строк, получим
T(n)=c1n+c2(n-1) +c3(n-1)+c4∑j=2ntj+c5∑j=2n(tj-1)+c6∑j=2n(tj-1)+c8(n-1)
Как мы уже говорили, время работы процедуры зависит не только от n, но и от того, какой именно массив размера n подан ей на вход. Для процедуры Insertion-Sort наиболее благоприятен случай, когда массив уже отсортирован. Тогда цикл в строке 4 завершается после первой же проверки (поскольку А[i]≤key при i=j-1), так что все tj равны 1, и общее время есть
T(n)=c1n+c2(n-1) +c3(n-1)+c4(n-1) +c8(n-1)= (c1+с2+с3+с4+ с8)n - (с2+с3+с4+ с8).
Таким образом, в наиболее благоприятном случае время Т(n), необходимое для сортировки массива размера га, является линейной функцией (linear function) от n, т.е. имеет вид Т(n) = аn-b для некоторых констант а и b. (Эти константы определяются выбранными значениями с1,...,с8.)
Если же массив расположен в обратном (убывающем) порядке, время работы процедуры будет максимальным: каждый элемент A[j] придётся сравнить со всеми элементами А[1],..., A[j-1]. При этом tj=j. Поскольку
∑j=2nj=n(n+1)/2-1, ∑j=2n(j-1)=n(n-1)/2,
получаем, что в худшем случае время работы процедуры равно
Т(п)=с1+с2(п-1) +с3(п-1) + с4 (n(n+1)/2-1)+c5+c6)(n(n-1)/2)+c8(n-1)= 0.5(c4+c5+c6)n2+(c1+c2+c3+0.5(c4-c5-c6+c8)n-(c2+c3+c4+c8).
Теперь функция Т(n) — квадратичная (quadratic function), т.е. имеет вид Т(п)=an2 + bn + с. (Константы а, b и c здесь также определяются значениями c1, ... ,с8.)