哈希表(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,也大量使用了哈希表数据结构。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/971805.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java 富文本编辑器

所有桌面工具包都提供文本编辑控件&#xff0c;范围从最基础的到更高级的选项。但富文本编辑呢&#xff1f;是否有控件允许用户格式化文本并添加图片&#xff1f;有没有可以在 Java 应用程序中使用的 WYSIWYG 编辑器&#xff1f; 在本文中&#xff0c;我们将探讨如何使用 JxBr…

几道常见的链表算法题

1. 两数相加 题目描述 Leetcode:给定两个非空链表来表示两个非负整数。位数按照逆序方式存储&#xff0c;它们的每个节点只存储单个数字。将两数相加返回一个新的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数字都不会以零开头。 示例&#xff1a; 输入&#xff1a;…

STM32——HAL库开发笔记20(定时器1—时基单元)(参考来源:b站铁头山羊)

一、定义 单片机中的定时器&#xff08;Timer&#xff09;是一a个非常重要的外设模块&#xff0c;用于生成精确的时间延迟、测量时间间隔、产生PWM信号等。定时器的核心是一个计数器&#xff0c;它通过对时钟信号进行计数来实现时间相关的功能。 时钟树为定时器提供时钟信号&…

字符串操作总结(C# and Lua)

C# 获取字符串长度 字符串拼接 替换字符串 分割字符串 截取字符串 判断子串是否存在 转大写字母 转小写字母 剔除空白符 首尾匹配 查找索引 插入字符串 Lua 声明字符串 获取字符串长度 例子 &#xff1a;local str "123abc我" 长度 #str 9 string.len(str) …

ComfyUI的安装

ComfyUI的安装 1、访问官网进行下载 https://github.com/comfyanonymous/ComfyUI 2、下载完成后解压、双击运行run_nvidia_gpu.bat 3、进入主页面&#xff0c;可以开始玩耍了

新数据结构(11)——Java类的产生和反射

反射是获取类信息的一种能力 类信息包括属性、方法、构造器、父类、接口等 类信息的来源 来自类的加载器&#xff0c;这是从.class文件到内存中的java虚拟器&#xff08;JVM&#xff09;中间的一个阶段&#xff08;如下图&#xff09; 类的加载器里&#xff0c;用Field数组存…

DeepSeek横空出世,真的拯救了算力焦虑吗?

目录 DeepSeek横空出世&#xff0c;真的拯救了算力焦虑吗&#xff1f; 一、为什么会有算力焦虑 二、来自硅谷四大科技巨头的决策 1、Deepseek在24年底的突然崛起 2、利好算力的大背景下&#xff0c;硅谷四大科技巨头的“落后”加码 3、在算法博弈中加强算力基建的战略 三…

VSCode Error Lens插件介绍(代码静态检查与提示工具)(vscode插件)

文章目录 VSCode Error Lens 插件介绍**功能概述****开发背景****使用方法****适用场景** VSCode Error Lens 插件介绍 功能概述 Error Lens 是一款增强 VS Code 错误提示的扩展工具&#xff0c;通过 内联显示错误和警告信息&#xff0c;直接定位代码问题&#xff0c;提升开发…

网络安全攻防演练——RT实战技巧篇

前言 又是一年hw招聘季&#xff0c;每年hw行动都会吸引大量网络安全从业者参加。同时也会有很多热爱网络安全但无从下手的网安爱好者参与。笔者旨在对网络安全有想法但是没有方向的师傅做一个简单的方向的了解&#xff0c;让师傅有方向去学习。 RT(红队) 1.引入 首先红队的…

[Android] 【汽车OBD软件】Torque Pro (OBD 2 Car)

[Android] 【汽车OBD软件】Torque Pro &#xff08;OBD 2 & Car&#xff09; 链接&#xff1a;https://pan.xunlei.com/s/VOIyKOKHBR-2XTUy6oy9A91yA1?pwdm5jm# 获取 OBD 故障代码、汽车性能数据等等。Torque 使用连接到您的 OBD2 发动机管理/ECU 的 OBD II 蓝牙适配器。…

拯救者电脑在重装系统之后电源计划丢失Fn+Q切换不了模式怎么恢复?

参考联想知识库的一下链接&#xff1a; https://iknow.lenovo.com.cn/detail/196192 其中下载的解压文件后的文件需要复制粘贴到D盘的根目录下&#xff0c;再来运行文件。若在生成的log文件中看到导入成功以及控制面板中看到已添加的电源计划即可 如果还是无效可是试试以下的…

sql盲注脚本

在sqli-labs中的第8题无回显可以尝试盲注的手法获取数据 发现页面加载了3秒左右可以进行盲注 布尔盲注数据库名 import requestsdef inject_database(url):datanamefor i in range(1,15):low 32high 128mid (low high) // 2while low < high:path "id1 and asci…

Linux下学【MySQL】常用函数助你成为数据库大师~(配sql+实操图+案例巩固 通俗易懂版~)

绪论​ 每日激励&#xff1a;“唯有努力&#xff0c;才能进步” 绪论​&#xff1a; 本章是MySQL中常见的函数&#xff0c;利用好函数能很大的帮助我们提高MySQL使用效率&#xff0c;也能很好处理一些情况&#xff0c;如字符串的拼接&#xff0c;字符串的获取&#xff0c;进制…

Linux(centos)系统安装部署MySQL8.0数据库(GLIBC版本)

安装前检查服务器glibc版本&#xff0c;下载对应版本包 rpm -qa | grep glibc mysql安装包及依赖包已整理好&#xff0c;下载地址&#xff1a;https://pan.quark.cn/s/3137acc814c0&#xff0c;下载即可安装 一、下载MySQL mysql安装包及依赖包已整理好&#xff0c;下载地址…

Java基于 SpringBoot+Vue的微信小程序跑腿平台V2.0(附源码,文档)

博主介绍&#xff1a;✌Java徐师兄、7年大厂程序员经历。全网粉丝13w、csdn博客专家、掘金/华为云等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb; 不…

git版本控制工具介绍

版本控制 版本控制是软件开发过程中用于管理代码变更的重要手段&#xff0c;它可以记录代码的历史版本&#xff0c;方便开发者进行回溯、协作和问题排查。本地版本控制、集中版本控制和分布式版本控制是三种不同的版本控制模式 本地版本控制 本地版本控制是最基础的版本控制方…

深入解析 vLLM:高性能 LLM 服务框架的架构之美(二)调度管理

深入解析 vLLM&#xff1a;高性能 LLM 服务框架的架构之美&#xff08;一&#xff09;原理与解析 深入解析 vLLM&#xff1a;高性能 LLM 服务框架的架构之美&#xff08;二&#xff09;调度管理 1. vLLM 调度器结构与主要组件 在 vLLM 中&#xff0c;调度器的结构设计围绕任务…

重构测试项目为spring+springMVC+Mybatis框架

重构测试项目为springspringMVCMybatis框架 背景 继上次将自动化测试时的医药管理信息系统项目用idea运行成功后&#xff0c;由于项目结构有些乱&#xff0c;一部分代码好像也重复&#xff0c;于是打算重新重构以下该项目&#xff0c;这次先使用springspringMVCMybatis框架 …

RAGFlow

相关链接 ragflow.io 官网 github 相关术语 RAG “Retrieval-Augmented Generation”&#xff08;RAG&#xff09;是一种结合了检索&#xff08;Retrieval&#xff09;和生成&#xff08;Generation&#xff09;的深度学习模型架构。这种模型通常用于处理自然语言处理&…

java断点调试(debug)

在开发中&#xff0c;新手程序员在查找错误时, 这时老程序员就会温馨提示&#xff0c;可以用断点调试&#xff0c;一步一步的看源码执行的过程&#xff0c;从而发现错误所在。 重要提示: 断点调试过程是运行状态&#xff0c;是以对象的运行类型来执行的 断点调试介绍 断点调试是…