C语言哈希表

哈希表(Hash Table)是一种高效的数据结构,用于实现快速的数据查找、插入和删除操作。哈希表通过将关键字(Key)映射到表中的位置(索引),实现近似常数时间的操作效率。哈希表在许多应用中广泛使用,如数据库索引、缓存系统、编译器符号表等。本文将详细介绍如何使用C语言实现哈希表,包括基本概念、哈希函数、冲突处理方法、基本操作、示例代码及其优缺点。

哈希表的基本概念

定义

哈希表是一种通过哈希函数将关键字映射到数组索引的数据结构。每个元素(键值对)由一个键(Key)和一个值(Value)组成。理想情况下,哈希函数能将不同的键均匀地分布到哈希表的各个位置,从而实现高效的数据操作。

哈希函数

哈希函数是哈希表的核心,用于将键转换为数组索引。一个好的哈希函数应具备以下特性:

  • 确定性:相同的键每次映射的索引应相同。

  • 均匀性:键应均匀分布到哈希表的各个位置,尽量减少冲突。

  • 高效性:计算哈希值的时间复杂度应尽可能低。

常见的哈希函数包括除留余数法、乘法散列法、字符串哈希函数等。

冲突处理

由于哈希函数将有限的索引映射到可能无限的键集合,冲突是不可避免的。常见的冲突处理方法包括:

  1. 链地址法(Separate Chaining):每个哈希表的索引对应一个链表,当多个键映射到同一索引时,将它们存储在该索引的链表中。

  2. 开放地址法(Open Addressing):当发生冲突时,通过探测方法(如线性探测、二次探测、双重哈希等)寻找下一个可用的位置存储键值对。

本教程将采用链地址法来实现哈希表,因为其实现相对简单且适用于动态数据集。

应用场景

  1. 数据库索引:快速查找记录。

  2. 缓存系统:实现高效的缓存查找。

  3. 编译器符号表:存储变量和函数的信息。

  4. 密码学:用于数据加密和哈希验证。

  5. 字典和集合的实现

哈希表的基本结构

在C语言中,哈希表通常由一个数组和一组链表(用于链地址法)组成。每个数组元素称为桶(Bucket),每个桶存储一个或多个键值对。

哈希表节点的结构体定义

首先,定义哈希表节点(键值对)的结构体:

#include 
#include 
#include 

// 定义键值对的结构体
typedef struct HashNode {
    char* key;               // 键
    int value;               // 值
    struct HashNode* next;   // 指向下一个节点的指针(用于链地址法)
} HashNode;

哈希表的结构体定义

接下来,定义哈希表的结构体:

// 定义哈希表的结构体
typedef struct HashTable {
    int size;        // 哈希表的大小(桶的数量)
    HashNode** table; // 指向哈希表数组的指针
} HashTable;

哈希函数的实现

为了将键(字符串)映射到哈希表的索引,我们需要设计一个有效的哈希函数。以下是一个简单的哈希函数示例,采用了djbx33a算法(又称为djb2哈希函数),它对字符串具有较好的分布性:

// djb2哈希函数
unsigned int hashFunction(char* key, int size) {
    unsigned long hash = 5381;
    int c;
    while ((c = *key++))
        hash = ((hash << 5) + hash) + c; // hash * 33 + c
    return hash % size;
}

哈希表的基本操作

1. 创建哈希表

创建一个指定大小的哈希表,并初始化所有桶为空:

// 创建并初始化哈希表
HashTable* createHashTable(int size) {
    HashTable* hashtable = (HashTable*)malloc(sizeof(HashTable));
    if (!hashtable) {
        printf("内存分配失败\n");
        exit(1);
    }
    hashtable->size = size;
    hashtable->table = (HashNode**)malloc(sizeof(HashNode*) * size);
    if (!hashtable->table) {
        printf("内存分配失败\n");
        exit(1);
    }
    for (int i = 0; i < size; i++)
        hashtable->table[i] = NULL;
    return hashtable;
}

2. 创建新节点

创建一个新的哈希表节点:

// 创建一个新的哈希表节点
HashNode* createHashNode(char* key, int value) {
    HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));
    if (!newNode) {
        printf("内存分配失败\n");
        exit(1);
    }
    newNode->key = strdup(key); // 复制键字符串
    newNode->value = value;
    newNode->next = NULL;
    return newNode;
}

3. 插入键值对(Insert)

将一个键值对插入到哈希表中。如果键已存在,则更新其值:

// 插入键值对到哈希表
void insert(HashTable* hashtable, char* key, int value) {
    unsigned int index = hashFunction(key, hashtable->size);
    HashNode* current = hashtable->table[index];
    
    // 检查键是否已存在
    while (current) {
        if (strcmp(current->key, key) == 0) {
            current->value = value; // 更新值
            return;
        }
        current = current->next;
    }
    
    // 如果键不存在,则创建新节点并插入链表头部
    HashNode* newNode = createHashNode(key, value);
    newNode->next = hashtable->table[index];
    hashtable->table[index] = newNode;
}

4. 查找键值对(Search)

根据键查找对应的值:

// 查找键对应的值,若找到则返回值,否则返回-1
int search(HashTable* hashtable, char* key) {
    unsigned int index = hashFunction(key, hashtable->size);
    HashNode* current = hashtable->table[index];
    
    while (current) {
        if (strcmp(current->key, key) == 0)
            return current->value;
        current = current->next;
    }
    return -1; // 表示未找到
}

5. 删除键值对(Delete)

根据键删除对应的键值对:

// 根据键删除键值对
void deleteKey(HashTable* hashtable, char* key) {
    unsigned int index = hashFunction(key, hashtable->size);
    HashNode* current = hashtable->table[index];
    HashNode* prev = NULL;
    
    while (current) {
        if (strcmp(current->key, key) == 0) {
            if (prev)
                prev->next = current->next;
            else
                hashtable->table[index] = current->next;
            free(current->key);
            free(current);
            return;
        }
        prev = current;
        current = current->next;
    }
    printf("键 '%s' 未找到,无法删除。\n", key);
}

6. 打印哈希表

遍历并打印哈希表中的所有键值对:

// 打印哈希表
void printHashTable(HashTable* hashtable) {
    for (int i = 0; i < hashtable->size; i++) {
        HashNode* current = hashtable->table[i];
        printf("桶 %d:", i);
        while (current) {
            printf(" -> [键: %s, 值: %d]", current->key, current->value);
            current = current->next;
        }
        printf("\n");
    }
}

7. 销毁哈希表

释放哈希表占用的内存:

// 销毁哈希表,释放内存
void destroyHashTable(HashTable* hashtable) {
    for (int i = 0; i < hashtable->size; i++) {
        HashNode* current = hashtable->table[i];
        while (current) {
            HashNode* temp = current;
            current = current->next;
            free(temp->key);
            free(temp);
        }
    }
    free(hashtable->table);
    free(hashtable);
}

哈希表的示例代码

以下是一个完整的哈希表实现示例程序,展示了如何插入、查找、删除和打印哈希表中的键值对:

#include 
#include 
#include 

// 定义键值对的结构体
typedef struct HashNode {
    char* key;
    int value;
    struct HashNode* next;
} HashNode;

// 定义哈希表的结构体
typedef struct HashTable {
    int size;
    HashNode** table;
} HashTable;

// djb2哈希函数
unsigned int hashFunction(char* key, int size) {
    unsigned long hash = 5381;
    int c;
    while ((c = *key++))
        hash = ((hash << 5) + hash) + c; // hash * 33 + c
    return hash % size;
}

// 创建并初始化哈希表
HashTable* createHashTable(int size) {
    HashTable* hashtable = (HashTable*)malloc(sizeof(HashTable));
    if (!hashtable) {
        printf("内存分配失败\n");
        exit(1);
    }
    hashtable->size = size;
    hashtable->table = (HashNode**)malloc(sizeof(HashNode*) * size);
    if (!hashtable->table) {
        printf("内存分配失败\n");
        exit(1);
    }
    for (int i = 0; i < size; i++)
        hashtable->table[i] = NULL;
    return hashtable;
}

// 创建一个新的哈希表节点
HashNode* createHashNode(char* key, int value) {
    HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));
    if (!newNode) {
        printf("内存分配失败\n");
        exit(1);
    }
    newNode->key = strdup(key);
    newNode->value = value;
    newNode->next = NULL;
    return newNode;
}

// 插入键值对到哈希表
void insert(HashTable* hashtable, char* key, int value) {
    unsigned int index = hashFunction(key, hashtable->size);
    HashNode* current = hashtable->table[index];
    
    // 检查键是否已存在
    while (current) {
        if (strcmp(current->key, key) == 0) {
            current->value = value; // 更新值
            return;
        }
        current = current->next;
    }
    
    // 如果键不存在,则创建新节点并插入链表头部
    HashNode* newNode = createHashNode(key, value);
    newNode->next = hashtable->table[index];
    hashtable->table[index] = newNode;
}

// 查找键对应的值,若找到则返回值,否则返回-1
int search(HashTable* hashtable, char* key) {
    unsigned int index = hashFunction(key, hashtable->size);
    HashNode* current = hashtable->table[index];
    
    while (current) {
        if (strcmp(current->key, key) == 0)
            return current->value;
        current = current->next;
    }
    return -1; // 表示未找到
}

// 根据键删除键值对
void deleteKey(HashTable* hashtable, char* key) {
    unsigned int index = hashFunction(key, hashtable->size);
    HashNode* current = hashtable->table[index];
    HashNode* prev = NULL;
    
    while (current) {
        if (strcmp(current->key, key) == 0) {
            if (prev)
                prev->next = current->next;
            else
                hashtable->table[index] = current->next;
            free(current->key);
            free(current);
            return;
        }
        prev = current;
        current = current->next;
    }
    printf("键 '%s' 未找到,无法删除。\n", key);
}

// 打印哈希表
void printHashTable(HashTable* hashtable) {
    for (int i = 0; i < hashtable->size; i++) {
        HashNode* current = hashtable->table[i];
        printf("桶 %d:", i);
        while (current) {
            printf(" -> [键: %s, 值: %d]", current->key, current->value);
            current = current->next;
        }
        printf("\n");
    }
}

// 销毁哈希表,释放内存
void destroyHashTable(HashTable* hashtable) {
    for (int i = 0; i < hashtable->size; i++) {
        HashNode* current = hashtable->table[i];
        while (current) {
            HashNode* temp = current;
            current = current->next;
            free(temp->key);
            free(temp);
        }
    }
    free(hashtable->table);
    free(hashtable);
}

int main() {
    // 创建一个大小为10的哈希表
    HashTable* hashtable = createHashTable(10);
    
    // 插入键值对
    insert(hashtable, "apple", 100);
    insert(hashtable, "banana", 200);
    insert(hashtable, "orange", 300);
    insert(hashtable, "grape", 400);
    insert(hashtable, "melon", 500);
    
    // 打印哈希表
    printf("初始哈希表:\n");
    printHashTable(hashtable);
    
    // 查找键
    char* keyToFind = "banana";
    int value = search(hashtable, keyToFind);
    if (value != -1)
        printf("键 '%s' 的值是 %d\n", keyToFind, value);
    else
        printf("键 '%s' 未找到\n", keyToFind);
    
    // 删除键
    char* keyToDelete = "orange";
    deleteKey(hashtable, keyToDelete);
    printf("删除键 '%s' 后的哈希表:\n", keyToDelete);
    printHashTable(hashtable);
    
    // 更新键
    insert(hashtable, "apple", 150);
    printf("更新键 'apple' 后的哈希表:\n");
    printHashTable(hashtable);
    
    // 销毁哈希表
    destroyHashTable(hashtable);
    
    return 0;
}

示例输出

初始哈希表:
桶 0:
桶 1: -> [键: apple, 值: 100]
桶 2:
桶 3:
桶 4:
桶 5: -> [键: banana, 值: 200]
桶 6: -> [键: orange, 值: 300]
桶 7: -> [键: grape, 值: 400]
桶 8:
桶 9: -> [键: melon, 值: 500]

键 'banana' 的值是 200
删除键 'orange' 后的哈希表:
桶 0:
桶 1: -> [键: apple, 值: 100]
桶 2:
桶 3:
桶 4:
桶 5: -> [键: banana, 值: 200]
桶 6:
桶 7: -> [键: grape, 值: 400]
桶 8:
桶 9: -> [键: melon, 值: 500]

更新键 'apple' 后的哈希表:
桶 0:
桶 1: -> [键: apple, 值: 150]
桶 2:
桶 3:
桶 4:
桶 5: -> [键: banana, 值: 200]
桶 6:
桶 7: -> [键: grape, 值: 400]
桶 8:
桶 9: -> [键: melon, 值: 500]

哈希表的优缺点

优点

  1. 高效的查找、插入和删除:哈希表的平均时间复杂度为O(1),适用于需要频繁操作的数据集。

  2. 灵活的键类型:支持多种键类型(如字符串、整数等),通过适当的哈希函数实现。

  3. 动态扩展:哈希表的大小可以根据需要动态调整,以适应不同的数据规模。

缺点

  1. 冲突处理开销:虽然大多数情况下操作效率高,但在冲突频繁的情况下,性能可能下降。

  2. 哈希函数设计:需要设计高效且均匀的哈希函数,避免冲突过多。

  3. 空间浪费:为了减少冲突,哈希表通常需要预留一定的空桶,可能导致空间浪费。

  4. 不支持有序操作:哈希表不维护键的顺序,不适合需要有序数据的场景。

哈希表的应用场景

  1. 数据库索引:用于快速定位记录。

  2. 编译器符号表:存储变量、函数等符号的信息。

  3. 缓存系统:实现快速的数据缓存,如内存缓存、网页缓存。

  4. 唯一性检测:用于检查数据集合中的重复元素。

  5. 字典和集合的实现:实现高效的键值对存储和查询。

哈希表的优化

动态扩展

随着数据量的增加,哈希表的负载因子(Load Factor,即元素数量与哈希表大小的比值)可能增大,导致冲突增加。为了保持高效性,通常需要在负载因子达到某一阈值时,扩展哈希表的大小,并重新哈希所有元素。

使用更好的哈希函数

选择适合具体键类型的哈希函数,可以显著减少冲突,提高哈希表的性能。例如,对于字符串键,可以使用更复杂的哈希函数如MurmurHash、SipHash等。

冲突处理策略的选择

根据具体应用场景,选择适合的冲突处理策略。例如,链地址法适用于动态数据集,而开放地址法在内存使用上更为紧凑。

你可能感兴趣的:(c语言,哈希算法,数据结构,开发语言,单片机)