【问题标题】:C Language - Rehashing a Separate Chaining Hash TableC 语言 - 重新散列一个单独的链式哈希表
【发布时间】:2021-03-11 22:29:28
【问题描述】:

所以我到处寻找灵感,但我并没有真正找到使用单独链接方法重新散列哈希表的任何东西。所以我自己尝试了一下,我想我知道我做错了什么,但我不知道如何实现它,请帮助。

一切正常,除了新添加的函数 rehash()

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

struct list_node
{

   struct list_node *next;
   char *key;
   char *value; 

};

struct hash_table 
{

    int table_size;
    struct list_node **list_arr;

};  




unsigned int hash(const char *key, unsigned int table_size);
struct hash_table *initialize(unsigned int table_size);
struct list_node *find(struct hash_table *H, const char *key);
void insert(struct hash_table *H, const char *key, const char *value);
void dump(struct hash_table *H);
void del(struct hash_table *H, const char *key);
struct hash_table *rehash(struct hash_table *H);

unsigned int 
hash(const char *key, unsigned int table_size)
{
    unsigned long int hashx = 0;
    for(int i=0;key[i];i++)
    {
        hashx = (hashx<<5) + key[i];
    }

    return (hashx%table_size);
}


struct hash_table
*initialize(unsigned int table_size)
{

    struct hash_table *H = malloc(sizeof(*H));
    H->list_arr = malloc(sizeof(*H->list_arr)*table_size);

    H->table_size = table_size;

    for(unsigned int i = 0; i<table_size; i++)
    {
        H->list_arr[i] = malloc(sizeof(*H->list_arr[i]));
        H->list_arr[i]->next = NULL;
    }

    return H;

}


void
insert(struct hash_table *H, const char *key, const char *value)
{   
    unsigned int index = hash(key, H->table_size);
    struct list_node *head = H->list_arr[index];
    struct list_node *current = head->next;
    
    
    while(current!=NULL)
    {
        if(strcmp(current->key,key)==0)
        {
            free(current->value);
            current->value = malloc(strlen(value)+1);
            strcpy(current->value,value);
            return;
        }

        current=current->next;

    }

    struct list_node *newNode = malloc(sizeof(*H->list_arr[index]));
    newNode->next = head->next;
    head->next = newNode;
    newNode->key = malloc(strlen(key)+1);
    newNode->value = malloc(strlen(value)+1);
    strcpy(newNode->key,key);
    strcpy(newNode->value,value);


}

void
dump(struct hash_table *H)
{
    for( int i = 0; i<H->table_size; i++)
    {
        struct list_node *entry = H->list_arr[i]->next;
        if(entry==NULL){continue;}
        printf("Index[%d]: ", i);
        while(entry!=NULL)
        {
            printf("\t%s|%s\t--> ", entry->key, entry->value);
            entry = entry->next;
        }
        printf("\tNULL");
        printf("\n");

    }
}

void delete(struct hash_table *H, const char *key)
{
    unsigned int index = hash(key,H->table_size);
    struct list_node *prev = H->list_arr[index];

    while(strcmp(prev->next->key,key)!=0)
    {
        if(prev->next==NULL){printf("Key not found!");return;}
        prev=prev->next;
    }

    struct list_node *temp = prev->next;
    prev->next = temp->next;
    free(temp);

}


struct hash_table *rehash(struct hash_table *H)
{
    unsigned int old_size = H->table_size;
    struct list_node *old_entries = H->list_arr;

    H = initialize(2*old_size);

    for(unsigned int i = 0; i<old_size; i++)
    {
        while(old_entries[i]!=NULL)
        {
            insert(H,old_entries[i].key,old_entries[i].value);
            old_entries[i] = old_entries[i]->next;
        }
    }

    free(old_entries);

    return H;
}



int main()
{   

    struct hash_table *H = initialize(20);
    insert(H,"name1","David");
    insert(H,"name2","Radka");
    dump(H);    
    H = rehash(H);
    dump(H);
    return 1;

}

我认为做 old_entries[i] 是错误的,但没有想到别的,请帮我解决这个问题。

【问题讨论】:

  • 这在多个层面上都是错误的。是的,你的 old_entries 逻辑被破坏了,如果没有其他原因,只是因为类型显然不匹配。但真正的问题是列表项管理。您永远不需要在重新散列中触发插入。所有的桶扩展都是关于保存旧表链,开始一个新表,从旧项目(仍然是有效节点)计算新哈希值,然后将这些节点移动到它们的新表中的新链。唯一的分配应该是链指针的新表床。
  • @WhozCraig 感谢您的回复。我应该如何将旧参数复制到新的“新表”而不插入?由于我是第三学期的 CS 本科生,请更“简单”地解释一下。
  • 使用insert() 进行重新散列本身并没有错,但是您将创建新节点,这是一种浪费。但是,您忘记释放旧节点,因此您确实应该有一个单独的函数来插入节点而不是值。
  • @ThomasMailund 好的,所以如果我将 free((*H)-&gt;list_arr[i]); 添加到 while 循环中应该没问题,对吧?
  • @DaveHlave 您不能只释放 bin 中的第一个元素。那里可能不止一个。您必须遍历 bin 中的所有节点并释放它们。

标签: c function pointers data-structures hashtable


【解决方案1】:

好的!想了一会儿,我意识到我创建了一个struct list_node 指针变量,它指向H-&gt;list_arr,它是一个指针数组。那是我的错误。我应该将其声明为double pointer

这是修改后的 rehash() 函数:

struct hash_table *rehash(struct hash_table *H)
{
    unsigned int old_size = H->table_size;
    struct list_node **old_entries = H->list_arr;

    H = initialize(2*old_size);

    for(unsigned int i = 0; i<old_size; i++)
    {
        old_entries[i] = old_entries[i]->next;
        while(old_entries[i]!=NULL)
        {
            insert(H,old_entries[i]->key,old_entries[i]->value);
            old_entries[i] = old_entries[i]->next;
        }
    }

    free(old_entries);

    return H;
}

使用此代码,您必须将新 hash_table 的地址返回到指向旧 hash_table 的指针 --> [H = rehash(H)] 因为将指针 H 作为参数传递只会改变它本地。因此,我尝试了第二个版本(因为我太懒了;)并且注意力不集中并且可能忘记重新分配它)我不必返回任何东西,我想简单地通过调用函数来更改它并且我的指针指向新的 hash_table 自动 -> [rehash(&amp;H)],这是另一个“懒惰”的选择:

void
rehash(struct hash_table **H)
{
    unsigned int old_size = (*H)->table_size;
    struct list_node **old_entries = (*H)->list_arr;

    *H = initialize(2*old_size);

    for(unsigned int i = 0; i<old_size; i++)
    {
        old_entries[i] = old_entries[i]->next;
        while(old_entries[i]!=NULL)
        {
            insert(*H,old_entries[i]->key,old_entries[i]->value);
            old_entries[i] = old_entries[i]->next;
        }
    }

    free(old_entries);

    
}

如果我正在做一些效率低下的事情(在空间和时间方面),请告诉我,因为我只是 CS 学士的第 3 学期,我们本学期才开始 DSA。

【讨论】:

  • 您可能会发现 this example 很有趣,改编自您的原始帖子。最重要的是,请注意我之前提到的 rehash 算法中旧节点的重用。
【解决方案2】:

在每个 bin 的开头放置虚拟元素是个好主意,但您不需要使用 malloc() 分配此类虚拟元素。您可以只使 bin 数组成为节点数组,而不是指向节点的指针。然后,您在分配数组时分配了虚拟对象。所以你可以将你的哈希表定义为

struct hash_table
{
    int table_size;
    struct list_node *list_arr;
};

(而不是使用struct list_node **list_arr)。

在初始化过程中循环遍历 bin 时,必须将 bin 的 next 指针设置为 NULL,但不要分配它们。

struct hash_table
*initialize(unsigned int table_size)
{
    struct hash_table *H = malloc(sizeof(*H));
    H->list_arr = malloc(sizeof(*H->list_arr)*table_size);
    H->table_size = table_size;

    for(unsigned int i = 0; i<table_size; i++)
    {
        // no malloc here!
        H->list_arr[i].next = NULL;
    }

    return H;
}

无论如何,这与重新散列无关,只是一个建议。但是因为你有虚拟元素作为垃圾箱,你可以重构你的代码(这就是我认为虚拟元素是个好主意的原因)。您可以从桌子上取出垃圾箱并从那里工作,之后无需担心桌子本身。您可以使用

获取密钥的相关bin
struct list_node *get_bin(struct hash_table *H, const char *key)
{
    unsigned int index = hash(key, H->table_size);
    return &H->list_arr[index];
}

你可以在 bin 中找到节点

struct list_node *find_node(struct list_node *bin, const char *key)
{
    for (struct list_node *current = bin->next;
         current;
         current = current->next) {
        if(strcmp(current->key,key)==0) return current;
    }
    return 0;
}

例如,将插入简化为

void prepend_node(struct list_node *node, struct list_node *bin)
{
  node->next = bin->next;
  bin->next = node;
}

void insert(struct hash_table *H, const char *key, const char *value)
{
  struct list_node *bin = get_bin(H, key);
  struct list_node *node = find_node(bin, key);
  if (node) {
    // update node
    free(node->value);
    node->value = malloc(strlen(value)+1);
    strcpy(node->value,value);
  } else {
    // prepend new node
    prepend_node(new_node(key, value), bin);
  }
}

new_node() 函数的样子

struct list_node *new_node(const char *key, const char *value)
{
  struct list_node *node = malloc(sizeof *node);
  if (!node) abort(); // add some error handling here
  node->key = malloc(strlen(key)+1);
  if (!node->key) abort(); // add some error handling here
  strcpy(node->key,key);
  node->value = malloc(strlen(value)+1);
  if (!node->value) abort(); // add some error handling here
  strcpy(node->value,value);
  return node;
}

由于 bin 嵌入在数组中,您可以放心地在所有函数中假设它们不是 NULL,这样可以避免测试一些特殊情况。

这不是更短的代码,因为我把它分成了几个函数,但在我看来,当每个函数都做一件简单的事情时,它更具可读性。在这里,获取 bin,在 bin 中查找密钥,创建节点,假装 bin 等。使用“原始”malloc()strcpy() 等,分散在代码中,很难跟踪所有内容工作正常。代码的总行数增加了,但每个函数都更短更简单了。而且您可以摆脱它,因为您可以将 bin 作为列表处理,而无需访问哈希表数组,这正是因为所有 bin 都有一个虚拟头元素。

您现在可以重写 rehash() 以仅添加到垃圾箱。您知道旧垃圾箱中的所有密钥都是唯一的,因此您无需检查任何内容。您只需将每个节点放在其新 bin 的前面即可:

struct hash_table *rehash(struct hash_table *H)
{
    unsigned int old_size = H->table_size;
    struct list_node *old_entries = H->list_arr;
    free(H); // You forgot to free this one!
    H = initialize(2*old_size);

    for(unsigned int i = 0; i<old_size; i++)
    {
        struct list_node *old_bin = &old_entries[i];
        for (struct list_node *node = old_bin->next;
             node; node = node->next) {
          // just prepend to new bin; the key should be unique
          prepend_node(node, get_bin(H, node->key));
        }
    }
    free(old_entries);

    return H;
}

我添加了free(H),因为您忘记为H 释放内存,但是在不创建新表的情况下更新H 会更有效。您可以分开初始化和分配。但是你并没有获得太多的收益,因为初始化垃圾箱是耗时的部分。

不过,说到释放。记得编写一个函数来释放哈希表(记得释放 bin,包括所有节点)。当然,如果您在更新之前释放H,请不要将其与重新散列一起使用——您需要保留节点——但您确实需要这样的功能。

【讨论】:

  • 那是天才。谢谢!顺便说一句,我只取消引用变量作为 sizeof() 的参数,这要归功于我之前帖子中的提示,它更加安全。
猜你喜欢
  • 2018-05-30
  • 2012-12-25
  • 2022-01-10
  • 1970-01-01
  • 1970-01-01
  • 2016-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多