Технологические интерфейсы
Поиск и сортировка
Поиск и сортировка данных, располагающихся в оперативной памяти, – типичная и очень важная часть многих приложений, качество реализации которой во многом определяет технические характеристики программной системы в целом.
Стандарт POSIX-2001 предлагает несколько способов поиска в таблицах разных форматов.
Бинарный поиск (см. листинг 9.13) является самым быстрым среди методов, основанных на сравнении ключей. Он применим к предварительно отсортированным одномерным массивам.
#include <stdlib.h>
void *bsearch (const void *key,
const void *base,
size_t nel, size_t width,
int (*compar) (const void *,
const void *));
Листинг
9.13.
Описание функции бинарного поиска bsearch().
Функция bsearch() предназначена для выполнения бинарного поиска в соответствии с алгоритмом, описанным Д. Кнутом (см. [ 4 ] в дополнительной литературе, пункт 6.2.1, алгоритм B).
Функция bsearch() возвращает указатель внутрь массива на искомые данные или NULL в случае неудачи поиска. Предварительно массив должен быть отсортирован в возрастающем порядке согласно предоставленной функции сравнения compar().
Аргумент key указывает на объект данных, разыскиваемый в массиве ( ключ поиска ); base указывает на начало (первый элемент) массива; nel задает количество элементов в массиве; width специфицирует размер элемента в массиве.
Аргумент compar() – это функция сравнения, аргументами которой служат два указателя на сравниваемые объекты – ключ и элемент массива. В соответствии с тем, какое целое число она возвращает: меньшее нуля, равное нулю или большее нуля, ключ считается меньшим, равным или большим по отношению к элементу массива.
Для сортировки массивов целесообразно пользоваться функцией qsort() (см. листинг 9.14), реализующей метод быстрой сортировки (называемый также методом обменной сортировки с разделением, см. [ 4 ] в дополнительной литературе, пункт 5.2.2, алгоритм Q). Ее аргументы имеют тот же смысл, что и одноименные аргументы функции bsearch().
#include <stdlib.h>
void qsort (void *base, size_t nel,
size_t width,
int (*compar) (const void *,
const void *));
Листинг
9.14.
Описание функции быстрой сортировки qsort().
Рассмотрим пример последовательного применения функций qsort() и bsearch() (см. листинг 9.15). Здесь в роли элементов массива выступают указатели на цепочки символов, которые размещены в области StringSpace ; тот же тип имеет и ключ поиска. Критерием сравнения служит алфавитная упорядоченность указуемых цепочек.
/* * * * * * * * * * * * * * * * * * * * * */
/* Программа сортирует массив указателей */
/* на случайные цепочки символов, а затем */
/* выполняет в этом массиве бинарный поиск */
/* * * * * * * * * * * * * * * * * * * * * */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* Размер области для хранения цепочек символов */
#define SPACE_SIZE 10000000
/* Число элементов в таблице указателей на цепочки символов */
#define TAB_SIZE 1000000
/* Длина одной цепочки символов */
/* (включая завершающий нулевой байт) */
#define STRING_SIZE 10
/* Область для хранения цепочек символов */
static char StringSpace [SPACE_SIZE];
/* Массив указателей на цепочки символов */
static char *PtsTable [TAB_SIZE];
/* Число занятых элементов в массиве указателей */
static size_t nelst;
/* * * * * * * * * * * * * * * * * * * * * */
/* Формирование случайной цепочки символов */
/* * * * * * * * * * * * * * * * * * * * * */
static void str_rnd (char *buf, size_t str_siz) {
for ( ; str_siz > 1; str_siz--) {
*buf++ = 'A' + rand () % 26;
}
if (str_siz > 0) {
*buf = 0;
}
}
/* * * * * * * * * * * * * * * * * */
/* Заполнение массива указателями */
/* на случайные цепочки символов */
/* * * * * * * * * * * * * * * * * */
static void tabl_fill (void) {
char *pss; /* Указатель на свободное место */
/* в области StringSpace */
int i;
for (pss = StringSpace, i = 0; i < TAB_SIZE;
pss += STRING_SIZE, i++) {
if (((pss + STRING_SIZE) –
(StringSpace + SPACE_SIZE)) > 0) {
fprintf (stderr, "tabl_fill: исчерпано "
"пространство цепочек\n");
nelst = i;
return;
}
str_rnd (pss, STRING_SIZE);
PtsTable [i] = pss;
}
nelst = TAB_SIZE;
}
/* * * * * * * * * * */
/* Функция сравнения */
/* * * * * * * * * * */
static int str_compar (const void *pkey,
const void *pelem) {
return strcoll (*((char **) pkey), *((char **) pelem));
}
/* * * * * * * * * * * */
/* Сортировка и поиск */
/* * * * * * * * * * * */
int main (void) {
char *skey; /* Указатель на искомую цепочку символов */
char **res; /* Результат бинарного поиска */
/* Буфер для формирования случайных цепочек */
char sbuf [STRING_SIZE];
double ntr; /* Номер найденной случайной цепочки */
/* Заполнение массивов */
tabl_fill ();
/* Сортировка массива указателей */
qsort (PtsTable, nelst, sizeof (PtsTable [0]),
str_compar);
/* Формирование ключа поиска */
/* (будем искать первую из случайных цепочек) */
skey = StringSpace;
if ((res = (char **) bsearch (&skey, PtsTable,
nelst, sizeof (PtsTable [0]), str_compar)) != NULL) {
printf ("Указатель на первую цепочку %s\n"
"после сортировки стал %d-м элементом массива\n",
skey, (res – PtsTable) / sizeof (PtsTable [0]));
} else {
printf ("Не удалось найти цепочку %s\n", skey);
}
/* Будем формировать и искать новые случайные цепочки */
skey = sbuf;
ntr = 0;
do {
str_rnd (skey, STRING_SIZE);
ntr++;
} while (bsearch (&skey, PtsTable, nelst,
sizeof (PtsTable [0]), str_compar) == NULL);
printf ("Удалось найти %g-ю по счету случайную цепочку"
" %s\n", ntr, skey);
return 0;
}
Листинг
9.15.
Пример применения функций быстрой сортировки и бинарного поиска.
Отметим, что при сортировке будут перемещаться элементы массива PtsTable [] – указатели на цепочки символов, но, конечно, не сами цепочки.
Если на компьютере, которым в данный момент пользуется автор, измерить время выполнения приведенной программы посредством утилиты time с опцией -p, результаты будут выглядеть следующим образом (см. листинг 9.16).
Указатель на первую цепочку NWLRBBMQB после сортировки стал 133253-м элементом массива Удалось найти 168221-ю по счету случайную цепочку VBBDZTNMZ real 15.67 user 15.57 sys 0.10Листинг 9.16. Возможные результаты выполнения программы, применяющей функции быстрой сортировки и бинарного поиска.
Читателю предлагается сравнить эти результаты с экспериментально полученными собственными (и с гордостью убедиться, что его компьютер гораздо мощнее), а также оценить зависимость длительности быстрой сортировки и бинарного поиска от размера массива (и подтвердить теоретические оценки из [ 4 ] ).
Стандартом POSIX-2001, помимо бинарного, предусматривается еще несколько способов поиска – последовательный, с помощью хэш-таблиц и бинарных деревьев. Соответствующие описания сосредоточены в заголовочном файле <search.h>.
В идейном плане самым простым является последовательный поиск. Он может производиться с вставкой (функция lsearch() ) или без таковой ( lfind() ) (см. листинг 9.17).
#include <search.h>
void *lsearch (const void *key,
void *base, size_t *nelp,
size_t width,
int (*compar) (const void *,
const void *));
void *lfind (const void *key,
const void *base, size_t *nelp,
size_t width, int (*compar) (const void *,
const void *));
Листинг
9.17.
Описание функций последовательного поиска.
Функции, реализующие последовательный поиск, по способу вызова напоминают bsearch(), только аргумент nelp является указателем на число элементов в массиве, которое функция lsearch() может увеличить на единицу (если искомого элемента в массиве не было, его добавляют в конец). Разумеется, для последовательного поиска не требуется, чтобы массив был предварительно отсортирован. Упрощены и требования к функции сравнения compar(): в случае неравенства ее результат должен быть отличен от нуля.
В качестве иллюстрации применения функций последовательного поиска рассмотрим программу, генерирующую случайные цепочки символов до первого повторения (см. листинг 9.18).
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Программа генерирует случайные цепочки символов до первого */
/* повторения (или до исчерпания отведенного пространства). */
/* Для выявления повторения применяется */
/* последовательный поиск с вставкой */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include <search.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Размер области для хранения цепочек символов */
#define SPACE_SIZE 200000
/* Число элементов в таблице указателей на цепочки символов */
#define TAB_SIZE 20000
/* Длина одной цепочки символов */
/* (включая завершающий нулевой байт) */
#define STRING_SIZE 7
/* Область для хранения цепочек символов */
static char StringSpace [SPACE_SIZE];
/* Массив указателей на цепочки символов */
static char *PtsTable [TAB_SIZE];
/* * * * * * * * * * */
/* Функция сравнения */
/* * * * * * * * * * */
static int str_compar (const void *pkey,
const void *pelem) {
return strcoll (*((char **) pkey), *((char **) pelem));
}
/* * * * * * * * * * * * * * * * * * * * * */
/* Формирование случайной цепочки символов */
/* * * * * * * * * * * * * * * * * * * * * */
static void str_rnd (char *buf, size_t str_siz) {
for ( ; str_siz > 1; str_siz--) {
*buf++ = 'A' + rand () % 26;
}
if (str_siz > 0) {
*buf = 0;
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * */
/* Поиск первого повтора в последовательности */
/* случайных цепочек символов */
/* * * * * * * * * * * * * * * * * * * * * * * * */
int main (int argc, char *argv []) {
char *pss; /* Указатель на свободное место */
/* в области StringSpace */
char **res; /* Результат поиска с вставкой */
size_t nelst; /* Число занятых элементов */
/* в массиве указателей */
size_t onelst; /* Число элементов в массиве */
/* до поиска с вставкой */
for (pss = StringSpace, nelst = 0; nelst < TAB_SIZE;
pss += STRING_SIZE) {
if (((pss + STRING_SIZE) – (StringSpace +
SPACE_SIZE)) > 0) {
fprintf (stderr, "%s: Исчерпано пространство "
"цепочек\n", argv [0]);
return (1);
}
str_rnd (pss, STRING_SIZE);
onelst = nelst;
res = (char **) lsearch (&pss, PtsTable, &nelst,
sizeof (PtsTable [0]), str_compar);
if (onelst == nelst) {
/* Искомая цепочка уже была порождена ранее */
printf ("Для случайных цепочек длины %d\n"
"первое совпадение получено на цепочке "
"%s\n", STRING_SIZE, pss);
printf ("Первый раз цепочка была порождена "
"под номером %d,\n" "второй – под номером "
"%d\n", (res – PtsTable) / sizeof
(PtsTable [0]) + 1, nelst + 1);
return 0;
}
} /* for */
printf ("Из %d случайных цепочек длины %d все "
"оказались уникальными\n", TAB_SIZE, STRING_SIZE);
return 0;
}
Листинг
9.18.
Пример применения последовательного поиска с вставкой.
Указатели на порождаемые случайные цепочки помещаются в массив PtsTable [] функцией lsearch(). В этой связи обратим внимание на нескольку вычурную организацию цикла for в функции main(). По сути здесь две переменные цикла – pss и nelst. Первая продвигается стандартным образом, в заголовке цикла, но проверяется на выход за допустимые границы в его теле; вторая, напротив, стандартно проверяется, но нестандартно продвигается (в результате вызова lsearch() ).
Возможные результаты выполнения этой программы показаны на листинге 9.19.
Для случайных цепочек длины 7 первое совпадение получено на цепочке GLPCSX Первый раз цепочка была порождена под номером 2548, второй - под номером 12530 real 34.80 user 13.70 sys 0.03Листинг 9.19. Возможные результаты выполнения программы, применяющей функцию последовательного поиска с вставкой.
При экспериментах с приведенной программой следует соблюдать определенную осторожность, поскольку время ее работы квадратично зависит от величины TAB_SIZE.