【问题标题】:Creating an empty hash table创建一个空的哈希表
【发布时间】:2018-12-10 11:22:52
【问题描述】:

我正在尝试创建一个空的结构哈希表:

struct htab
{
   size_t capacity;
   size_t size;
   struct pair *data;
};

数据是结构对值的链表数组。这些链表包含标记(虚拟值)作为第一个元素。

struct pair
{
   uint32_t hkey;
   char *key;
   void *value;
   struct pair *next;
};

所以我写了这个容量为 4,大小为 0。如何将“数据”数组的所有单元格初始化为 0?

struct htab *htab_new()
{
   struct htab *newtable = 
   malloc(sizeof(struct htab));
   if (newtable == NULL)
   {
       errx(1, "Not enough memory!");
   }
   newtable->capacity = 4;
   newtable->size = 0;
   newtable->data = calloc(// ??);
   return newtable;
}

另外,我如何测试这是否真的有效?

【问题讨论】:

  • 容量是哈希表中的桶数吗?否则就没有多大意义了。
  • 我还不明白你的哈希表实现的概念。你的意思是你的容量为 4 的哈希表最多可以有 4 个不同的哈希值,并且每个哈希值可以有多个键/值对作为链表连接到它?在这种情况下,您不需要在每个列表元素中都使用 hkey 值。
  • 一个元素数组。在这种特定情况下,它是一个“结构对”值的数组。实际上,这个数组中的每个单元格都将包含一个“结构对”值的链表。例如,如果数组的容量是四,它将包含四个链表。这些列表将使用哨兵。因此,当数组为空(大小 = 0)时,它只包含哨兵。将使用链表来处理键之间的冲突
  • // 小心,你必须分配两个内存空间。 // - 保存“struct htab”变量的内存空间。 // - 保存数据的内存空间。 // 'data' 数组的所有单元格必须初始化为零 // (它们包含链表的标记。)
  • 那么capacity不是表中可能元素的数量,而是buckets的数量(请学习术语)?然后只需分配一个 capacity 结构的“数组”,然后相应地对其进行初始化(所有这些都是你的哨兵)。如果您想更改存储桶的数量(即更改capacity),您需要重新散列表中已有的所有元素。

标签: c hash linked-list


【解决方案1】:

这些链表包含标记(虚拟值)作为第一个元素。

newtable->data = calloc(capacity, sizeof(*newtable->data));
// but you should check the result of calloc as well, just as you did for malloc!

你会发现 key 实际使用的插槽已经分配了一个值吗?然后,您将无法使用空指针作为键。如果您只使用next 指针,那么为什么要使用结构呢?你可以有一个指针数组,然后哨兵将是一个空指针:

struct pair** data;

有趣的是,有了这个,您不需要像我上面介绍的那样更改对 calloc 的调用(sizeof(*data) 现在将是指针的大小......)。

旁注:请参阅calloc and pointers...

【讨论】:

  • 对不起,你改变了结构,我不想要这个:(
  • @displayname 改变结构只是一个替代命题,你不需要。如前所述,对 calloc 的调用即使结构不变(在这种情况下,所有指针 + hkey 成员都设置为 0)也能正常工作。
  • 你能告诉我如何测试这个功能吗?我完全不知道
  • @displayname:请重新考虑。如果你使用struct htab { size_t size; struct pair **slot; };,实现会更简单、更高效。有关我的示例,请参阅here。为了进行测试,我将创建一个小型测试程序,该程序使用命令行参数或标准输入将对添加到哈希表中,然后将内容打印出来,例如用于视觉检查的 Graphviz DOT 格式。
  • @displayname 对于测试,最简单的方法是:使用调试器并检查数组元素。或者打印出每个元素及其成员到控制台(你必须相信calloc 已经分配了适当数量的元素......)。
【解决方案2】:

在评论中,OP 提到他们从示例中学习得更好。 (这本身不是答案,只是一个示例。请将此视为扩展评论,而不是答案。)

那么,让我们看一个简单的真实示例;但不能只是复制粘贴并作为自己的作品呈现。

假设我们需要一个文本或标记的哈希表,比如说

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

struct hashentry {
    struct hashentry   *next;
    size_t              hash;
    unsigned char       data[];
};

struct hashtable {
    size_t              size;
    struct hashentry  **slot;
};

其中表本身是一个指针数组,哈希冲突通过链接解决。请注意,我基本上只使用键而不是键值对;这是为了避免将此示例代码复制粘贴并作为某人自己的工作呈现。我写这篇文章是为了帮助新程序员理解,而不是让作弊者提交作为他们的作业。 (请注意,我不是指 OP。这些问题通常是通过网络搜索找到的,我为整个群体编写这些答案,而不仅仅是提问者。)

表初始化到特定大小:

static inline void hashtable_init(struct hashtable *const ht, const size_t size)
{
    size_t  i;

    if (!ht) {
        fprintf(stderr, "hashtable_init(): No hashtable specified (ht == NULL).\n");
        exit(EXIT_FAILURE);
    } else
    if (size < 1) {
        fprintf(stderr, "hashtable_init(): Invalid hashtable size (size == %zu).\n", size);
        exit(EXIT_FAILURE);
    }

    /* Allocate an array of pointers. */
    ht->slot = calloc(size, sizeof ht->slot[0]);
    if (!ht->slot) {
        fprintf(stderr, "hashtable_init(): Failed to allocate an array of %zu slots.\n", size);
        exit(EXIT_FAILURE);
    }
    ht->size = size;

    /* Mark each slot unused. (On all current architectures, this is unnecessary,
       as calloc() does initialize the pointers to NULL, but let's do it anyway.) */
    for (i = 0; i < size; i++)
        ht->slot[i] = NULL;
}

对于散列函数,我喜欢文本字符串的 DJB2 Xor 变体。不是特别好(会有碰撞),但是非常简单快速:

static inline size_t  hash_data(const char *data, const size_t datalen)
{
    const char *const ends = data + datalen;
    size_t            hash = 5381;

    while (data < ends)
        hash = (33 * hash) ^ (unsigned char)(*(data++));

    return hash;
}

请注意,我使用size_t 作为散列的类型。你可以使用任何你想要的类型,但在大多数架构上,它的大小与指针相同,即 .

向哈希表添加数据条目:

static inline void hashtable_add(struct hashtable *ht, const char *data, const size_t datalen)
{
    struct hashentry *entry;
    size_t            hash, slotnum;

    if (!ht) {
        fprintf(stderr, "hashtable_add(): No hashtable specified (ht == NULL).\n");
        exit(EXIT_FAILURE);
    } else
    if (ht->size < 1) {
        fprintf(stderr, "hashtable_add(): Hashtable has zero size.\n");
        exit(EXIT_FAILURE);
    } else
    if (!data && datalen > 0) {
        fprintf(stderr, "hashtable_add(): data is NULL, but datalen == %zu.\n", datalen);
        exit(EXIT_FAILURE);
    }

    /* Allocate memory for the entry, including the data, and the string-terminating nul '\0'. */
    entry = malloc(sizeof (struct hashentry) + datalen + 1);
    if (!entry) {
        fprintf(stderr, "hashtable_add(): Out of memory (datalen = %zu).\n", datalen);        
        exit(EXIT_FAILURE);
    }

    /* Copy the data, if any. */
    if (datalen > 0)
        memcpy(entry->data, data, datalen);

    /* Ensure the data is terminated with a nul, '\0'. */
    entry->data[datalen] = '\0';

    /* Compute the hash. */
    hash = hash_data(data, datalen);
    entry->hash = hash;

    /* The slot number is the hash modulo hash table size. */
    slotnum = hash % ht->size;

    /* Prepend entry to the corresponding slot chain. */
    entry->next = ht->slot[slotnum];
    ht->slot[slotnum] = entry;
}

当我最初编写上述代码时,我总是将其编写为测试程序,并对其进行测试。 (这在技术上属于unit testing 范式。)

在这种情况下,我们可以简单地将槽数作为命令行参数,从标准输入中读取每一行作为要添加到哈希表的数据。

因为标准 C 没有实现 getline(),我们最好使用 fgets() 来代替,并使用固定大小的行缓冲区。如果我们声明

#ifndef  MAX_LINE_LEN
#define  MAX_LINE_LEN  16383
#endif

我们有一个预处理器宏MAX_LINE_LEN,默认为 16383,但可以在编译时使用编译器选项覆盖。 (对于 GCC、Intel CC 和 clang,您可以使用例如 -DMAX_LINE_LEN=8191 将其减半。)

main()中,如果参数计数不正确,或者-h--help是第一个参数,我喜欢打印用法:

int main(int argc, char *argv[])
{
    char              buffer[MAX_LINE_LEN + 1];
    char             *line;
    size_t            size, i;
    struct hashtable  table;
    char              dummy;

    if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s ENTRIES < DATA-FILE > DOT-FILE\n", argv[0]);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program reads lines from DATA-FILE, adding them to\n");
        fprintf(stderr, "a hash table with ENTRIES slots and hash chaining.\n");
        fprintf(stderr, "When all input lines have been read, the contents of the\n");
        fprintf(stderr, "hash table slots will be output as a Graphviz DOT format graph.\n");
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

接下来,我们可以尝试将第一个命令行参数解析为size_t size;。我喜欢使用“哨兵”字符来检测参数值后面是否有垃圾(除了空格):

    if (sscanf(argv[1], "%zu %c", &size, &dummy) != 1 || size < 1) {
        fprintf(stderr, "%s: Invalid number of hash table entries.\n", argv[1]);
        return EXIT_FAILURE;
    }
    hashtable_init(&table, size);

下一部分是从标准输入中读取每一行,并将它们添加到哈希表中。

    while (1) {

        line = fgets(buffer, sizeof buffer, stdin);
        if (!line)
            break;

        /* Skip leading ASCII whitespace. */
        line += strspn(line, "\t\n\v\f\r ");

        /* Find out the remaining length of the line. */
        size = strlen(line);

        /* Ignore trailing ASCII whitespace. */
        while (size > 0 && (line[size-1] == '\t' || line[size-1] == '\n' ||
                            line[size-1] == '\v' || line[size-1] == '\f' ||
                            line[size-1] == '\r' || line[size-1] == ' '))
            size--;

        /* Ignore empty lines. */
        if (size < 1)
            continue;

        /* Add line to the hash table. */
        hashtable_add(&table, line, size);
    }

    /* Check if fgets() failed due to an error, and not EOF. */
    if (ferror(stdin) || !feof(stdin)) {
        fprintf(stderr, "Error reading from standard input.\n");
        return EXIT_FAILURE;
    }

此时,我们有 tablesize 插槽。我编写测试程序来编写纯文本输出(如果简单)或 Graphviz DOT 格式输出(如果结构像图形)。在这种情况下,图形输出格式听起来更好。

    /* Print the hash table contents as a directed graph, with slots as boxes. */
    printf("digraph {\n");

    for (i = 0; i < table.size; i++) {
        struct hashentry *next = table.slot[i];

        /* The slot box. */
        printf("    \"%zu\" [ shape=\"box\", label=\"%zu\" ];\n", i, i);

        if (next) {

            /* The edge from the slot box to the entry oval. */
            printf("    \"%zu\" -> \"%p\";\n", i, (void *)next);

            while (next) {
                struct hashentry *curr = next;

                /* Each entry oval; text shown is the value read from the file. */
                printf("    \"%p\" [ shape=\"oval\", label=\"%s\" ];\n", (void *)curr, curr->data);

                next = next->next;

                /* The edge to the next oval, if any. */
                if (next)
                    printf("    \"%p\" -> \"%p\";\n", (void *)curr, (void *)next);
            }
        } 
    }

    printf("}\n");
    return EXIT_SUCCESS;
}

就是这样。如果以10作为命令行参数编译运行上面的程序,然后喂

one
two
three
four
five
six
seven
eight
nine
ten

到它的标准输入,它会输出

digraph {
    "0" [ shape="box", label="0" ];
    "1" [ shape="box", label="1" ];
    "1" -> "0xb460c0";
    "0xb460c0" [ shape="oval", label="three" ];
    "0xb460c0" -> "0xb46080";
    "0xb46080" [ shape="oval", label="one" ];
    "2" [ shape="box", label="2" ];
    "3" [ shape="box", label="3" ];
    "3" -> "0xb46180";
    "0xb46180" [ shape="oval", label="nine" ];
    "0xb46180" -> "0xb460a0";
    "0xb460a0" [ shape="oval", label="two" ];
    "4" [ shape="box", label="4" ];
    "4" -> "0xb46160";
    "0xb46160" [ shape="oval", label="eight" ];
    "0xb46160" -> "0xb46140";
    "0xb46140" [ shape="oval", label="seven" ];
    "5" [ shape="box", label="5" ];
    "5" -> "0xb46100";
    "0xb46100" [ shape="oval", label="five" ];
    "6" [ shape="box", label="6" ];
    "6" -> "0xb461a0";
    "0xb461a0" [ shape="oval", label="ten" ];
    "7" [ shape="box", label="7" ];
    "7" -> "0xb46120";
    "0xb46120" [ shape="oval", label="six" ];
    "0xb46120" -> "0xb460e0";
    "0xb460e0" [ shape="oval", label="four" ];
    "8" [ shape="box", label="8" ];
    "9" [ shape="box", label="9" ];
}

提供给 Graphviz dot 将生成一个漂亮的图表:

如果要查看数据字符串上方的实际哈希值,请更改为

                /* Each entry oval; text shown is the value read from the file. */
                printf("    \"%p\" [ shape=oval, label=\"%zu:\\n%s\" ];\n", (void *)curr, curr->hash, curr->data);

正如我所说,DJB2 Xor 哈希不是特别好,对于上述输入,您至少需要 43 个哈希表槽以避免链接。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-02-19
    • 2013-12-20
    • 2011-05-06
    • 2012-05-30
    • 2011-01-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多