用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,一经查实,立即删除!

相关文章

JDK17

JDK 17是Java开发工具包&#xff08;Java Development Kit&#xff09;的一个版本。JDK是用于开发和运行Java应用程序的软件包&#xff0c;它包含了编译器、调试器、运行时环境和其他一些实用工具。JDK 17是Java的最新版本&#xff0c;它提供了许多新的功能、增强和改进。 使用…

Python二级:二叉树问题求解

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

在 Centos 7.9 中,安装与配置 Docker 20.10.18

1. 检测内核版本 在使用Docker之前&#xff0c;首先需要确保系统内核版本达到3.10以上。可以通过以下命令检查&#xff1a; uname -r2. 升级内核及软件包 为了确保系统的稳定性和安全性&#xff0c;建议在安装Docker之前先升级系统内核和相关软件包&#xff1a; yum -y upd…

zustand状态管理工具(react)

分别创建文件continue.js、shoes.js 1、continue.js import create from zustand import { persist } from zustand/middlewareexport default create(persist((set) > ({counter: 12,incrementer: () > set((state) > ({ counter: state.counter 1 })),decrementer…

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…

Leetcode 3008. Find Beautiful Indices in the Given Array II

Leetcode 3008. Find Beautiful Indices in the Given Array II 1. 解题思路2. 代码实现 题目链接&#xff1a;3008. Find Beautiful Indices in the Given Array II 1. 解题思路 这一题其实算是套路题了&#xff0c;知道的话就挺快的&#xff0c;不知道的话就会很难了…… …

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的环境也让…

如何使用設置靜態住宅IP

靜態住宅IP就是一種靜態的、分配給住宅用戶的IP地址。與動態IP地址不同&#xff0c;靜態住宅IP一旦分配給用戶&#xff0c;就會一直保持不變&#xff0c;除非ISP&#xff08;Internet Service Provider&#xff0c;互聯網服務提供商&#xff09;進行手動更改。那麼&#xff0c;…

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…

【angular教程240111】08异步数据流编程与angular :promise,Rxjs6.x

【angular教程240111】08异步数据流编程与angular &#xff1a;promise&#xff0c;Rxjs6.x 目录标题 一级目录二级目录三级目录 〇、编程里的异步1异步编程常见的几种方法2 代码示例其中的一些方法&#xff1a;1. 回调函数&#xff08;Callback Functions&#xff09;2. 事件监…