Лекция 4
Реализация
В реализации сортировки Шелла на Си следует
изменить операторы typedef T и
сравнения compGT так, чтобы они
соответствовали данным, хранимым в массиве. Основная часть алгоритма -
сортировка вставками с шагом h.
// Коды
для сортировки Шелла
typedef int T;
/* Тип элемента, который нужно сортировать */
typedef int tblIndex; /* Тип
ключа сортировки */
#define compGT(a,b) (a > b)
void shellSort(T *a, tblIndex lb, tblIndex ub) {
tblIndex n, h, i, j;
T t;
/*********************************
* сортируемый массив a[lb..ub] *
*********************************/
/* Вычисление наибольшего шага */
n = ub - lb + 1;
h = 1;
if (n <
14)
h = 1;
else if
(sizeof(tblIndex) == 2 && n > 29524)
h =
3280;
else {
while (h
< n) h = 3*h + 1;
h /= 3;
h /= 3;
}
while (h > 0) {
/* «Сортировка вставкой» с шагом h */
for (i = lb + h; i <= ub; i++) {
t =
a[i];
for
(j = i-h; j >= lb && compGT(a[j], t); j -= h)
a[j+h] = a[j];
a[j+h] = t;
}
/* Вычисление следующего шага */
h /= 3;
}
}
Быстрая сортировка
Хотя идея Шелла значительно улучшает сортировку вставками,
резервы еще остаются. Один из наиболее известных алгоритмов сортировки -
быстрая сортировка, предложенная Ч.Хоором. Метод и в самом деле очень быстр,
недаром по-английски его так и величают QuickSort к неудовольствию всех
«спелл-чекеров».
Этому методу требуется O(n lg n) в среднем и O(n2) в худшем случае. К
счастью, если принять адекватные предосторожности, наихудший случай крайне
маловероятен. Быстрый поиск не является устойчивым. Кроме того, ему требуется
стек, т.е. он не является и методом сортировки на месте. Дальнейшую информацию
можно получить в работе Кормена [1990] [Cormen].
Теория
Алгоритм разбивает сортируемый массив на разделы, затем
рекурсивно сортирует каждый раздел. В функции Partition (рис. 2.3) один из
элементов массива выбирается в качестве центрального. Ключи, меньшие
центрального следует расположить слева от него, те, которые больше, - справа.
int function Partition
(Array A, int Lb, int Ub);
begin
Выбрать центральный среди
элементов A[Lb]...A[Ub];
Переупорядочить A[Lb]...A[Ub] так
чтобы:
слева от центра все значения были <= центрального
справа от центра все значения были
>= центрального
Вернуть
индекс центрального элемента;
end;
procedure QuickSort (Array A, int Lb, int Ub);
begin
if Lb < Ub then
M = Partition (A, Lb, Ub);
QuickSort (A, Lb, M - 1);
QuickSort (A, M + 1, Ub);
end;
|
Рис. 2.3. Быстрый поиск
На рис. 2.4(a) в качестве центрального (pivot)
выбран элемент 3. Индексы начинают изменяться с концов массива. Индекс i
начинается слева и используется для выбора элементов, которые больше
центрального, индекс j начинается справа и используется для выбора элементов,
которые меньше центрального. Эти элементы меняются местами - см. рис. 2.4(b).
Процедура QuickSort рекурсивно
сортирует два подмассива, в результате получается массив, представленный на
рис. 2.4(c).
Рис 2-4. Пример работы алгоритма Quicksort
В процессе сортировки может потребоваться передвинуть
центральный элемент. Если нам повезет, выбранный элемент окажется медианой
значений массива, т.е. разделит его пополам. Предположим на минутку, что это и
в самом деле так. Поскольку на каждом шаге мы делим массив пополам, а функция
Partition в конце концов просмотрит все n
элементов, время работы алгоритма есть O(n lg n).
В качестве центрального функция Partition может попросту брать первый элемент (A[Lb]). Все остальные
элементы массива мы сравниваем с центральным и передвигаем либо влево от него,
либо вправо. Есть, однако, один случай, который безжалостно разрушает эту
прекрасную простоту. Предположим, что наш массив с самого начала отсортирован.
Функция Partition всегда будет
получать в качестве центрального минимальный элемент и потому разделит массив
наихудшим способом: в левом разделе окажется один элемент, соответственно, в
правом останется Ub - Lb элементов. Таким образом, каждый
рекурсивный вызов процедуры QuickSort
всего лишь уменьшит длину сортируемого массива на 1. В результате для
выполнения сортировки понадобится n
рекурсивных вызовов, что приводит к времени работы алгоритма порядка O(n2). Один из способов
побороть эту проблему - случайно выбирать центральный элемент. Это сделает
наихудший случай чрезвычайно маловероятным.
Реализация
В приведенной реализации
typedef int T;
/* Тип элемента, который нужно сортировать */
typedef int tblIndex; /* Тип
ключа сортировки */
#define сompGT(a,b) (a > b)
tblIndex partition(T *a, tblIndex lb,
tblIndex ub) {
T t, pivot;
tblIndex i, j, p;
/**********************************
* разделение
массива a[lb..ub] *
**********************************/
/* выбор центрального элемента и обмен его
с первым */
p = lb + ((ub -
lb)>>1);
pivot = a[p];
a[p] = a[lb];
/* сортировка lb+1..ub относительно центрального элемента */
i = lb+1;
j = ub;
while (1) {
while (i < j && compGT(pivot, a[i])) i++;
while (j >= i && compGT(a[j], pivot)) j--;
if (i >= j) break;
t = a[i];
a[i] = a[j];
a[j] = t;
j--; i++;
}
/* центральный элемент размещается в a[j] */
a[lb] = a[j];
a[j] = pivot;
return j;
}
void quickSort(T *a, tblIndex lb, tblIndex
ub) {
tblIndex m;
/**********************************
* сортировка
массива a[lb..ub] *
**********************************/
while (lb < ub) {
/* короткие массивы сортируются методом
вставки */
if (ub - lb
<= 12) {
insertSort(a, lb, ub);
return;
}
/* разделение на два сегмента */
m = partition
(a, lb, ub);
/* сортировка меньшего раздела
*/
/* требуется более короткий стек */
if (m - lb
<= ub - m) {
quickSort(a, lb, m - 1);
lb = m + 1;
} else {
quickSort(a, m + 1, ub);
ub = m - 1;
}
}
}
|