当前位置: 首页 > article >正文

哈希表(C语言版)

文章目录

  • 哈希表
    • 原理
    • 实现(无自动扩容功能)
      • 代码
      • 运行结果
    • 分析
    • 应用

哈希表

如何统计一段文本中,小写字母出现的次数?

显然,我们可以用数组 int table[26] 来存储每个小写字母出现的次数,而且这样处理,效率奇高。假如我们想知道字母’k’出现的次数,直接访问元素 table['k' - 'a'] 即可,时间复杂度为O(1)。

在现实生活中,我们经常需要存储键值对(key-value)数据,比如上面的 ‘a’:10, ‘b’:6,再比如账号:个人信息,关键字:网页等等。如果键的取值范围很小(比如上面的例子),那么我们可以用数组存储,为每一个键绑定一个索引。

但是,如果键的取值范围很大,那么数组的方式就行不通了。哈希表就是被设计用来解决这样一个问题的~

原理

哈希表的核心设计分为两个部分:

  1. 哈希函数。哈希函数将 key 转换为数组中的一个索引。理想情况下不同的 key 都能转换成不同的索引值。当然这只是理想情况,所以我们还需要处理两个或者多个 key 都散列到相同索引值的情况 (哈希冲突)。

    优秀的哈希函数需要满足这些特性(拓展):
    a. 运算速度快。
    b. 尽量使键平均分布
    c. 逆向非常困难
    d. 对数据非常敏感
    e. 哈希冲突的概率非常小
    
    哈希函数:模拟等概率随机分布事件。
    
  2. 处理哈希冲突。

    • 开放地址法:线性探测法、平方探测法、再散列法
    • 拉链法

实现(无自动扩容功能)

这里,我们也采用常用的拉链法来解决哈希冲突,如下图所示:

在这里插入图片描述

代码

// Hash.h

#include <stdint.h>
#define N 10

typedef char* K;
typedef char* V;

typedef struct node {
    K key;
    V val;
    struct node* next;
} Node;

typedef struct {
    Node* table[N];
    int size;
    int capacity;
    uint32_t hashseed; // 哈希种子 保证哈希桶位置映射的随机性
} HashMap;

HashMap* hashmap_create();
void hashmap_destroy(HashMap* map);

V hashmap_put(HashMap* map, K key, V val);
V hashmap_get(HashMap* map, K key);
void hashmap_delete(HashMap* map, K key);
// Hash.c

#include "hash.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

HashMap* hashmap_create() {
    // calloc 方法
    HashMap* hashmap = (HashMap*)calloc(1, sizeof(HashMap));
    if (hashmap) {
        hashmap->size = 0;
        hashmap->capacity = N;
        hashmap->hashseed = time(NULL);
    }
    return hashmap;
}

// hashfunc()
/* murmurhash2 */
uint32_t hash(const void* key, int len, uint32_t seed) {
    const uint32_t m = 0x5bd1e995;
    const int r = 24;
    uint32_t h = seed ^ len;
    const unsigned char* data = (const unsigned char*)key;

    while (len >= 4) {
        uint32_t k = *(uint32_t*)data;

        k *= m;
        k ^= k >> r;
        k *= m;

        h *= m;
        h ^= k;

        data += 4;
        len -= 4;
    }

    switch (len)
    {
    case 3: h ^= data[2] << 16;
    case 2: h ^= data[1] << 8;
    case 1: h ^= data[0];
        h *= m;
    };

    h ^= h >> 13;
    h *= m;
    h ^= h >> 15;

    return h;
}

V hashmap_put(HashMap* map, K key, V val) {
    // a. 如果key不存在,添加key-val,并返回NULL
    // b. 如果key存在,更新key关联的val,返回原来的val
    int idx = hash(key, strlen(key), map->hashseed) % map->capacity; // 确定哈希桶
    Node* cur = map->table[idx];

    while (cur) {
        if (strcmp(cur->key, key) == 0) { // 如果key存在
            V oldVal = cur->val;
            cur->val = val;
            printf("有重复key, 已将旧值:%s 更换为新值:%s\n", oldVal, val);
            return oldVal;
        }
        cur = cur->next;
    } // cur == NULL

    // key不存在的情况,插入新的键值对
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->key = key;
    newNode->val = val;
    newNode->next = map->table[idx]; // 头插法

    map->table[idx] = newNode; // 更新哈希桶的地址

    map->size++;
    printf("插入键值对 key: %s  val: %s\n", key, val);

    return NULL;
}

V hashmap_get(HashMap* map, K key) {
    // a. 如果key不存在,返回NULL
    // b. 如果key存在,返回key关联的val
    int idx = hash(key, strlen(key), map->hashseed) % map->capacity; // 确定哈希桶
    Node* cur = map->table[idx];
    while (cur) {
        if (strcmp(cur->key, key) == 0) { // key 存在
            printf("找到了目标键:%s 对应的值为:%s\n", cur->key, cur->val);
            return cur->val;
        }
        cur = cur->next;
    }
    // key不存在
    printf("没找到目标键 %s 对应的键值对\n", key);
    return NULL;
}

void hashmap_delete(HashMap* map, K key) {
    int idx = hash(key, strlen(key), map->hashseed) % map->capacity; // 确定哈希桶
    Node* cur = map->table[idx];
    Node* prev = NULL;

    while (cur) {
        if (strcmp(cur->key, key) == 0) {  // 找到了目标键
            if (prev == NULL)  // 第一个结点
                map->table[idx] = cur->next;
            else 
                prev->next = cur->next;
            printf("键值对 key: %s val: %s 已释放\n", cur->key, cur->val);
            free(cur);

            map->size--;
            return;
        }
        prev = cur;
        cur = cur->next;
    }
    // 没有找到目标键
    printf("没找到目标键 %s 对应的键值对,无法删除\n", key);
}

void hashmap_destroy(HashMap* map) {
    // 1. 释放所有结点
    printf("即将释放哈希表中共 %d 对键值对\n", map->size);
    for (int i = 0; i < map->capacity; i++) {
        Node* cur = map->table[i];
        while (cur) {
            Node* freeNode = cur;
            cur = cur->next;
            printf("键值对 key: %s val: %s 已释放\n", freeNode->key, freeNode->val);
            free(freeNode);
        } // cur == NULL

    }

    // 2. 释放map->table
    free(map->table);
    // 3. 释放map结构体
    free(map);
    printf("哈希表释放成功\n");
}
// main.c
#include "hash.h"
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    HashMap* map = hashmap_create();

    hashmap_put(map, "1", "tom");
    hashmap_put(map, "2", "jack");

    hashmap_get(map, "1");

    hashmap_put(map, "1", "jane");

    hashmap_get(map, "1");
    hashmap_get(map, "100");

    hashmap_delete(map, "1");
    hashmap_get(map, "1");

    hashmap_put(map, "3", "musk");
    hashmap_put(map, "4", "musk");
    hashmap_put(map, "5", "musk");
    hashmap_put(map, "6", "musk");

    hashmap_destroy(map);

    return 0;
}

运行结果

在这里插入图片描述

分析

在哈希函数保证 key 平均分布的前提下,那么哈希表的性能就取决于链表的平均长度 (L)。

put : O(L)

​ 先对 key 进行哈希,找到对应的链表,然后遍历链表,判断是添加结点还是更新结点。

get : O(L)

​ 先对 key 进行哈希,找到对应的链表,然后遍历链表,找到对应的结点。

delete : O(L)

​ 先对 key 进行哈希,找到对应的链表,然后遍历链表,删除对应的结点。

如果我们想在常数时间复杂度内, 完成哈希表的增删查操作,那么我们就得控制链表的平均长度不超过某个值。这个值我们称之为加载因子(load factor),也就是链表平均长度可以达到的最大值。

因此,当元素个数达到一定的数目的时候,我们就需要对数组进行扩容(哈希种子也需要重新生成,防止极端情况:所有结点都在一个哈希桶中),然后把所有元素重新映射到哈希表中。

应用

哈希表的应用很广,比如 C++ 中的 unordered_map , unordered_set 和 Java 中的 HashMap, HashSet 底层的数据结构都是哈希表。再比如,常用的缓存中间件 Redis,也大量使用了哈希表数据结构。


http://www.kler.cn/a/547592.html

相关文章:

  • Dubbo 集群策略:负载均衡与容错机制
  • CST无限平面圆孔RCS --- 单站, 多角单频,T求解器(远场或探针),去耦平面
  • SOUI基于Zint生成Code 2 of 5码
  • AI预测气候变化:数据能否揭示未来的风暴?
  • 【若依免登录】登录状态已过期,您可以继续留在该页面,或者重新登录
  • 机试刷题_寻找第K大【python】
  • ElasticSearch基础和使用
  • UniApp 中制作一个横向滚动工具栏
  • 如何在 Mac 上解决 Qt Creator 安装后应用程序无法找到的问题
  • 技术速递|5 分钟用 GitHub Copilot Agent Mode 创建 Copilot Chat 应用
  • js考核第三题
  • 20250214 随笔 Elasticsearch(ES)索引数据 vs. 业务数据库冗余双写
  • 智能AI之隐私安全,尤其是医疗
  • Python - 构造函数、继承与重载
  • firefox的升级
  • 【PyQt】PyQt复选框与单选框
  • 【Linux网络编程】应用层协议HTTP(请求方法,状态码,重定向,cookie,session)
  • 类与对象C++详解(中)-----构造函数与析构函数
  • Unity CommandBuffer绘制粒子系统网格显示
  • 《Mycat核心技术》第14章:实现ER分片