UP | HOME

散列表之链接法

目录

许多应用都需要一种数据结构,至少支持 INSERT、SEARCH 和 DELETE 字典操作。例如,用于程序语言编译的编译器维护了一个符号表,其中元素的关键字为任意字符串,它与程序中的标识符相对应。散列表(hash table)是实现字典操作的一种有效数据结构。尽管最坏情况下,散列表中欧给你查找一个元素的时候与链表中查找的时间相同,达到了Θ(n)。然后在实际应用中,散列查找的性能是非常好的。

直接寻址表

当关键字的全域 U 比较小时,直接寻址是一种简单而有效的技术。一般可以采用数组实现直接寻址表,数组下标对应的就是关键字的值,即具有关键字 k 的元素被放在直接寻址表的槽 k 中。直接寻址表的字典操作实现比较简单,直接操作数组即可,只需 O(1)的时间。

散列表

直接寻址技术的缺点也非常明显:如果全域 U 很大,则在一台标准的计算机可用内存容量中,要存储大小为|U|的一张表 T 也许不实际。还有,实际存储的关键字集合 K 相对 U 来说可能很小,使得分配给 T 的大部分空间都浪费掉。 所以我们需要将散列表的存储需求降至Θ(|k|),同时散列表中查找一个元素的优势仍得到保持(O(1)时间)。所以在散列方式下,利用散列函数(hash function)h,由关键字 k 计算出槽的位置 h(k)。这里函数 h 将关键字的全域 U 映射到散列表 T[0..m-1]的槽位上。

hash1.png

图1  hash1

这里存在一个问题:两个关键字可能映射到同一个槽中。我们称这种情形为冲突(collision)。幸运的是,我们能找到有效的方法来解决冲突。

有两种种解决冲突的方法,一种是链接法(chaining),另一种是开放寻址法(open addressing)。本文先介绍其中的一种——链接法。

链接法

在链接法中,把散列到同一槽中的所有元素都放在一个链表中。槽中有一个指针,它指向存储所有散列到该槽的元素的链表的表头,如果不存在这样的元素,则该槽中为 NIL。其插入操作、删除操作的最坏情况运行时间为 O(1),而搜索操作的最坏情况运行时间为 O(n),如果所有的元素都散列到同一槽中。

hash2.png

图2  hash2

在实现链接法散列表前先来看看散列函数。

散列函数

一个好的散列函数应近似的满足简单均匀假设:每一个关键字都被等可能的散列到 m 个槽位中的任何一个,并与其他关键字已散列到哪个槽位无关。另外如果关键字的全域不是自然数集,我们需要找到一种方法将它转换为自然数。散列函数可以说是散列表性能最关键的一部分。

(1)除法散列法

通过取 k 除以 m 的余数,将关键字 k 映射到 m 个槽中的某一个上:

h(k) = k mod m

当应用除法散列法时,要避免选择 m 的某些值。如 m 不应为 2 的幂,如果 m=2^{p},则 h(k)就是 k 的 p 个最低位数字。

(2)乘法散列法

第一步:用关键字 k 乘上常熟 A(0 < A <1),并提取 kA 的小数部分。第二步:用 m 乘以这个值,再向下取整。总之,散列函数为:h(k) = m(kA mod 1)。

乘法散列法也是某种随机的一个实现,乘法与除法散列法要好的多。

(3)全域散列法

给定一组散列函数 H,每次进行散列时候从 H 中随机的选择一个散列函数 h,使得 h 独立于要存储的关键字。全域散列函数类的平均性能是比较好的。  在网上搜索过不少散列表的实现,不知道为什么都是比较复杂,在这里给出一种比较简单的使用链表数组的实现。

链接法散列表的 C 语言实现:

    #include <stdlib.h>
    #include <stdio.h>

    #define m           16
    //#define HASH(k)     k % m         //除法散列法

    #define A           0.65
    #define HASH(k)     (int)(m * (k * A - (int)(k * A)))         //乘法散列法

    struct elemt{
        struct elemt    *prev;
        struct elemt    *next;
        int     key;
    };

    typedef struct elemt    ELEM;

    typedef struct {
        ELEM    *head;
    }LIST;

    void
    list_init(LIST *L)
    {
        L->head = (ELEM *)malloc(sizeof(ELEM));
        L->head->next = L->head;
        L->head->prev = L->head;
        L->head->key = NULL;
    }

    void
    list_insert(LIST *L, int key)
    {
        /*
         * 给定一个关键字key,将key插入到链表的最前端。
         */
        ELEM    *x;

        x = (ELEM *)malloc(sizeof(ELEM));
        x->key = key;
        if (L->head->next == L->head && L->head->prev == L->head)
            x->next = NULL;
        else
            x->next = L->head;
        if (L->head->next != L->head) {
            L->head->prev = x;      //L->head->prev表示的是L->head所指向的对象的prev属性
        }
        L->head = x;
        x->prev = NULL;
    }

    void
    list_delete(LIST *L, ELEM *x)
    {
        /*
         * 给定需要删除的元素x,通过修改指针将x从链表中删除。
         */
        if (x->prev != NULL) {
            x->prev->next = x->next;
        } else {
            L->head = x->next;
        }

        if (x->next != NULL) {
            x->next->prev = x->prev;
        }

        free(x);
    }

    ELEM *
    list_search(LIST *L, int k)
    {
        /*
         * 给定关键字k,查找链表中第一个关键字为k的元素,并返回指向该元素的指针。
         */
        ELEM    *x;

        x = L->head;
        while (x != NULL && x->next != x && x->key != k) {
            x = x->next;
        }

        if (x == NULL || x->key != k)
            x = NULL;

        return(x);
    }

    void
    hash_insert(LIST *table, int x)
    {
        /*
         * 将元素x散列后,调用链表的插入以将x到散列表中
         */
        list_insert(&table[HASH(x)], x);
    }

    void
    hash_delete(LIST *table, int x)
    {
        /*
         * 调用链表搜索找到元素x的,然后再调用链表删除
         * 将其从散列表中删除
         */
        ELEM    *k;

        k = list_search(&table[HASH(x)], x);
        list_delete(&table[HASH(x)], k);
    }

    void
    hash_search(LIST *table, int x)
    {
        /*
         * 为了方便测试,将散列表的搜索做了一点修改,
         * 直接在stdout中输出是否找到了该元素。
         */
        ELEM    *k;
        k = list_search(&table[HASH(x)], x);
        /*
        if (k != NULL)
            return(k->key);
        else
            return(NULL);
        */

        if (k != NULL)
            printf("found %d, key = %d\n", x, HASH(x));
        else
            fprintf(stderr, "can't found %d\n", x);
    }

    void
    print_hash(LIST *table)
    {
        /*
         * 打印散列表
         */
        int     i;
        ELEM    *j;

        printf("--------------HASHTABLE--------------\n");
        for (i = 0; i < m; i++) {
            printf("key = %2d: ", i);
            j = table[i].head;
            if (j != NULL && j->next != table[i].head)
                for ( ; j != NULL; j = j->next) {
                    printf("value = %4d  ", j->key);
                }
            else
                printf("value = NULL  ", i);

            printf("\n");
        }
        printf("-----------------END-----------------\n");
    }

    int
    main(void)
    {
        /*
         * 简单的使用链表数组实现散列表
         */
        int     i, key;
        LIST    table[m];

        for (i = 0; i < m; i++) {
            list_init(&table[i]);
        }

        hash_insert(table, 38);
        hash_insert(table, 123);
        hash_insert(table, 94);
        hash_insert(table, 29);
        hash_insert(table, 48);
        hash_insert(table, 38);
        hash_insert(table, 923);
        hash_insert(table, 31);
        hash_insert(table, 32);
        hash_insert(table, 39);
        hash_insert(table, 2);
        hash_insert(table, 24);
        hash_insert(table, 823);

        print_hash(table);

        hash_delete(table, 38);
        hash_delete(table, 48);
        hash_delete(table, 31);

        printf("\nafter delete 38, 48, 31:\n\n");

        print_hash(table);

        printf("\nsearch 923: \n\n");
        hash_search(table, 923);

        printf("\nsearch 239: \n\n");
        hash_search(table, 239);

        exit(0);
    }

作者: Petrus.Z

Created: 2021-09-29 Wed 16:38