用C语言实现哈希表HashMap

代码仓库地址

1. 功能说明
  • 自定义初始容量和负载因子;
  • 当键值对的个数与容量比值超过负载因子时,自动扩容;
  • 借鉴Java8的HashMap部分扩容逻辑,定义了单独的桶结构体用于记录hash值,以及2倍扩容,减少了hash运算和移位的开销。
2. 实现原理

采用动态数组加链表的方式实现(之后撸完红黑树,增加动态数组+链表+红黑树的版本)。

在这里插入图片描述

3. 定义头文件
#ifndef HASH_MAP_H
#define HASH_MAP_H
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
// 默认桶的个数(动态数组默认大小)
#define DEFAULT_CAPACITY 8
// 默认负载因子
#define DEFAULT_LOAD_FACTOR 0.75typedef char* K;    
typedef char* V;    // 定义hashmap的链表结点
typedef struct map_node {K key;  // 键V value;    // 值struct map_node *next; // 下一个结点
} MapNode;// 定义hashmap的桶结点
typedef struct {MapNode *head; // 桶中链表的第一个节点uint32_t hash_code; // 桶中键值对hash值
} Bucket;// 定义hashmap结构体
typedef struct {Bucket **buckets;  // 存键值对的桶(动态的Bucket指针数组)int size;   // map中键值对个数int capacity; // map的容量double load_factor; // map的负载因子uint32_t hash_seed; // hash函数的种子
} HashMap;// 创建HashMap(容量和负载因子填NULL则使用默认值)
HashMap* create_hashmap(const void* capacity_p, const void* load_factor_p);
// 销毁HashMap
void destroy_hashmap(HashMap *map);
// ---------------- 基本操作 ----------------
// 存入键值对
V put(HashMap *map, K key, V val);
// 查询键值对
V get(const HashMap *map, K key);
// 删除键值对
bool map_remove(HashMap *map, K key);
// 键key在表中是否有对应的值
bool contains(const HashMap *map, K key);
// 判断表是否为空
bool is_empty(const HashMap *map);// ---------------- 其他操作 ----------------
// 是否需要扩容
static bool is_need_resize(const HashMap *map);
// 扩容
static void resize(HashMap *map);
// murmur_hash2 哈希函数
static uint32_t hash(const void* key, int len, uint32_t seed);#endif
4. 具体实现

1. 创建HashMap

需要注意buckets是动态数组,需要手动初始化,并且calloc(capacity, sizeof(Bucket*)注意是Bucket* ,写成Bucket也不会报错,但是会造成更耗内存!

// 创建HashMap(容量和负载因子填NULL则使用默认值)
HashMap* create_hashmap(const void* capacity_p, const void* load_factor_p) {// 1. 创建hashmapHashMap *map = calloc (1, sizeof(HashMap));if (map == NULL) {puts("error:create_hashmap()内分配内存失败");return NULL;}//2. 初始化Bucket动态数组int capacity = capacity_p == NULL ? DEFAULT_CAPACITY : *(int*)capacity_p;Bucket **buckets = calloc(capacity, sizeof(Bucket*));if (map == NULL) {puts("error:create_hashmap()内分配内存失败");free(map);return NULL;}map->buckets = buckets;// 3. 初始化hashmap的其他属性map->capacity = capacity;double load_factor = load_factor_p == NULL ? DEFAULT_LOAD_FACTOR : *(double*)load_factor_p;map->load_factor = load_factor;map->hash_seed = time(NULL);return map;
}

2. 销毁HashMap

要注意销毁桶,避免造成内存泄漏!

void destroy_hashmap(HashMap *map) {if (map == NULL) {puts("error:destroy_hashmap()的参数map为NULL");exit(-1);}// 1. 销毁链表结点for (int i = 0; i < map->capacity; i++) {Bucket *cur_bucket = map->buckets[i];if (cur_bucket != NULL) {MapNode *cur = cur_bucket->head;while(cur != NULL) {MapNode *temp = cur->next;free(cur);cur = temp;}// 2. 销毁桶free(cur_bucket);}}// 3. 销毁mapfree(map);
}

3. 存入键值对

在存入时,可以先判断一下key值在桶中是否存在,然后再进行扩容,这样对与重复键值对的存储速度有一定的提升。

V put(HashMap *map, K key, V val) {if (map == NULL) {puts("error:put()的参数map为NULL");exit(-1);}// 1. 计算key的hash值以及桶的位置idxuint32_t hash_code = hash(key, strlen(key), map->hash_seed);int idx = hash_code % map->capacity;// 2. 判断idx位置的桶是否存在if (map->buckets[idx] != NULL) {// 2-1. idx位置的桶已存在,则判断idx桶中是否存在key值MapNode *cur = map->buckets[idx]->head;while (cur != NULL) {if (strcmp(cur->key, key) == 0) {// 2-1-1. 若idx桶中已存在key值则更新value值,并返回旧value值V old_val = cur->value;cur->value = val;return old_val;}cur = cur->next;}}// 3. 判断是否需要扩容if (is_need_resize(map)) {// 3-1. 若需扩容,则扩容,并重新计算key的hash值和桶位置idxresize(map);hash_code = hash(key, strlen(key), map->hash_seed);idx = hash_code % map->capacity;}// 4. 重新判断idx位置的桶是否存在(在‘2.’虽然判断了一次,但有可能新的idx位置桶还未创建过if (map->buckets[idx] == NULL) {// 4-1. idx位置的桶不存在,则在idx位置创建桶,并在桶中存入hash值Bucket *bucket = calloc(1, sizeof(Bucket));if (bucket == NULL) {puts("error:put()内分配内存失败");exit(-1);}bucket->hash_code = hash_code;map->buckets[idx] = bucket;}// 5. 在桶中插入这键值对(用头插 O(1))MapNode *new_node = calloc(1, sizeof(MapNode));if (new_node == NULL) {puts("error:put()内分配内存失败");exit(-1);}new_node->key = key;new_node->value = val;new_node->next = map->buckets[idx]->head;map->buckets[idx]->head = new_node;// 6. 更新size map->size++;return NULL;
}

4. 查询键值对

注意要对桶进行判空,不能直接map->buckets[idx]->head

V get(const HashMap *map, K key) {if (map == NULL) {puts("error:get()的参数map为NULL");exit(-1);}// 1. 计算key的hash值应存放的桶的位置idxint idx = hash(key, strlen(key), map->hash_seed) % map->capacity;// 2. 在idx位置的桶中搜索对应key值if (map->buckets[idx] != NULL) { // 先判断桶是否为空MapNode *cur = map->buckets[idx]->head;while(cur != NULL) {if (strcmp(cur->key, key) == 0) {return cur->value;}cur = cur->next;}}return NULL;
}

5. 删除键值对

注意删除的结点是否是第一个节点,要分情况处理。

bool map_remove(HashMap *map, K key) {if (map == NULL) {puts("error:map_remove()的参数map为NULL");exit(-1);}// 1. 计算key的hash值应存放的桶的位置idxint idx = hash(key, strlen(key), map->hash_seed) % map->capacity;// 2. 在idx位置的桶中搜索对应key值if (map->buckets[idx] != NULL) { // 先判断桶是否为空MapNode *pre = NULL;MapNode *cur = map->buckets[idx]->head;while(cur != NULL) {if (strcmp(cur->key, key) == 0) {// 3. 删除结点if (pre == NULL) { // 删除的是第一个结点map->buckets[idx]->head = cur->next;}else {pre->next = cur->next;}// 4. 释放内存free(cur);// 5. 更新size map->size--;return true;}pre = cur;cur = cur->next;}}return false;
}

6. 扩容

注意扩容机制是采用每次库容2倍,这样每次新下标就是old_idx或者为old_idx+(new_cap-old_cap),不让会出现多个旧桶在新数组里下标冲突被覆盖抹除的问题!!!

static void resize(HashMap *map) {// 1. 创建新的桶数组int old_cap = map->capacity;// 每次扩容2倍,好处是,每次新下标就是old_idx或者为old_idx+(new_cap-old_cap)!!!(详见Java8hashmap扩容)int new_cap = old_cap << 1; Bucket **new_buckets = calloc(new_cap, sizeof(Bucket*));// 2. 挪动旧桶中的数据到新桶中Bucket **old_buckets = map->buckets;for (int idx = 0; idx < old_cap; idx++) {Bucket *cur = old_buckets[idx];if (cur != NULL) { // 只操作非空桶// 2-1. 计算新的位置idxint new_idx = cur->hash_code % new_cap;// 2-2. 挪动桶new_buckets[new_idx] = old_buckets[idx];} }// 3. 将新桶交给hashmapmap->buckets = new_buckets;// 4. 更新hashmap的容量map->capacity = new_cap;// 5. 将就旧桶销毁free(old_buckets);
}

7. 其他函数

剩下的几个函数简单不易出错,此处不再赘述,其中hash函数使用的开源的murmur_hush2详见开头处github代码仓库。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python二级:二叉树问题求解

一、题源 在Python二级考试中前10道基础题是必考题&#xff0c;虽然没有什么卵用&#xff0c;但是你得分不达标&#xff0c;还不让你过&#xff0c;没有办法只好硬着头皮去刷题了。这10道题中有一个二叉树题比较难&#xff0c;现摘录如下&#xff0c;同时给出gpt-4的解答&…

ruoyi后台管理系统部署-3-安装redis

centos7安装redis 1. yum 安装 查看是否安装了redis yum installed list | grep redis ps -ef | grep redis安装epel 仓库&#xff08;仓库是软件包下载的&#xff0c;类似maven&#xff0c;nuget&#xff09; yum install epel-release搜索 redis 包 yum search redis安装…

逸学Docker【java工程师基础】1.认识docker并且安装

场景问题 在实际开发过程中我们有这样的场景问题 在开发阶段的环境配置到了其他人项目人员那里就不能运行了&#xff0c;尽管配置规格相同&#xff0c;但是在较多的不同的环境情况下还是可能会有错误。 开发&#xff1a;程序员&#xff1a;你那边可以运行了吗 测试&#xf…

爬虫补环境jsdom、proxy、Selenium案例:某条

声明&#xff1a; 该文章为学习使用&#xff0c;严禁用于商业用途和非法用途&#xff0c;违者后果自负&#xff0c;由此产生的一切后果均与作者无关 一、简介 爬虫逆向补环境的目的是为了模拟正常用户的行为&#xff0c;使爬虫看起来更像是一个真实的用户在浏览网站。这样可以…

前端基础知识整理汇总(下)

react 生命周期 React v16.0前的生命周期 初始化(initialization)阶段 此阶段只有一个生命周期方法&#xff1a;constructor。 constructor() 用来做一些组件的初始化工作&#xff0c;如定义this.state的初始内容。如果不初始化 state 或不进行方法绑定&#xff0c;则不需…

编程艺术之Unix哲学

Unix 哲学不算是一种正规设计方法&#xff0c;它并不打算从计算机科学的理论高度来产生理论上完美的软件。那些毫无动力、松松垮垮而且薪水微薄的程序员们&#xff0c;能在短短期限内&#xff0c;如神灵附体般开发出稳定而新颖的软件——这只不过是经理人永远的梦呓罢了。 1 Un…

isis实验

根据要求制作大概&#xff1a; 使用isis配置路由器&#xff1a; 配置好物理接口地址后配置isis 为实现r1访问r5的环回走r6,需要在r6上制作路由泄露&#xff1a; 在r5上产生r1的路由明细&#xff1a; 全网可达&#xff1a;

华为 HarmonyOS 页面跳转

HarmonyOS 页面跳转 1.新建页面2.添加跳转方法3.实现跳转效果 1.新建页面 我们新建2个页面(page)&#xff0c;一个Hello World页面&#xff0c;一个Hello HarmonyOS页面&#xff0c;注意修改红色框内容&#xff0c;保持一致 2.添加跳转方法 导入导入router模块&#xff0c;页…

Rust-内存安全

堆和栈 一个进程在执行的时候&#xff0c;它所占用的内存的虚拟地址空间一般被分割成好几个区域&#xff0c;我们称为“段”(Segment)。常见的几个段如下。 代码段。编译后的机器码存在的区域。一般这个段是只读的。bss段。存放未初始化的全局变量和静态变量的区域。数据段。…

MATLAB Deep learning

文章目录 Chapter 1: Machine Learning存在的问题过拟合Overfitting解决过拟合 regularization and validationregularization 正则化validation 验证 机器学习的类型有监督学习分类Classification回归Regression 无监督学习聚类 强化学习 Chapter 2: Neural NetworkChapter 3:…

(BUUCTF)ycb_2020_easy_heap (glibc2.31的off-by-null + orw)

文章目录 前置知识整体思路高版本的off-by-nullorw exp 前置知识 未初始化内存导致的地址泄露 高版本下的off-by-null利用 glibc2.31下的orw做法 整体思路 非常综合的一道题目&#xff0c;和ciscn之前做过的一道silverwolf很相似&#xff0c;本道题目的glibc2.31的环境也让…

Django教程第6章 | web开发实战-文件上传(导入文件、上传图片)

专栏系列&#xff1a;Django学习教程 导入文件 目标&#xff1a;导入部门清单excel&#xff0c;解析excel数据存储到数据库。 1.准备要导入的excel文件 2.编写模板HTML <div class"panel panel-default"><!-- Default panel contents --><div class…

Embedding Watermarks into Deep Neural Networks

将水印嵌入深度神经网络 ABSTRACT 最近在深度神经网络领域取得了显著的进展。分享深度神经网络的训练模型对于这些系统的快速研究课并发进展至关重要。与此同时&#xff0c;保护共享训练模型的权利也变得十分必要。为此我们提议使用数字水印技术来保护知识产权&#xff0c;并…

vue3-模板引用

//1.调用ref函数 -> ref对象 const h1Ref ref(null) const comRef ref(null) //组件挂载完毕之后才能获取 onMounted(()>{console.log(h1Ref.value);console.log(comRef.value); })<div class"father"><!-- 通过ref标识绑定ref对象 --><h2 re…

【工具栏】SequenceDiagram插件的使用(根据代码生成时序图)

1. 安装 2.使用 进入代码页面&#xff0c;点击鼠标右键 选择方法 根据方法中的代码生成时序图

2024年【山东省安全员C证】考试及山东省安全员C证复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 山东省安全员C证考试是安全生产模拟考试一点通总题库中生成的一套山东省安全员C证复审考试&#xff0c;安全生产模拟考试一点通上山东省安全员C证作业手机同步练习。2024年【山东省安全员C证】考试及山东省安全员C证复…

docker部署私人云盘

目录 1.安装 2.登陆 1.安装 mkdir -p /opt/alist docker run -d --restartalways -v /opt/alist:/opt/alist/data -p 5244:5244 --name"alist" xhofe/alist:latest 2.登陆 ip:5224 默认账户admin 密码在日志中

test-04-test case generate 测试用例生成 tcases 快速开始

拓展阅读 junit5 系列 基于 junit5 实现 junitperf 源码分析 Auto generate mock data for java test.(便于 Java 测试自动生成对象信息) Junit performance rely on junit5 and jdk8.(java 性能测试框架。性能测试。压测。测试报告生成。) 自动生成测试用例 入门指南 关于…

js中关于字符串的创建和判断类型

文章目录 创建方法判断类型的技巧区分1、typeof2、instanceof 共点1、Object.prototype.toString.call2、库函数 参考链接&#xff1a;JS字符串的创建和常用方法 如何判断JS中一个变量是 string 类型 创建方法 字符串有着两种的创建方法&#xff0c;一个是使用构造函数&#x…

openssl3.2 - 官方demo学习 - server-arg.c

文章目录 openssl3.2 - 官方demo学习 - server-arg.c概述笔记备注END openssl3.2 - 官方demo学习 - server-arg.c 概述 TLS服务器, 等客户端来连接; 如果客户端断开了, 通过释放bio来释放客户端socket, 然后继续通过bio读来aceept. 笔记 对于开源工程, 不可能有作者那么熟悉…