Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Основные понятия.rtf
Скачиваний:
9
Добавлен:
01.04.2015
Размер:
317.79 Кб
Скачать

Время работы в худшем случае и в среднем

Итак, мы видим, что время работы в худшем случае и в лучшем случае мо­гут сильно различаться. Большей частью нас будет интересовать время работы в худшем случае (worst-case running time), которое определяется как максимальное время работы для входов данного размера. Почему? Вот несколько причин.

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

  • На практике «плохие» входы (для которых время работы близко к максимуму) могут часто попадаться. Например, для базы данных плохим запросом может быть поиск отсутствующего элемента (довольно частая ситуация).

  • Время работы в среднем (о котором мы говорим дальше) может быть до­вольно близко к времени работы в худшем случае. Пусть, например, мы сортируем случайно расположенные п чисел с помощью процедуры INSERTION-SoRT. Сколько раз придётся выполнить цикл в строках 4-8? В среднем около половины элементов массива A[1..j-1] больше A[j], так что tj в среднем можно считать равным j/2, и время Т(п) квадратично зависит от п.

В некоторых случаях нас будет интересовать также среднее время работы (average-case running time, expexted running time) алгоритма на входах данной длины. Конечно, эта величина зависит от выбранного распределения вероятно­стей, и на практике реальное распределение входов может отличаться от пред­полагаемого, которое обычно считают равномерным. Иногда можно добиться равномерности распределения, используя датчик случайных чисел.

Порядок роста

Наш анализ времени работы процедуры Insertion-Sort был основан на нескольких упрощающих предположениях. Сначала мы предположили, что время выполнения n-й строки постоянно и равно с,. Затем мы огрубили оценку до an2+bn+c. Сейчас мы пойдём ещё дальше и скажем, что время работы в худшем случае имеет порядок роста (rate of growth, order of growth) n2, отбрасывая члены меньших порядков (линейные) и не интересуясь коэффициентом при n2. Это записывают так: Т(n) = 0(n2) (подробное объяснение обозначений мы отложим до следующей главы).

Алгоритм с меньшим порядком роста времени работы обычно предпочти­тельнее: если, скажем, один алгоритм имеет время работы 0(n2), а другой — 0(n3), то первый более эффективен (по крайней мере для достаточно длинных входов; будут ли реальные входы таковыми — другой вопрос).

Построение алгоритмов

Есть много стандартных приёмов, используемых при построении алгорит­мов. Сортировка вставками является примером алгоритма, действующего по шагам (incremental approach): мы добавляем элементы один за другим к отсор­тированной части массива.

В этом разделе мы покажем в действии другой подход, который называют «разделяй и властвуй» (divide-and-conquer approach), и построим с его помощью значительно более быстрый алгоритм сортировки.

Принцип «разделяй и властвуй»

Многие алгоритмы по природе своей рекурсивны (recursive): решая некото­рую задачу, они вызывают самих себя для решения её подзадач. Идея метода «разделяй и властвуй» состоит как раз в этом. Сначала задача разбивается на несколько подзадач меньшего размера. Затем эти задачи решаются (с помо­щью рекурсивного вызова — или непосредственно, если размер достаточно мал). Наконец, их решения комбинируются и получается решение исходной задачи.

Для задачи сортировки эти три этапа выглядят так. Сначала мы разби­ваем массив на две половины меньшего размера. Затем мы сортируем каждую из половин отдельно. После этого нам остаётся соединить два упорядоченных массива половинного размера в один. Рекурсивное разбиение задачи на мень­шие происходит до тех пор, пока размер массива не дойдёт до единицы (любой массив длины 1 можно считать упорядоченным).

Нетривиальным этапом является соединение двух упорядоченных массивов в один. Оно выполняется с помощью вспомогательной процедуры Merge(A,p,q,r). Параметрами этой процедуры являются массив А и числа p,q,r, указывающие границы сливаемых участков. Процедура предполагает, что pq<r и что участки A[p..q] и A[q+1..r] уже отсортированы, и сливает (merges) их в один участок A[p..r].

Мы оставляем подробную разработку этой процедуры читателю (упр. 1.3-2), но довольно ясно, что время работы процедуры merge есть О(n), где n — общая длина сливаемых участков (n=r-p+1). Это легко объяснить на картах. Пусть мы имеем две стопки карт (рубашкой вниз), и в каждой карты идут сверху вниз в возрастающем порядке. Как сделать из них одну? На каждом шаге мы берём меньшую из двух верхних карт и кладём ее (рубашкой вверх) в результирующую стопку. Когда одна из исходных стопок становится пустой, мы добавляем все оставшиеся карты второй стопки к результирующей стопке. Ясно, что каждый шаг требует ограниченного числа действий, и общее число действий есть Θ(n).

5

2

4

6

1

3

2

6

2 5

4 6

1 3

2 6

2 4 5 6

1 2 3 6

1 2 2 3 4 5 6 6

Рис. 2. Сортировка слиянием для массива А = (5,2,4,6,1, 3,2,6).

Теперь напишем процедуру сортировки слиянием Merge-Sort(A,p,r), ко­торая сортирует участок А[р..r] массива А, не меняя остальную часть массива. При рr участок содержит максимум один элемент, и тем самым уже отсор­тирован. В противном случае мы отыскиваем число q, которое делит участок на две примерно равные части A[р..q] (содержит [n/2˥ элементов) и A[q+1..r] (содержит [п/2˩ элементов). Здесь через ˥ мы обозначаем целую часть х (наибольшее целое число, меньшее или равное х), а через [х˩—наименьшее целое число, большее или равное х.

void Merge_Sort(Vector A, int p, int r)

{ int q;

if (p<r) {

q=(p+r)/2;

Merge_Sort(A,p,q);

Merge_Sort(A,q+1,r);

Merge(A,p,q,r); // Слияние сортированных подмассивов

} // A[p..q] и A[q+1,r]

}

Весь массив теперь можно отсортировать с помощью вызова Merge-Sort(A,1, length[A]). Если длина массива п=length[A] есть степень двойки, то в процессе сортировки произойдёт слияние пар элементов в отсортирован­ные участки длины 2, затем слияние пар таких участков в отсортированные участки длины 4 и так далее до п (на последнем шаге соединяются два отсортированных участка длины п/2). Этот процесс показан на рис. 2.

В рекурсивном методе использованы:

const int maxn=100001;

typedef int Vector[maxn];

Vector A, b;

int Merge(Vector A, int p, int q, int r)

{ int i1, i2, i;

i1=p; i2=q+1; i=p;

while (i1<=q && i2<=r) {

if (A[i1]<=A[i2]) b[i++]=A[i1++];

else b[i++]=A[i2++];

}

while (i1<=q) b[i++]=A[i1++];

while (i2<=r) b[i++]=A[i2++];

for (i=p;i<=r;i++) A[i]=b[i];

return 0;

}