C语言实现哈希表

简介

哈希表是一种数据结构,它通过哈希函数将键映射到存储桶中,以实现高效的键值对存储和检索。

简单版本

简单版本的哈希表实现采用了最基本的哈希函数和直接寻址法。

  • 哈希函数:采用简单的求和取模法计算键的哈希值,将键映射到哈希表的位置。
  • 直接寻址法:哈希表的每个槽位直接存储一个键值对,而不是链表或其他数据结构。
  • 键值对结构体:使用结构体 KeyValuePair 来表示键值对,包括键和值两个成员。
  • 创建和销毁哈希表:提供了创建和销毁哈希表的函数,通过动态内存分配和释放来管理哈希表的内存。
  • 插入和查找键值对:提供了插入和查找键值对的函数。在插入时,根据计算出的哈希值将键值对直接存储到哈希表的对应位置;在查找时,根据键的哈希值找到对应位置,并进行比较以确定是否找到目标键。

代码

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

#define TABLE_SIZE 100

// 哈希表中存储的键值对结构体
typedef struct {
    char* key;
    int value;
} KeyValuePair;

// 哈希表结构体
typedef struct {
    KeyValuePair* table[TABLE_SIZE];
} HashTable;

// 计算哈希值
int hash(char* key) {
    int hash = 0;
    for (int i = 0; key[i] != '\0'; i++) {
        hash = (hash + key[i]) % TABLE_SIZE;
    }
    return hash;
}

// 创建哈希表
HashTable* createHashTable() {
    HashTable* ht = (HashTable*)malloc(sizeof(HashTable));
    for (int i = 0; i < TABLE_SIZE; i++) {
        ht->table[i] = NULL;
    }
    return ht;
}

// 插入键值对到哈希表
void insert(HashTable* ht, char* key, int value) {
    int index = hash(key);
    KeyValuePair* kvp = (KeyValuePair*)malloc(sizeof(KeyValuePair));
	//kvp->key = (char*)malloc(strlen(key) + 1);
	//strcpy(kvp->key, key); //另一种写法,但要先为kvp->key分配内存空间
    kvp->key = strdup(key); // 复制键的字符串(strdup() 函数可以自动分配内存并复制字符串)
    kvp->value = value;
    ht->table[index] = kvp;
}

// 查找键对应的值
int find(HashTable* ht, char* key) {
    int index = hash(key);
    if (ht->table[index] != NULL && strcmp(ht->table[index]->key, key) == 0) {
        return ht->table[index]->value;
    }
    return -1; // 没有找到
}

// 销毁哈希表
void destroyHashTable(HashTable* ht) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (ht->table[i] != NULL) {
            free(ht->table[i]->key);
            free(ht->table[i]);
        }
    }
    free(ht);
}

int main() {
    HashTable* ht = createHashTable();

    insert(ht, "apple", 5);
    insert(ht, "banana", 10);

    printf("apple: %d\n", find(ht, "apple"));
    printf("banana: %d\n", find(ht, "banana"));
    printf("grape: %d\n", find(ht, "grape"));

    destroyHashTable(ht);

    return 0;
}


if (ht->table[index] != NULL && strcmp(ht->table[index]->key, key) == 0)

  • ht->table[index] != NULL 确保了在哈希表中,索引 index 处的节点不为空,即确保了该位置上存在一个有效的键值对。
  • strcmp(ht->table[index]->key, key) == 0 检查键的值是否与给定的键值相等。如果相等,则表示找到了与给定键相匹配的节点。

问题

在上述最初版本的哈希表实现中,没有考虑哈希冲突的情况。哈希冲突是指当两个不同的键经过哈希函数计算后得到相同的哈希值,导致它们存储在哈希表中的同一个位置,这可能会导致数据丢失或无法正确访问。

解决哈希冲突的常见方法包括以下几种:

  • 开放寻址法(Open Addressing):在发生冲突时,顺序地探查哈希表的下一个位置,直到找到一个空闲的位置来存储键值对。这种方法包括线性探查、二次探查和双重哈希等。
  • 链地址法(Separate Chaining):在哈希表的每个位置上维护一个链表或其他数据结构,存储所有哈希值相同的键值对。当发生冲突时,新的键值对被添加到链表的末尾。

改进版本

为了解决哈希冲突问题,我们需要做以下修改:

  • 将哈希表的每个槽位改为指向链表头部的指针。
  • 当插入新的键值对时,首先检查该位置是否已经有键值对存在。如果存在,则将新的键值对追加到链表末尾;如果不存在,则将其直接插入到该位置。

通过使用链地址法,我们可以更好地处理哈希冲突,确保哈希表的性能和稳定性。

代码

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

#define TABLE_SIZE 100

// 哈希表中存储的键值对结构体
typedef struct KeyValuePair {
    char* key;
    int value;
    struct KeyValuePair* next; // 指向下一个键值对的指针
} KeyValuePair;

// 哈希表结构体
typedef struct {
    KeyValuePair* table[TABLE_SIZE];
} HashTable;

// 计算哈希值
int hash(char* key) {
    int hash = 0;
    for (int i = 0; key[i] != '\0'; i++) {
        hash = (hash + key[i]) % TABLE_SIZE;
    }
    return hash;
}

// 创建哈希表
HashTable* createHashTable() {
    HashTable* ht = (HashTable*)malloc(sizeof(HashTable));
    for (int i = 0; i < TABLE_SIZE; i++) {
        ht->table[i] = NULL; // 初始化每个槽位为空链表
    }
    return ht;
}

// 插入键值对到哈希表
void insert(HashTable* ht, char* key, int value) {
    int index = hash(key);
    KeyValuePair* kvp = (KeyValuePair*)malloc(sizeof(KeyValuePair));
    kvp->key = strdup(key);
    kvp->value = value;
    kvp->next = NULL;

    // 如果该位置为空,则直接将键值对插入
    if (ht->table[index] == NULL) {
        ht->table[index] = kvp;
    } else { // 否则,将键值对添加到链表末尾
        KeyValuePair* curr = ht->table[index];
        while (curr->next != NULL) {
            curr = curr->next;
        }
        curr->next = kvp;
    }
}

// 查找键对应的值
int find(HashTable* ht, char* key) {
    int index = hash(key);
    KeyValuePair* curr = ht->table[index];
    while (curr != NULL) {
        if (strcmp(curr->key, key) == 0) {
            return curr->value;
        }
        curr = curr->next;
    }
    return -1; // 没有找到
}

// 销毁哈希表
void destroyHashTable(HashTable* ht) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        KeyValuePair* curr = ht->table[i];
        while (curr != NULL) {
            KeyValuePair* temp = curr;
            curr = curr->next;
            free(temp->key);
            free(temp);
        }
    }
    free(ht);
}

int main() {
    HashTable* ht = createHashTable();

    insert(ht, "apple", 5);
    insert(ht, "banana", 10);
    insert(ht, "orange", 15);
    insert(ht, "grape", 20);
    insert(ht, "apple", 25); // 重复键,将会形成链表

    printf("apple: %d\n", find(ht, "apple"));
    printf("banana: %d\n", find(ht, "banana"));
    printf("orange: %d\n", find(ht, "orange"));
    printf("grape: %d\n", find(ht, "grape"));

    destroyHashTable(ht);

    return 0;
}

问题

上述代码输出如下:

apple: 5
banana: 10
orange: 15
grape: 20

观察可知,对于重复的键值对(如apple),没有进行处理,导致相同键的值会形成链表,而不是更新已有键的值。

最终版本

仅需修改insert(插入)函数,解决方案如下:

  • 遍历链表:在插入新的键值对之前,我们先遍历链表,查找是否已经存在相同的键。这是为了处理哈希冲突的情况。

  • 插入新节点:如果遍历完链表没有找到相同的键,则将新的键值对插入到链表的末尾。

代码

// 插入键值对到哈希表
void insert(HashTable* ht, char* key, int value) {
    int index = hash(key);
    KeyValuePair* kvp = (KeyValuePair*)malloc(sizeof(KeyValuePair));
    kvp->key = strdup(key);
    kvp->value = value;
    kvp->next = NULL;

    // 检查该位置是否已经存在节点
    KeyValuePair* curr = ht->table[index];
    KeyValuePair* prev = NULL;
    while (curr != NULL) {
        if (strcmp(curr->key, key) == 0) {
            // 键已经存在,更新值并返回
            curr->value = value;
            free(kvp->key); // 释放键的内存,因为键已经存在
            free(kvp); // 释放键值对的内存
            return;
        }
        prev = curr;
        curr = curr->next;
    }

    // 如果该位置为空,则直接将键值对插入
    if (prev == NULL) {
        ht->table[index] = kvp;
    } else { // 否则,将键值对添加到链表末尾
        prev->next = kvp;
    }
}

总结

  • 最初版本的哈希表实现简单,但存在哈希冲突问题。每个哈希桶只能存储一个键值对,导致相同哈希值的键会发生冲突。
  • 为了解决哈希冲突问题,我们引入了链地址法(Separate Chaining)。每个哈希桶不再存储单个键值对,而是指向一个链表的头指针。当发生哈希冲突时,新的键值对被插入到链表的末尾。但仍然存在一个问题:在插入新键值对时,没有检查是否已经存在相同的键。
  • 在进一步改进中,我们对插入函数进行了改进。在插入新的键值对之前,我们先遍历链表,检查是否已经存在相同的键。如果已经存在,则更新该键对应的值;如果不存在,则将新的键值对插入到链表的末尾。这样,我们就能够正确处理哈希冲突的情况,确保每个键值对都能正确存储并能够正确检索。
posted @ 2024-04-26 17:56  岸南  阅读(581)  评论(0)    收藏  举报