Опубликован: 06.12.2004 | Доступ: свободный | Студентов: 1179 / 142 | Оценка: 4.76 / 4.29 | Длительность: 20:58:00
ISBN: 978-5-9556-0021-5
Лекция 9:

Технологические интерфейсы

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