Технологические интерфейсы
Поиск и сортировка
Поиск и сортировка данных, располагающихся в оперативной памяти, – типичная и очень важная часть многих приложений, качество реализации которой во многом определяет технические характеристики программной системы в целом.
Стандарт 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.