用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.75

typedef 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. 创建hashmap
    HashMap *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. 销毁map
    free(map);
}

3. 存入键值对

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

V put(HashMap *map, K key, V val) {
    if (map == NULL) {
        puts("error:put()的参数map为NULL");
        exit(-1);
    }
    // 1. 计算key的hash值以及桶的位置idx
    uint32_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值和桶位置idx
        resize(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值应存放的桶的位置idx
    int 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值应存放的桶的位置idx
    int 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. 计算新的位置idx
            int new_idx = cur->hash_code % new_cap;
            // 2-2. 挪动桶
            new_buckets[new_idx] = old_buckets[idx];
        } 
    }
    // 3. 将新桶交给hashmap
    map->buckets = new_buckets;
    // 4. 更新hashmap的容量
    map->capacity = new_cap;
    // 5. 将就旧桶销毁
    free(old_buckets);
}

7. 其他函数

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

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

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

相关文章

K8S--service

一、简介 Service 是将集群中的 一个或一组 Pod应用程序公开为网络服务的方法。我们都知道pod是不稳定的,有可能时时刻刻都在创建和销毁,这一时刻运行的 Pod 集合可能不同于下一刻运行该应用的 Pod 集合,并且新创建的pod的ip地址会改变,所以我们不应该寄期望于pod的稳定性…

Calibre DESIGNrev Object Selection Toolbar

包括 Reference Path Polygon Edge Vertex Text的解释说明 FieldDescription用法&#xff08;勾选后&#xff09;ReferenceUsed to move or select a cell reference or array reference.可以选择一个cellPathUsed to move or select a contiguous path object.暂时不明请指教…

浏览器进程模型和JS的事件循环

一、浏览器的进程模型 1、什么是进程&#xff1f; 程序运行所需要的专属内存空间 2、什么是线程&#xff1f; ​​​​​运行​代码的称为线程&#xff08;同一个进程中的线程共享进程的资源&#xff09; ⼀个进程⾄少有⼀个线程&#xff0c;所以在进程开启后会⾃动创建⼀个线…

k8s云原生环境搭建笔记——第二篇

目录 1、使用普通方式安装prometheus和grafana1.1、安装kube-state-metrics容器1.1.1、下载并修改yaml文件1.1.2、导入kube-state-metrics镜像1.1.3、执行yaml文件目录 1.2、安装node-exploer1.2.1、创建名称空间prometheus1.2.2、执行yaml 1.3、安装prometheus1.3.1、创建集群…

【JUC进阶】14. TransmittableThreadLocal

目录 1、前言 2、TransmittableThreadLocal 2.1、使用场景 2.2、基本使用 3、实现原理 4、小结 1、前言 书接上回《【JUC进阶】13. InheritableThreadLocal》&#xff0c;提到了InheritableThreadLocal虽然能进行父子线程的值传递&#xff0c;但是如果在线程池中&#x…

e2studio开发三轴加速度计LIS2DW12(4)----测量倾斜度

e2studio开发三轴加速度计LIS2DW12.4--测量倾斜度 概述视频教学样品申请源码下载计算倾斜角度工作原理单轴倾斜检测双轴倾斜检测三轴倾斜检测通信模式管脚定义IIC通信模式速率新建工程工程模板保存工程路径芯片配置工程模板选择时钟设置UART配置UART属性配置设置e2studio堆栈e…

自编C++题目——输入程序

预估难度 简单 题目描述 小明编了一个输入程序&#xff0c;当用户的输入之中有<时&#xff0c;光标移动到最右边&#xff1b;当输入有>时&#xff0c;光标移动到最左边&#xff0c;当输入有^时&#xff0c;光标移动到前一个字符&#xff0c;当输入为#时&#xff0c;清…

大模型实战营Day4 XTuner 大模型单卡低成本微调实战 作业

按照文档操作&#xff1a; 单卡跑完训练&#xff1a; 按照要求更改微调的数据&#xff1a; 完成微调数据的脚本生成&#xff1a; 修改配置文件&#xff1a; 替换好文件后启动&#xff1a; 启动后终端如图&#xff1a; 用于微调的一些数据显示&#xff1a; 训练时间&#x…

记录一次git merge后发现有些文件不对的问题,排查过程

分支进行merge&#xff08;A merge到B&#xff09;之后&#xff0c;发现string.xml中有些字段的值没有merge过来&#xff0c;一开始还以为自己是自己merge错误&#xff0c;检查了一遍自己的merge操作没有问题。 那为啥没有merge过来呢&#xff1f;有一种可能是&#xff0c;merg…

vue2实现日历12个月平铺,显示工作日休息日

参考&#xff1a;https://blog.csdn.net/weixin_40292154/article/details/125312368 1.组件DateCalendar.vue&#xff0c;sass改为less <template><div class"cc-calendar"><div class"calendar-title"><span>{{ year }}年{{ mo…

【Linux Shell】5. 运算符

文章目录 【 1. 算术运算符 】1.1 expr 命令1.2 [ ] 方括号 【 2. 关系运算符 】【 3. 布尔运算符 】【 4. 逻辑运算符 】【 5. 字符串运算符 】【 6. 文件测试运算符 】 【 1. 算术运算符 】 运算符说明举例赋值a$b 把变量 b 的值赋给 a。 1.1 expr 命令 原生 bash 不支持简…

SDRAM小项目——写模块

写模块跟着视频看了一个多星期&#xff0c;一开始始终有点弄不清楚&#xff0c;现在记录一下理解的过程。 阅读文档信息&#xff1a; 首先阅读文档信息&#xff0c;了解SDRAM写过程的状态转换和时序图 SDRAM整体状态流程如图所示&#xff1a; 在SDRAM整体系统中&#xff0c…

数据结构之bool类

bool类 bool 是布尔类。它是最简单的一个类&#xff0c;其取值有两种&#xff0c;1和O&#xff0c;即 True 和 False。可以这样简单地理解&#xff0c;除了1和0以及 True 和 False 的情况之外&#xff0c;但凡有值&#xff08;非空&#xff09;即为真&#xff0c;但凡无值&…

nodemon(自动重启 node 应用程序)的安装与使用

1、安装&#xff0c;在随意一个命令窗口都可以 我们可以执行安装选项 -g 进行全局安装 npm i -g nodemon 全局安装完成之后就可以在命令行的任何位置运行 nodemon 命令 该命令的作用是 自动重启 node 应用程序 2、使用&#xff1a; 可能报错如下 windows 默认不允许 npm …

【数据结构 | 直接插入排序】

直接插入排序 基本思路直接插入排序SelectSort 基本思路 扑克牌是我们几乎每个人都可能玩过的游戏最基本的扑克玩法都是—边模牌,— 边理牌。加入我们拿到如图这样的扑克牌&#xff1a; 我们会按照如下理牌&#xff1a; 将3和4移动至5的左侧&#xff0c;再将2移动到最左侧&a…

顺序表的实现(上)(C语言)

本文章主要对顺序表的介绍以及数据结构的定义,以及几道相关例题,帮助大家更好理解顺序表. 文章目录 前言 一、顺序表的静态实现 二、顺序表的动态实现 三.定义打印顺序表函数 四.定义动态增加顺序表长度函数 五.创建顺序表并初始化 六.顺序表的按位查找 七.顺序表的按值…

网络爬虫丨基于scrapy+mysql爬取博客信息并保存到数据库中

文章目录 写在前面实验描述实验框架实验需求 实验内容1.安装依赖库2.创建Scrapy项目3.配置系统设置4.配置管道文件5.连接数据库6.分析要爬取的内容7.编写爬虫文件 运行结果写在后面 写在前面 本期内容&#xff1a;基于scrapymysql爬取博客信息并保存到数据库中 实验需求 ana…

java并发编程

一、java线程 1.三种创建线程的方式 Integer sum futureTask.get(); 会等待其对应的线程执行完 &#xff0c;即阻塞 再获得结果。 所以我在测试时&#xff0c;出现一个小插曲 Slf4j public class ThreeWays {//1.自定义MyThread进行继承Threadstatic void test001(){Thread t…

HCIA-Datacom题库(自己整理分类的)_09_Telnet协议【14道题】

一、单选 1.某公司网络管理员希望能够远程管理分支机构的网络设备&#xff0c;则下面哪个协议会被用到&#xff1f; RSTP CIDR Telnet VLSM 2.以下哪种远程登录方式最安全&#xff1f; Telnet Stelnet v100 Stelnet v2 Stelnet v1 解析&#xff1a; Telnet 明文传输…

DAY7--learning english

一、积累 1.instinct Bro showed me his primal instinct 老兄给我展示他原始的本能&#xff08;返祖现象&#xff09;. 2. assembly Todays assembly is about part of journey. 今天的集会是讲述关于旅程的一部分。 3.aluminum Aluminum Casting Motocycle Engine Cover. …