Технологические интерфейсы
Управление хэш-таблицами поиска производится в соответствии с алгоритмом, описанным Д. Кнутом (см. [ 4 ] в дополнительной литературе, раздел 6.4, алгоритм D). Предоставляются функции для создания ( hcreate() ) и ликвидации ( hdestroy() ) хэш-таблиц, а также для выполнения в них поиска ( hsearch() ), быть может, с вставкой (см. листинг 9.20). Сразу отметим, что в каждый момент времени может быть активна только одна хэш-таблица.
#include <search.h> int hcreate (size_t nel); void hdestroy (void); ENTRY *hsearch (ENTRY item, ACTION action);Пример 9.20. Описание функций управления хэш-таблицами поиска.
Предполагается, что элементы таблицы поиска имеют тип ENTRY, определенный так, как показано на листинге 9.21.
typedef struct entry { char *key; /* Ключ поиска */ void *data; /* Дополнительные данные, */ /* ассоциированные с ключом */ } ENTRY;Листинг 9.21. Описание типа ENTRY.
Функция hcreate() резервирует достаточное количество памяти для таблицы и должна вызываться перед обращением к hsearch(). Значением аргумента nel является ожидаемое максимальное количество элементов в таблице. Это число можно взять с запасом, чтобы уменьшить среднее время поиска.
Нормальный для hcreate() результат отличен от нуля.
Функция hdestroy() ликвидирует таблицу поиска. За вызовом этой функции может следовать новое обращение к функции создания таблицы hcreate().
Функция hsearch() возвращает указатель внутрь таблицы на искомые данные. Аргумент item – это структура типа ENTRY, содержащая два указателя: item.key указывает на сравниваемый ключ ( функцией сравнения при поиске в хэш-таблице служит strcmp() ), а item.data – на любые дополнительные данные, ассоциированные с этим ключом.
Аргумент action имеет тип ACTION, определенный так, как показано на листинге 9.22. Он задает способ действий в случае неудачного поиска: значение ENTER предписывает производить поиск с вставкой, то есть в случае неудачи искомый элемент следует поместить в таблицу; значение FIND предписывает в случае неудачи вернуть пустой указатель NULL. Пустой указатель возвращается и тогда, когда значение аргумента action равно ENTER, и таблица заполнена.
enum { FIND, ENTER } ACTION;Листинг 9.22. Определение типа ACTION.
В качестве примера применения функций, управляющих хэш-таблицами поиска, рассмотрим программу, которая помещает в хэш-таблицу заданное число элементов с указателями на случайные цепочки символов, а затем выполняет в этой таблице поиск новых случайных цепочек, пока он не окажется успешным (см. листинг 9.23).
/* * * * * * * * * * * * * * * * * * * * */ /* Программа помещает в хэш-таблицу */ /* заданное число элементов с указателями*/ /* на случайные цепочки символов, */ /* а затем выполняет в этой таблице */ /* поиск новых случайных цепочек, */ /* пока он не окажется успешным */ /* * * * * * * * * * * * * * * * * * * * */ #include <search.h> #include <stdlib.h> #include <stdio.h> /* Размер области для хранения цепочек символов */ #define SPACE_SIZE 10000000 /* Число элементов, помещаемых в хэш-таблицу */ #define TAB_NEL 1000000 /* Размер хэш-таблицы */ #define TAB_SIZE (2 * TAB_NEL) /* Длина одной цепочки символов */ /* (включая завершающий нулевой байт) */ #define STRING_SIZE 10 /* Область для хранения цепочек символов */ static char StringSpace [SPACE_SIZE]; /* * * * * * * * * * * * * * * * * * * * * */ /* Формирование случайной цепочки символов */ /* * * * * * * * * * * * * * * * * * * * * */ 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 []) { ENTRY item; /* Искомый элемент */ char sbuf [STRING_SIZE]; /* Буфер для формирования */ /* случайных цепочек */ double ntr; /* Номер найденной */ /* случайной цепочки */ size_t i; if (hcreate (TAB_SIZE) == 0) { fprintf (stderr, "%s: Не удалось создать хэш-таблицу" " размера %d\n", argv [0], TAB_SIZE); return (1); } item.data = NULL; /* Нет ассоциированных данных */ /* Заполним таблицу */ for (item.key = StringSpace, i = 0; i < TAB_NEL; item.key += STRING_SIZE, i++) { if (((item.key + STRING_SIZE) – (StringSpace + SPACE_SIZE)) > 0) { fprintf (stderr, "%s: Исчерпано пространство " "цепочек\n", argv [0]); return (2); } str_rnd (item.key, STRING_SIZE); if (hsearch (item, ENTER) == NULL) { fprintf (stderr, "%s: Переполнена хэш-таблица\n", argv [0]); return (3); } } /* for */ /* Будем формировать и искать новые случайные цепочки */ item.key = sbuf; ntr = 0; do { str_rnd (item.key, STRING_SIZE); ntr++; } while (hsearch (item, FIND) == NULL); printf ("Удалось найти %g-ю по счету случайную цепочку %s\n", ntr, item.key); hdestroy (); return 0; }Листинг 9.23. Пример применения функций, управляющих хэш-таблицами поиска.
Обратим внимание на то, что размер хэш-таблицы выбран вдвое большим по сравнению с реально используемым числом элементов; это уменьшает число коллизий (случаев совпадения хэш-кодов разных ключей ) и ускоряет их разрешение. При небольшом числе коллизий время поиска одного элемента в хэш-таблице ограничено константой (не зависит от количества элементов в таблице). Это значит, что время работы приведенной программы должно быть пропорционально размеру таблицы, то есть по порядку величины оно меньше, чем для рассмотренной выше комбинации быстрой сортировки и бинарного поиска (убирается множитель, равный логарифму числа элементов). Сделанный вывод подтверждается результатами измерения времени работы программы (см. листинг 9.24).
Удалось найти 168221-ю по счету случайную цепочку VBBDZTNMZ real 9.61 user 9.36 sys 0.25Листинг 9.24. Возможные результаты выполнения программы, применяющей функции управления хэш-таблицами поиска.
Читателю предлагается измерить время работы этой программы на своем компьютере, сравнить его с аналогичным временем для быстрой сортировки и бинарного поиска, а также оценить зависимость среднего времени поиска от размера таблицы (и подтвердить теоретические оценки из [ 4 ] ).
Бинарные деревья поиска – замечательное средство, позволяющее эффективно (за время, логарифмически зависящее от числа элементов) осуществлять операций поиска с вставкой (функция tsearch() ) и без таковой ( tfind() ), удаления ( tdelete() ) и, кроме того, выполнять обход всех элементов (функция twalk() ) (см. листинг 9.25). Функции реализуют алгоритмы T и D, описанные в пункте 6.2.2 книги Д. Кнута [ 4 ] .
#include <search.h> void *tsearch (const void *key, void **rootp, int (*compar) (const void *, const void *)); void *tfind (const void *key, void *const *rootp, int (*compar) (const void *, const void *)); void *tdelete (const void *restrict key, void **restrict rootp, int (*compar) (const void *, const void *)); void twalk (const void *root, void (*action) (const void *, VISIT, int));Листинг 9.25. Описание функций управления бинарными деревьями поиска.
Функция tsearch() используется для построения дерева и доступа к нему. Аргумент key является указателем на искомые данные ( ключ ). Если в дереве есть узел, первым полем которого является ссылка на данные, равные искомым, то результатом функции служит указатель на этот узел. В противном случае в дерево вставляется вновь созданный узел со ссылкой на искомые данные и возвращается указатель на него. Отметим, что копируются только указатели, поэтому прикладная программа сама должна позаботиться о хранении данных.
Аргумент rootp указывает на переменную, которая является указателем на корень дерева. Ее значение, равное NULL, специфицирует пустое дерево ; в этом случае в результате выполнения функции tsearch() переменная устанавливается равной указателю на единственный узел – корень вновь созданного дерева.
Подобно функции tsearch(), функция tfind() осуществляет поиск по ключу, возвращая в случае успеха указатель на соответствующий узел. Однако в случае неудачного поиска функция tfind() возвращает пустой указатель NULL.
Функция tdelete(), как и tfind(), сначала производит поиск, но не останавливается на этом, а удаляет найденный узел из бинарного дерева. Результатом tdelete() служит указатель на вышележащий по сравнению с удаляемым узел или NULL, если поиск оказался неудачным.
Функция twalk() осуществляет обход бинарного дерева в глубину, слева направо ( дерево строится функцией tsearch() так, что, в соответствии с функцией сравнения compar(), все узлы левого поддерева предшествуют его корню, который, в свою очередь, предшествует узлам правого поддерева ). Аргумент root указывает на корень обрабатываемого (под)дерева (любой узел может быть использован в качестве корня для обхода соответствующего поддерева ).
Очевидно, в процессе обхода все неконцевые узлы посещаются трижды (при спуске в левое поддерево, при переходе из левого поддерева в правое и при возвращении из правого поддерева ), а концевые ( листья ) – один раз. Эти посещения обозначаются величинами типа VISIT с исключительно неудачными именами (см. листинг 9.26).
enum { preorder, postorder, endorder, leaf } VISIT;Листинг 9.26. Определение типа VISIT.
Имена неудачны, потому что они совпадают с названиями разных способов обхода деревьев (см., например, [ 3 ] в дополнительной литературе, пункт 2.3.1). В частности, порядок обхода, реализуемый функцией twalk(), называется в [ 3 ] прямым (по-английски – preorder ). Остается надеяться, что читатель не даст себя запутать и уверенно скажет, что в данном контексте postorder – это второе посещение неконцевого узла бинарного дерева поиска, а не какой-то там обратный порядок обхода.