哈希表(散列表)看这一篇就够了!

一、哈希函数

        哈希函数是将键映射到哈希表索引的函数。它应该尽可能地将键均匀地分布在哈希表的所有位置上,以减少冲突(多个键映射到同一索引)的可能性。

以下是常见的哈希函数:

1.直接定址法(Direct Addressing)

        直接定址法适用于键的取值范围较小且连续的情况。它的基本思想是将每个键直接映射到哈希表中的一个具体位置,这个位置通常就是键的值。

        在直接定址法中,哈希函数的形式为:

h(k) = k

        其中 k 是键的值。这意味着每个键的值就是其在哈希表中的位置,因此需要一个大小足够的数组来存储所有可能的键值。

        直接定址法的优点是简单快速,哈希值的计算时间是常数级别的。但是它的缺点是需要一个足够大的数组来存储所有可能的键值,因此适用于键值范围较小且连续的情况。如果键值范围过大或者不连续,就会造成空间的浪费。

        以下是一个简单的示例,演示了如何使用直接定址法来实现哈希表:

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

#define TABLE_SIZE 1000

// 定义哈希表结构
typedef struct {
    int keys[TABLE_SIZE];
    int values[TABLE_SIZE];
} HashTable;

// 创建哈希表
HashTable* createHashTable() {
    HashTable* table = (HashTable*)malloc(sizeof(HashTable));
    return table;
}

// 插入键值对到哈希表中
void insert(HashTable* table, int key, int value) {
    table->keys[key] = key;
    table->values[key] = value;
}

// 查找键对应的值
int find(HashTable* table, int key) {
    return table->values[key];
}

// 主函数
int main() {
    HashTable* table = createHashTable();

    // 插入键值对
    insert(table, 10, 100);
    insert(table, 20, 200);
    insert(table, 30, 300);

    // 查找键对应的值
    printf("Value for key 10: %d\n", find(table, 10));
    printf("Value for key 20: %d\n", find(table, 20));
    printf("Value for key 30: %d\n", find(table, 30));

    return 0;
}

2.除留取余法(Division Method)

        除留取余法使用键值除以哈希表的大小,然后取余数作为哈希值。它的形式为:

h(k) = k mod m

        其中,k 是键的值,m 是哈希表的大小。

        除留取余法的实现非常简单,适用于大多数哈希表的场景。它的优点是简单易实现,并且在键值分布均匀的情况下可以得到良好的哈希值。但是,如果键的值分布不均匀,可能会导致哈希冲突较多。

        以下是一个简单的示例,演示了如何使用除留取余法来实现哈希函数:

#include <stdio.h>

#define TABLE_SIZE 10

// 哈希函数:除留取余法
int hash(int key) {
    return key % TABLE_SIZE;
}

// 主函数
int main() {
    int keys[] = {12, 25, 6, 8, 17, 35, 14, 23, 11, 9};
    int n = sizeof(keys) / sizeof(keys[0]);

    // 计算哈希值并输出
    printf("Keys and their hash values:\n");
    for (int i = 0; i < n; i++) {
        int h = hash(keys[i]);
        printf("Key: %d, Hash: %d\n", keys[i], h);
    }

    return 0;
}

 3.平方取中法(Mid Square Method)

        平方取中法将键值的平方作为中间结果,然后取中间结果的某一部分作为哈希值。具体步骤如下:

  1. 将键值进行平方运算。
  2. 取平方结果的中间一部分作为哈希值。

        平方取中法的形式为:

h(k) = mid ( (k2) ,d,w)

        其中,k 是键的值,d 是哈希值的位数,w 是键的位数。函数mid(x,d,w) 表示取 x 的中间 d 位作为哈希值。

        以下是一个示例,演示如何使用平方取中法来实现哈希函数:

#include <stdio.h>
#include <math.h>

// 计算 x 的中间 d 位数作为哈希值
int mid(int x, int d, int w) {
    int mask = (int)pow(10, w - d) - 1; // 用于取中间 d 位数的掩码
    return (x / (int)pow(10, (w - d) / 2)) % (int)pow(10, d); // 取中间 d 位数
}

// 平方取中法哈希函数
int hash(int key, int d, int w) {
    return mid(key * key, d, w); // 对键的平方取中间部分作为哈希值
}

// 主函数
int main() {
    int keys[] = {12, 25, 6, 8, 17, 35, 14, 23, 11, 9};
    int n = sizeof(keys) / sizeof(keys[0]);

    // 计算哈希值并输出
    printf("Keys and their hash values:\n");
    for (int i = 0; i < n; i++) {
        int h = hash(keys[i], 2, 4); // 设置哈希值的位数为 2,键的位数为 4
        printf("Key: %d, Hash: %d\n", keys[i], h);
    }

    return 0;
}

二、哈希冲突

        当两个或多个键通过哈希函数映射到哈希表的同一个索引位置时,就发生了哈希冲突。处理冲突的方法有很多种,包括开放寻址法和链表法等。

三、解决哈希冲突的方法

1.开放寻址法(Open Addressing)

线性探测(Linear Probing)

        线性探测是一种开放寻址法的处理哈希冲突的方法。当发生哈希冲突时,线性探测会依次检查哈希表中的下一个位置,直到找到一个空闲位置为止。具体步骤如下:

  1. 当发生哈希冲突时,计算出下一个探测位置。
  2. 检查该位置是否为空,如果为空,则将键值对存储在该位置;如果不为空,则继续探测下一个位置。
  3. 重复以上步骤,直到找到一个空闲位置为止,或者遍历了整个哈希表。

        线性探测的探测步长是固定的,通常为 1。也就是说,每次探测都会顺序地检查下一个位置。

        下面是一个简单的示例,演示了如何使用线性探测来处理哈希冲突:

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

#define TABLE_SIZE 10

// 哈希表结构
typedef struct {
    int keys[TABLE_SIZE];
    int values[TABLE_SIZE];
} HashTable;

// 创建哈希表
HashTable* createHashTable() {
    HashTable* table = (HashTable*)malloc(sizeof(HashTable));
    for (int i = 0; i < TABLE_SIZE; ++i) {
        table->keys[i] = -1; // 初始化所有键为 -1,表示空闲位置
    }
    return table;
}

// 哈希函数:除留余数法
int hash(int key) {
    return key % TABLE_SIZE;
}

// 插入键值对到哈希表中(使用线性探测解决冲突)
void insert(HashTable* table, int key, int value) {
    int index = hash(key);
    while (table->keys[index] != -1) { // 发生冲突时,进行线性探测
        index = (index + 1) % TABLE_SIZE; // 计算下一个探测位置
    }
    table->keys[index] = key;
    table->values[index] = value;
}

// 查找键对应的值
int find(HashTable* table, int key) {
    int index = hash(key);
    while (table->keys[index] != -1) { // 在哈希表中进行线性探测
        if (table->keys[index] == key) {
            return table->values[index]; // 找到键,返回对应的值
        }
        index = (index + 1) % TABLE_SIZE; // 继续探测下一个位置
    }
    return -1; // 未找到键,返回 -1
}

// 主函数
int main() {
    HashTable* table = createHashTable();

    // 插入键值对
    insert(table, 12, 120);
    insert(table, 22, 220);
    insert(table, 32, 320);

    // 查找键对应的值
    printf("Value for key 12: %d\n", find(table, 12));
    printf("Value for key 22: %d\n", find(table, 22));
    printf("Value for key 32: %d\n", find(table, 32));
    printf("Value for key 42: %d\n", find(table, 42)); // 键不存在

    return 0;
}

二次探测(Quadratic Probing)

        二次探测是一种解决哈希冲突的开放寻址法方法之一。与线性探测不同,二次探测的探测步长不是固定的增量,而是通过一个二次函数计算得到的。具体步骤如下:

  1. 当发生哈希冲突时,计算出下一个探测位置。
  2. 计算下一个探测位置时,使用一个二次函数,而不是简单地加上固定的增量。典型的二次函数为f(i)=i^2。
  3. 检查该位置是否为空,如果为空,则将键值对存储在该位置;如果不为空,则继续计算下一个探测位置。
  4. 如果在探测过程中遍历了整个哈希表仍未找到空闲位置,表示哈希表已满,需要进行相应的处理(例如重新哈希或扩展哈希表)。

        以下是一个简单的示例,演示了如何使用二次探测来处理哈希冲突:

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

#define TABLE_SIZE 10

// 哈希表结构
typedef struct {
    int keys[TABLE_SIZE];
    int values[TABLE_SIZE];
} HashTable;

// 创建哈希表
HashTable* createHashTable() {
    HashTable* table = (HashTable*)malloc(sizeof(HashTable));
    for (int i = 0; i < TABLE_SIZE; ++i) {
        table->keys[i] = -1; // 初始化所有键为 -1,表示空闲位置
    }
    return table;
}

// 哈希函数:除留余数法
int hash(int key) {
    return key % TABLE_SIZE;
}

// 插入键值对到哈希表中(使用二次探测解决冲突)
void insert(HashTable* table, int key, int value) {
    int index = hash(key);
    int i = 1;
    while (table->keys[index] != -1) { // 发生冲突时,进行二次探测
        index = (index + i * i) % TABLE_SIZE; // 计算下一个探测位置
        i++; // 递增二次探测的步长
    }
    table->keys[index] = key;
    table->values[index] = value;
}

// 查找键对应的值
int find(HashTable* table, int key) {
    int index = hash(key);
    int i = 1;
    while (table->keys[index] != -1) { // 在哈希表中进行二次探测
        if (table->keys[index] == key) {
            return table->values[index]; // 找到键,返回对应的值
        }
        index = (index + i * i) % TABLE_SIZE; // 计算下一个探测位置
        i++; // 递增二次探测的步长
    }
    return -1; // 未找到键,返回 -1
}

// 主函数
int main() {
    HashTable* table = createHashTable();

    // 插入键值对
    insert(table, 12, 120);
    insert(table, 22, 220);
    insert(table, 32, 320);

    // 查找键对应的值
    printf("Value for key 12: %d\n", find(table, 12));
    printf("Value for key 22: %d\n", find(table, 22));
    printf("Value for key 32: %d\n", find(table, 32));
    printf("Value for key 42: %d\n", find(table, 42)); // 键不存在

    return 0;
}

双重哈希(Double Hashing)

        双重哈希不是简单地在哈希表中依次查找下一个位置,而是使用第二个独立的哈希函数来计算探测的步长。具体步骤如下:

  1. 当发生哈希冲突时,计算出第一个哈希函数的结果作为起始位置。
  2. 如果该位置为空,则将键值对存储在该位置。
  3. 如果该位置不为空,则使用第二个哈希函数计算出一个步长,然后依次探测下一个位置,直到找到一个空闲位置为止。
  4. 将键值对存储在空闲位置上。

        双重哈希的优点在于它可以提供更好的探测步长,从而减少哈希冲突的发生,并且相对于线性探测和二次探测,它更均匀地分布键值对。       

        以下是一个简单的示例,演示了如何使用双重哈希来处理哈希冲突:

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

#define TABLE_SIZE 10

// 哈希表结构
typedef struct {
    int keys[TABLE_SIZE];
    int values[TABLE_SIZE];
} HashTable;

// 创建哈希表
HashTable* createHashTable() {
    HashTable* table = (HashTable*)malloc(sizeof(HashTable));
    for (int i = 0; i < TABLE_SIZE; ++i) {
        table->keys[i] = -1; // 初始化所有键为 -1,表示空闲位置
    }
    return table;
}

// 哈希函数1:除留余数法
int hash1(int key) {
    return key % TABLE_SIZE;
}

// 哈希函数2:另一种简单的哈希函数
int hash2(int key) {
    return 7 - (key % 7); // 7 是一个较小的质数,确保哈希函数2与哈希表大小互质
}

// 插入键值对到哈希表中(使用双重哈希解决冲突)
void insert(HashTable* table, int key, int value) {
    int index = hash1(key);
    int step = hash2(key);
    while (table->keys[index] != -1) { // 发生冲突时,进行双重哈希探测
        index = (index + step) % TABLE_SIZE; // 计算下一个探测位置
    }
    table->keys[index] = key;
    table->values[index] = value;
}

// 查找键对应的值
int find(HashTable* table, int key) {
    int index = hash1(key);
    int step = hash2(key);
    while (table->keys[index] != -1) { // 在哈希表中进行双重哈希探测
        if (table->keys[index] == key) {
            return table->values[index]; // 找到键,返回对应的值
        }
        index = (index + step) % TABLE_SIZE; // 继续探测下一个位置
    }
    return -1; // 未找到键,返回 -1
}

// 主函数
int main() {
    HashTable* table = createHashTable();

    // 插入键值对
    insert(table, 12, 120);
    insert(table, 22, 220);
    insert(table, 32, 320);

    // 查找键对应的值
    printf("Value for key 12: %d\n", find(table, 12));
    printf("Value for key 22: %d\n", find(table, 22));
    printf("Value for key 32: %d\n", find(table, 32));
    printf("Value for key 42: %d\n", find(table, 42)); // 键不存在

    return 0;
}

2.链表法(Chaining)

        链表法通过在哈希表的每个槽点(slot)上维护一个链表或其他形式的容器来存储具有相同哈希值的键值对。当发生冲突时,新的键值对会被添加到相应槽点所对应的链表中。

        具体步骤如下:

  1. 对键进行哈希运算,得到哈希值。
  2. 使用哈希值确定键值对应该存储的槽点(slot)。
  3. 如果槽点上已经有键值对存在,将新的键值对添加到链表的末尾;如果槽点为空,则创建一个新的链表,并将新的键值对作为链表的第一个元素。
  4. 当需要查找或删除某个键值对时,根据哈希值找到对应的槽点,然后遍历链表查找目标键值对。

        链表法的优点是简单易实现,并且可以有效地处理哈希冲突,适用于大多数情况。但是,当链表过长时,会影响哈希表的性能,因此需要在实际应用中进行适当的调优,例如通过动态调整哈希表大小、使用更高效的哈希函数等方式来优化。

        以下是一个简单的示例,演示了如何使用链表法来处理哈希冲突:

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

#define TABLE_SIZE 10

// 键值对结构
typedef struct Node {
    int key;
    int value;
    struct Node* next;
} Node;

// 哈希表结构
typedef struct {
    Node* slots[TABLE_SIZE];
} HashTable;

// 创建新的节点
Node* createNode(int key, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->key = key;
    newNode->value = value;
    newNode->next = NULL;
    return newNode;
}

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

// 哈希函数:除留余数法
int hash(int key) {
    return key % TABLE_SIZE;
}

// 插入键值对到哈希表中(使用链表法解决冲突)
void insert(HashTable* table, int key, int value) {
    int index = hash(key);
    Node* newNode = createNode(key, value);
    if (table->slots[index] == NULL) {
        table->slots[index] = newNode; // 槽点为空,直接插入新节点
    } else {
        Node* current = table->slots[index];
        while (current->next != NULL) {
            current = current->next;
        }
        current->next = newNode; // 槽点非空,遍历链表找到末尾插入新节点
    }
}

// 查找键对应的值
int find(HashTable* table, int key) {
    int index = hash(key);
    Node* current = table->slots[index];
    while (current != NULL) {
        if (current->key == key) {
            return current->value; // 找到键,返回对应的值
        }
        current = current->next;
    }
    return -1; // 未找到键,返回 -1
}

// 主函数
int main() {
    HashTable* table = createHashTable();

    // 插入键值对
    insert(table, 12, 120);
    insert(table, 22, 220);
    insert(table, 32, 320);

    // 查找键对应的值
    printf("Value for key 12: %d\n", find(table, 12));
    printf("Value for key 22: %d\n", find(table, 22));
    printf("Value for key 32: %d\n", find(table, 32));
    printf("Value for key 42: %d\n", find(table, 42)); // 键不存在

    return 0;
}

四、哈希表的实现:

        哈希表通常由一个数组和一个哈希函数组成。哈希函数将键映射到数组的索引位置,然后在该位置存储键值对。在处理哈希冲突时,需要根据所选择的解决方法调整哈希表的存储结构。

        以下示例演示了如何使用 C 语言实现一个简单的哈希表。哈希表由一个固定大小的数组构成,数组的每个元素是一个指向哈希表节点的指针。每个节点包含键和值。通过哈希函数计算键的哈希值,将其映射到数组的索引,然后将节点插入到对应的索引位置。在查找时,通过哈希函数计算键的哈希值,然后在对应的索引位置查找节点。

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

#define TABLE_SIZE 10

// 定义哈希表节点结构
typedef struct {
    char* key;
    int value;
} Node;

// 定义哈希表结构
typedef struct {
    Node* nodes[TABLE_SIZE];
} HashTable;

// 哈希函数,计算键的哈希值
unsigned int hash(const char* key) {
    unsigned int hash = 0;
    for (int i = 0; i < strlen(key); i++) {
        hash = hash * 31 + key[i];
    }
    return hash % TABLE_SIZE;
}

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

// 插入键值对到哈希表中
void insert(HashTable* table, const char* key, int value) {
    unsigned int index = hash(key);
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->key = strdup(key);
    newNode->value = value;
    table->nodes[index] = newNode;
}

// 查找键对应的值
int find(HashTable* table, const char* key) {
    unsigned int index = hash(key);
    if (table->nodes[index] != NULL && strcmp(table->nodes[index]->key, key) == 0) {
        return table->nodes[index]->value;
    }
    return -1;  // 如果键不存在,则返回-1
}

// 主函数
int main() {
    HashTable* table = createHashTable();

    // 插入键值对
    insert(table, "apple", 10);
    insert(table, "banana", 20);
    insert(table, "orange", 30);

    // 查找键对应的值
    printf("Value for 'apple': %d\n", find(table, "apple"));
    printf("Value for 'banana': %d\n", find(table, "banana"));
    printf("Value for 'orange': %d\n", find(table, "orange"));
    printf("Value for 'grape': %d\n", find(table, "grape"));  // 键不存在

    return 0;
}
  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

多宝气泡水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值