Технологические интерфейсы
Управление хэш-таблицами поиска производится в соответствии с алгоритмом, описанным Д. Кнутом (см. [ 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 – это второе посещение неконцевого узла бинарного дерева поиска, а не какой-то там обратный порядок обхода.