Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Kernigan_B__Payk_R_Praktika_programmirovania.pdf
Скачиваний:
78
Добавлен:
18.03.2016
Размер:
2.53 Mб
Скачать

первый проход делит массив из п элементов на две группы примерно по п/2 элементов;

второй проход разделяет две группы по п/2 элементов на 4 группы, в каждой из которых примерно по п/4 элементов;

на следующем проходе четыре группы по п/4 делятся на восемь групп по

(примерно) п/8 элементов;

и т. д.

Данный процесс продолжается примерно Iog2 n раз, поэтому общее время работы в лучшем случае пропорционально п + 2 X и/2 + 4 х и/4 + + 8 X п/8 ... (Iog2 и слагаемых), что равно п log? п. В среднем алгоритм работает совсем не намного дольше. Обычно принято использовать именно двоичные логарифмы, поэтому мы можем сказать, что быстрая сортировка работает пропорционально n long.

Эта демонстрационная реализация быстрой сортировки наиболее прозрачна, но у нее есть одна слабина. Если каждый выбор разделителя разбивает массив на две примерно одинаковые группы, то наш анализ корректен, однако если разделение слишком часто происходит неровно, то время работы будет расти скорее как п1. В нашей реализации в качестве разделителя берется случайный элемент, чтобы уменьшить шанс того, что плохие входные данные приведут к слишком большому количеству неровных разбиений массива. Но если все входные значения одинаковы, то наша реализация за каждый проход будет отделять только один элемент, поэтому время работы будет расти как п2.

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

Библиотеки

В стандартные библиотеки языков С и C++ входят функции сортировки, которые устойчивы к неблагоприятным входным данным и настроены на предельно быструю работу.

Библиотечные функции написаны так, что они могут сортировать данные любого типа, но мы в свою очередь должны адаптироваться к их интерфейсу, что может быть несколько сложнее, чем в рассмотренном выше примере. В языке С библиотечная функция называется qso rt, и ей нужно предоставлять функцию сравнения двух значений. Поскольку значения могут быть любых типов, то функции сравнения передаются два нетипизированных указателя (void *) на сравниваемые значения. Функция преобразует указатели к нужному типу, извлекает значения данных, сравнивает их и возвращает результат (отрицательный, нуль или положительный в зависимости от того, меньше ли первый элемент, равен ли второму или больше его).

Рассмотрим реализацию функции сравнения для сортировки массива строк, случая, встречающегося довольно часто. Мы написали функцию scmp, которая -преобразует параметры к другому типу и затем вызывает st rcmp для выполнения самого сравнения:

/* scmp: сравнение строк *р1 и *р2 */ int scmp(const void *p1, const void *p2)

{

char *v1, *v2;

v1 = *(char **) p1; v2 = *(char **) p2; return strcmp(v1, v2); }

Мы могли бы написать эту функцию в одну строку, но при использовании временных переменных код становится более удобочитаемым.

Мы не можем напрямую использовать strcmp как функцию сравнения, поскольку qsort передает адрес каждого элемента в массиве &s t г [ i ] (типаспаг **), ане str[i] (типа char *), как показано на рисунке:

Для сортировки элементов массива строк с st r[0] по st r[N-1 ] функция qsort должна получить массив, его длину, размер сортируемых элементов и функцию сравнения:

char *str[N];

qsort(str, N, sizeof(str[OJ), scmp);

А вот аналогичная функция icmp для сравнения целых:

{

int v1, v2;

v1 = *(int *) p1; v2 = *(int *.) p2; if (v1 < v2)

return -1; else if (v1 == v2) return 0; else return 1; >

}

Мы могли бы написать

? return v1-v2;

но если v2 — большое положительное число, a v1 — большое по абсолютному значению отрицательное или наоборот, то получившееся переполнение привело бы к неправильному ответу. Прямое сравнение длиннее, но надежнее.

И здесь при вызове qsort нужно передать массив, его длину, размер сортируемых элементов и функцию сравнения:

int arr[N];

qsort(arr, N, sizeof(arr[0]), icmp);

В стандарте ANSI С определена также функция двоичного поиска bsea rch. Как и qso rt, этой функции нужен указатель на функцию сравнения (часто на ту же, что используется для qsort); bsearch возвращает указатель на найденный элемент или на NULL, если такого элемента нет. Вот наша программа для поиска имен в HTMLфайле, переписанная с использованием bsearch:

/* lookup: использует bsearch для поиска name в таблице tab, возвращает индекс */

int lookup(char *name, Nameval tab[], int ntab) {

Nameval key, *np;

key.value = 0; /* не используется;

годится любое значение */

np = (Nameval *) bsearch(&key, tab, ntab, sizeof(tab[0]), nvcmp); if (np == NULL) return -1; else

return np-tab;

Как и в случае qsort, функция сравнения получает адреса сравниваемых значений, поэтому ключевое (искомое) значение должно иметь этот же тип; в данном примере нам пришлось создать фиктивный элемент типа Nameval для передачи в функцию сравнения. Сама функция сравнения nvcmp сравнивает два значения типа Nameval, вызывая st rcmp для их строковых компонентов, полностью игнорируя численные компоненты:

/* nvcmp: сравнивает два вначения типа

Nameval */

int nvcmp(const void *va, const void *vb)

{

const Nameval *a, *b;

a = (Nameval *) va;* b = (Nameval *) vb; return strcmp(a->name, b->name); }

Это похоже на scmp, только с тем отличием, что строки хранятся как члены структуры.

Неудобство передачи ключевого значения показывает, что bsearch предоставляет меньше возможностей, чем qso rt. Хорошая многоцелевая функция сортировки занимает одну-две страницы кода, а двоичный поиск — ненамного больше, чем код для интерфейса с bsearch. Тем не менее лучше использовать bsearch, чем писать свою собственную версию. Как показывает опыт, программистам на удивление трудно написать двоичный поиск без ошибок.

В стандартной библиотеке C++ имеется обобщенная функция sort, которая обеспечивает время работы 0(п log n). Код для ее вызова проще, потому что нет необходимости в преобразовании типов и размеров элементов. Кроме того, для порядковых типов не требуется задавать функцию сравнения:

int arr[N]; sort(arr, arr+N);

Эта библиотека содержит также обобщенные функции двоичного поиска, с теми же преимуществами в вызове.

Упражнение 2-1

Алгоритм быстрой сортировки проще всего выразить рекурсивно. Реализуйте его итеративно и сравните две версии. (Хоар рассказывает, как было трудно разработать итеративный вавариант быстрой сортировки и как легко все оказалось, когда он сделал ее рекурсивной.)

Быстрая сортировка на языке Java

В Java ситуация другая. В ранних версиях не было стандартной функции сортировки, поэтому приходилось писать собственную. В последних версиях появилась такая функция, работающая с классами, реализующими интерфейс Comparable, поэтому теперь мы можем просить библиотеку сортировать то, что нам потребуется. Но поскольку используемые технологии полезны и в других ситуациях, в данном разделе мы опишем все детали реализации быстрой сортировки в Java.

Адаптировать быструю сортировку для каждого конкретного типа данных легко; однако же более поучительно написать обобщенную функцию для сортировки объектов любых типов, что больше похоже на интерфейс qsort.

Одно из крупных отличий от С и C++ заключается в том, что в Java мы не можем передать функцию сравнения в другую функцию — здесь не существует указателей на функции. Вместо этого мы создаем интерфейс (interface), единственным содержимым которого будет функция, сравнивающая два объекта типа Object. Далее для каждого сортируемого типа данных мы создаем класс с функцией (методом), которая реализует интерфейс для этого типа данных. Мы передаем экземпляр класса в функцию сортировки, которая в свою очередь использует функцию сравнения из этого класса для сравнения элементов.

Сначала опишем интерфейс Стр, который определяет единственную функцию стр, сравнивающую два значения типа Object:

interface Cmp {

int cmp(0bject x, Object y);

}

Теперь мы можем написать функции сравнения, которые реализуют этот интерфейс; например, следующий класс определяет функцию, сравнивающую объекты типа

Integer:

// Icmp: сравнение целых class Icmp implements Cmp {

public int cmp(0bject o1, Object o2)

{

int 11 = ((Integer) o1). intValueO; int 12 =

((Integer) o2). intValueO; if (11 < 12) return -1; else if (11 == 12)

return 0; else return 1; } }

;

аэта функция сравнивает объекты типа St ring:

//Scmp: сравнение

строк class

Scmp implements Cmp {

public int cmp(0bject o1, Object o2)

{

String s1 = (String) o1; String s2 = (String) o2; return s1.compareTo(s2);

} }

Данным способом можно сортировать только типы, наследуемые от класса Object; подобный механизм нельзя применять для базовых типов, таких как int или double. Поэтому мы сортируем элементы типа Integer,

а не int.

С этими компонентами мы теперь можем перенести функцию быстрой сортировки из языка С в Java и вызывать в ней функцию сравнения из объекта Стр, переданного параметром. Наиболее существенное изменение — это использование индексов left

иright, поскольку в Java нет указателей на массивы.

//Quicksort.sort: сортировать v[left]..v[right]

//алгоритмом quicksort

static void sort(0bject[] v, int left, int right, Cmp cmp)

{

int i, last;

if (left >= right) // ничего делать не надо return;

swap(v, left, rand(left,right)); // поместить разделитель last = left; // в v[left]

for (i = left+1; i <= right; i++) // разделить массив if (cmp.cmp(v[i], v[left]) < 0) swap(v, ++last, i);

swap(v, left, last); // вернуть разделитель sort(v, left, last-1, cmp); // рекурсивно отсортировать sort(v, last+1, right, cmp); // обе части }

Quicksort.sort использует cmp для сравнения двух объектов, как и раньше, вызывает swap для их обмена.

//Quicksort.swap: обменять v[i]

иv[j] static void swap

(0bject[] v, int i, int j) { Object temp;

temp = v[i]; v[i] = v[j] ; v[j] = temp; }

Генерация случайного номера происходит в функции rand, которая а возвращает случайное число в диапазоне с left по right включительно:

static