数据结构-LRU缓存(C语言实现)
遇到困难,不必慌张,正是成长的时候,耐心一点!
目录
- 前言
- 一、题目介绍
- 二、实现过程
- 2.1 实现原理
- 2.2 实现思路
- 2.2.1 双向链表
- 2.2.2 散列表
- 2.3 代码实现
- 2.3.1 结构定义
- 2.3.2 双向链表操作实现
- 2.3.3 实现散列表的操作
- 2.3.4 内存释放代码
- 2.3.5 题目代码实现
- 总结
前言
本篇文章主要是为了记录实现LRU缓存的方法和思考的过程。
一、题目介绍
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;
如果不存在,则向缓存中插入该组 key-value 。
如果插入操作导致关键字数量超过 capacity ,则应该逐出最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
示例:
输入
[“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”, “get”, “get”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
提示:
1 <= capacity <= 3000
0 <= key <= 10000
0 <= value <= 105
最多调用 2 * 105 次 get 和 put
下面是本人的一些废话,不感兴趣可直接看实现过程
看完题目,看到函数 get 和 put 必须以 O(1) 的平均时间复杂度运行,第一反应是顺序存储的随机存取才可以实现O(1)的时间复杂度,也就是说一定会有一块连续的存储空间存储数据,且大小为capacity。可以把key对应连续的存储空间的下标,但是看到提示里面的key的范围超出了capacity的范围,那如何在让key在[0,capacity]循环呢?脑子直接想到了循环队列的取余法,因为最近用循环队列比较频繁。
但是,经过取余后,还是会造成出现重复的key,该怎们解决呢?突然想到数据结构里面的散列表的碰撞的处理,立马去看关于散列表的介绍,以前没学的东西,现在又冒出来找我了。
看了以后,觉得很神奇,原来取余法是散列函数的一种,并使用频繁的一种。然后又看了关于碰撞的处理,书上介绍两种,第一种叫开地址法,第二种方法叫拉链法。
解决了碰撞问题,那么如何实现最近最少使用,一想到这是关于链表的题目,慢慢想到了循环单链表,头插法实现最近使用,而尾结点一定是最少使用,也就是当缓存空间达到capacity时,需要删除的。但是写了一半代码,发现当访问结点为尾结点时,需要更改尾结点,也就是需要尾结点的前驱。我知道,在单链表中,寻找某个结点前驱时间复杂度是O(n),不符合题意,立马把代码删除。
经过思考,心情里变得比较烦躁,但又不想看题解,因为想着现在正是考验我的时候,想着这道题一定想要教会我什么。尝试让自己变得冷静,不断地翻开数据结构这本书,看到双向链表,哎,这不就是为了解决以O(1)时间复杂度访问某个结点前驱的问题嘛!为什么没有马上想到,是因为平时做的题目都是单链表,双向链表用的太少了…
以上问题都解决了后,刚开始使用开地址法的线性探查法解决碰撞时,发现最后几个测试用例超时了,但是,说明思路是对的,因为线性探访的最坏情况的时间复杂度就是O(n)。
然后改为使用了拉链法,写代码用的时间不多,调试用了很多时间,最终,在不放弃的情况下,终于找到了代码的某处错误。真是太不容易了,因为常规测试用例通过了,在一些复杂的测试用例没通过,又无法一步一步的调试,只能不断地阅读代码,最后发现是在某个很隐秘且常规测试用例很难覆盖的地方,我当场麻了…
所幸,最后还是一步一步的写出来了,还是非常开心的,感觉时间花的太值了!
二、实现过程
2.1 实现原理
实现原理:散列表+双向链表
散列表解决了key重复问题,并解决函数 get 和 put 必须以 O(1) 的平均时间复杂度运行的问题
双向链表解决了最近使用和最少使用的问题,头插法解决最近使用,尾结点解决了最少使用
结构图如图2.1所示:

2.2 实现思路
2.2.1 双向链表
为了方便双向链表的插入和删除操作,可以使用两个辅助结点,一个伪头部和一个伪尾部,实现了每个真实结点都有前驱和后继

2.2.2 散列表
这里主要想介绍解决碰撞问题的拉链法。
设散列表的大小为m,使用拉链法需要建立m条链表,所有散列地址相同的元素放在同一条链表中,如果某个地址中没有存放任何元素,则对应的链表为空链表。设关键码key,根据散列函数h计算出h(key),即可确定第h(key)条链表,然后在该链表进行插入和删除及检索操作。
在本题中,散列函数为取余法,散列表的大小为capacity
h ( k e y ) = k e y % c a p a c i t y h(key) = key \,\%\, capacity h(key)=key%capacity
在本题中, h a s h K e y = h ( k e y ) , h a s h V a l u e = h a s h T a b l e [ h a s h K e y ] hashKey = h(key), hashValue = hashTable[hashKey] hashKey=h(key),hashValue=hashTable[hashKey]
如下图所示

2.3 代码实现
本篇文章的代码使用C语言实现
2.3.1 结构定义
//双向链表结点
struct DoubleNode
{int key; //真实的keyint value;struct DoubleNode* llink; //双向链表的前驱struct DoubleNode* rlink; //双向链表的后继
};//双向链表类型
//为了方便操作,使用两个伪结点
struct DoubleList
{struct DoubleNode* dummyHead; //双向链表的伪头部struct DoubleNode* dummyRear; //双向链表的伪尾部
};//哈希结点的定义
//相同hashKey构成的链表的结点类型
struct HashNode
{struct DoubleNode* address; //指向双向链表的某个结点struct HashNode* next;
};//使用双向链表
//保存双向链表的头结点
//散列函数 取余法
//解决地址碰撞 拉链法
typedef struct
{struct DoubleList* doubleList; //双向链表struct HashNode** hashTable; //哈希表int capacity; //缓存空间大小int curCapacty; //已用空间
} LRUCache;
2.3.2 双向链表操作实现
//双向链表的操作
//初始化双向链表
void initDoubleList(struct DoubleList* doubleList)
{doubleList->dummyHead = (struct DoubleNode*)calloc(1, sizeof(struct DoubleNode)); //初始化双向链表的伪头部doubleList->dummyRear = (struct DoubleNode*)calloc(1, sizeof(struct DoubleNode)); //初始化双向链表的伪尾部//头和尾互连doubleList->dummyHead->rlink = doubleList->dummyRear; doubleList->dummyRear->llink = doubleList->dummyHead;
}//将某个结点向双向链表中的第一个结点前执行插入操作
void insertNodeToDoubleListFirst(struct DoubleList* doubleList, struct DoubleNode* node)
{node->rlink = doubleList->dummyHead->rlink;node->llink = doubleList->dummyHead;doubleList->dummyHead->rlink->llink = node;doubleList->dummyHead->rlink = node;
}//将node结点移动到双向链表的第一个结点
void moveNodeToHead(struct DoubleList* doubleList, struct DoubleNode* node)
{ //从双向链表中断开node->llink->rlink = node->rlink;node->rlink->llink = node->llink;//将断开的结点重新插入到双向链表的伪头部后insertNodeToDoubleListFirst(doubleList, node);
}
2.3.3 实现散列表的操作
//哈希表的操作
//散列函数
//取余法
int hashFunc(int key, int m)
{return key % m;
}//在hashTable查看对应的hashKey的结点是否指向已存在的key
struct HashNode* getHashNode(struct HashNode** hashTable, int hashKey, int key)
{for (struct HashNode* hashValue = hashTable[hashKey]; hashValue != NULL; hashValue = hashValue->next){if (hashValue->address->key == key){return hashValue;}}return NULL;
}//往哈希表插入一个HashNode(头插法)
void insertHashNodeToHashTable(struct HashNode** hashTable, int hashKey,struct HashNode* hnode)
{ hnode->next = hashTable[hashKey];hashTable[hashKey] = hnode;
}//从哈希表hashTable[hashKey]->address == dnode的结点断开在之前的链表
struct HashNode* deleteHashNodeFromHashTable(struct HashNode** hashTable, int hashKey, struct DoubleNode* dnode)
{struct HashNode* pre_hashNode = hashTable[hashKey];struct HashNode* freeNode = NULL;if (pre_hashNode->address == dnode) //如果第一个为删除结点,则将hashTable[hashKey] = pre_hashNode->next{freeNode = pre_hashNode;hashTable[hashKey] = pre_hashNode->next;}else //否则需要寻找address为dnode的前驱结点{while (pre_hashNode->next->address != dnode){pre_hashNode = pre_hashNode->next;}freeNode = pre_hashNode->next;pre_hashNode->next = pre_hashNode->next->next;}return freeNode;
}
2.3.4 内存释放代码
//释放hashTable的内存
void hashTableNodeListFree(struct HashNode** hashTable, int capacity)
{ for(int hashKey = 0; hashKey < capacity; hashKey++){//释放相同hashKey的链表结点内存for(struct HashNode* head = hashTable[hashKey]; head != NULL; NULL){struct HashNode* freeNode = head;head = head->next;free(freeNode);}}free(hashTable);
}//释放双向链表的内存
void doubleNodeListFree(struct DoubleList* doubleList)
{ //释放双向链表每一个数据结点空间for(struct DoubleNode* head = doubleList->dummyHead; head != NULL; NULL){struct DoubleNode* freeNode = head;head = head->rlink;free(freeNode);}//释放双向链表的头结点空间free(doubleList);
}
2.3.5 题目代码实现
LRUCache* lRUCacheCreate(int capacity)
{LRUCache* obj = (LRUCache*)calloc(1, sizeof(LRUCache));obj->capacity = capacity;obj->hashTable = (struct HashNode**)calloc(capacity, sizeof(struct HashNode*));obj->doubleList = (struct DoubleList*)calloc(1, sizeof(struct DoubleList)); //初始化双向链表initDoubleList(obj->doubleList);return obj;
}int lRUCacheGet(LRUCache* obj, int key)
{int hashKey = hashFunc(key, obj->capacity);struct HashNode* hashValue = getHashNode(obj->hashTable, hashKey, key);if(hashValue != NULL){moveNodeToHead(obj->doubleList, hashValue->address);return hashValue->address->value;}return -1;
}void lRUCachePut(LRUCache* obj, int key, int value)
{int hashKey = hashFunc(key, obj->capacity);//查看当前的hashKey是否存在//存在则修改valuestruct HashNode* hashValue = getHashNode(obj->hashTable, hashKey, key);if(hashValue != NULL){hashValue->address->value = value;moveNodeToHead(obj->doubleList, hashValue->address);return;}//当前的key对应的hashKey不存在//则将当前的key插入到hashTable中if (obj->curCapacty < obj->capacity) //缓存空间未满 { //新建一个双向链表的结点struct DoubleNode* dnode = (struct DoubleNode*)calloc(1, sizeof(struct DoubleNode));dnode->key = key;dnode->value = value;//新建一个HashNodestruct HashNode* hnode = (struct HashNode*)calloc(1, sizeof(struct HashNode));hnode->address = dnode; //插入到哈希表insertHashNodeToHashTable(obj->hashTable, hashKey, hnode);//将dnode插入到双链表的头insertNodeToDoubleListFirst(obj->doubleList, dnode);obj->curCapacty++;}else //缓存空间已满 重用旧的结点->需要切断旧结点以前的联系->重新赋值->新生{//逐出最近未使用的关键字,即双向链表的尾结点struct DoubleNode* dnode = obj->doubleList->dummyRear->llink;//重置dnode在hashTable的位置struct HashNode* hnode = deleteHashNodeFromHashTable(obj->hashTable, hashFunc(dnode->key,obj->capacity), dnode);//将dnode重新赋值dnode->key = key;dnode->value = value;//使用原来的哈希结点,则 hnode->address = dnode 可省略insertHashNodeToHashTable(obj->hashTable, hashKey,hnode);moveNodeToHead(obj->doubleList, dnode);}
}void lRUCacheFree(LRUCache* obj)
{//先释放双向链表的内存doubleNodeListFree(obj->doubleList);//释放哈希表的内存hashTableNodeListFree(obj->hashTable, obj->capacity);//释放缓存的头结点内存free(obj);
}
总结
看到题目通过那瞬间,真的非常开心,但我知道,代码还有很多大优化的空间,希望能够持续不断地学习!
仅仅用这篇文章记录本人解题的过程,希望对读者有所帮助吧!
相关文章:
数据结构-LRU缓存(C语言实现)
遇到困难,不必慌张,正是成长的时候,耐心一点! 目录 前言一、题目介绍二、实现过程2.1 实现原理2.2 实现思路2.2.1 双向链表2.2.2 散列表 2.3 代码实现2.3.1 结构定义2.3.2 双向链表操作实现2.3.3 实现散列表的操作2.3.4 内存释放代…...
javacv FFmpegFrameGrabber 阻塞重连解决方法汇总
JavaCV中FrameGrabber类可以连接直播流地址, 进行解码, 获取Frame帧信息, 常用方式如下 FrameGrabber grabber new FrameGrabber("rtsp:/192.168.0.0"); while(true) {Frame frame grabber.grabImage();// ... } 在如上代码中, 若连接地址网络不通, 或者连接超时…...
自然语言处理问答系统技术
自然语言处理问答系统技术 随着人工智能的不断发展,自然语言处理(NLP)技术已成为推动智能问答系统发展的核心技术。问答系统是利用NLP来解析用户提出的问题,并从知识库中找到最相关的答案。在许多应用中,如智能客服、…...
交换机和路由器的区别
交换机和路由器的区别主要体现在以下几个方面: 工作层次不同:交换机通常工作在OSI模型的数据链路层(第二层),主要根据MAC地址进行数据包转发。而路由器则工作在OSI模型的网络层(第三层)…...
JavaScript Array(数组)
JavaScript Array(数组) JavaScript 中的数组是一种特殊的对象,用于存储一系列有序的值。数组是 JavaScript 中非常强大的数据结构,广泛用于各种编程任务。本文将详细介绍 JavaScript 数组的特性、用法和操作方法。 数组的创建 在 JavaScript 中,创建数组有多种方式: …...
示例说明:elasticsearch实战应用
Elasticsearch 是一个基于 Lucene 的分布式搜索和分析引擎,广泛应用于日志分析、全文搜索、数据可视化等领域。以下是 Elasticsearch 实战应用的一些关键点和步骤: 1. 环境搭建 首先,你需要在你的环境中安装和配置 Elasticsearch。 安装 E…...
暴力匹配算法和 KMP 算法的优缺点分别是什么?
暴力匹配算法和 KMP 算法的优缺点分别是什么? 在字符串匹配领域,暴力匹配算法和 KMP(Knuth-Morris-Pratt)算法是两种常见的方法。它们各有特点,适用于不同的场景。让我们深入探讨这两种算法的优缺点。 一、暴力匹配算法 (一)优点 简单易实现:暴力匹配算法的逻辑非常…...
web笔记
<form method"POST" action"{{ url_for(register) }}"><label for"username">用户名:</label><input type"text" id"username" name"username" required><br><label for"p…...
【网络安全】-访问控制-burp(1~6)
文章目录 前言 1.Lab: Unprotected admin functionality 2.Lab: Unprotected admin functionality with unpredictable URL 3.Lab: User role controlled by request parameter 4.Lab:User role can be modified in user profile 5.Lab: User ID controlled by…...
iOS 项目中的多主题颜色设计与实现
引言 在现代iOS应用中,用户对个性化体验的需求越来越高,除了功能上的满足,多样的视觉风格也是提升用户体验的重要手段之一。提供多主题颜色的切换功能不仅能满足用户的审美偏好,还可以让应用更具活力,适应不同场景下的…...
Android Camera2 与 Camera API技术探究和RAW数据采集
Android Camera2 Android Camera2 是 Android 系统中用于相机操作的一套高级应用程序接口(API),它取代了之前的 Camera API。以下是关于 Android Camera2 的一些主要信息: 主要特点: 强大的控制能力:提供…...
[python][pipenv]pipenv的使用
pipenv 是一个 Python 开发工作流程的工具,它旨在将 pip 的包管理和 virtualenv 的虚拟环境管理结合起来。以下是一些基本的 pipenv 使用方法: 安装 pipenv: 如果你还没有安装 pipenv,可以通过 pip 安装它: pip insta…...
SpringSession微服务
一.在linux中确保启动起来redis和nacos 依赖记得别放<dependencyManagement></dependencyManagement>这个标签去了 1.首先查看已经启动的服务 docker ps 查看有没有安装redis和nacos 2.启动redis和nacos 发现没有启动redis和nacos,我们先来启动它。,…...
强化学习:通过试错学习最优策略---示例:使用Q-Learning解决迷宫问题
强化学习(Reinforcement Learning, RL)是一种让智能体(agent)在与环境交互的过程中,通过最大化某种累积奖励来学习如何采取行动的学习方法。它适用于那些需要连续决策的问题,比如游戏、自动驾驶和机器人控制…...
OpenGL ES 纹理(7)
OpenGL ES 纹理(7) 简述 通过前面几章的学习,我们已经可以绘制渲染我们想要的逻辑图形了,但是如果我们想要渲染一张本地图片,这就需要纹理了。 纹理其实是一个可以用于采样的数据集,比较典型的就是图片了,我们知道我…...
【C#】CacheManager:高效的 .NET 缓存管理库
在现代应用开发中,缓存是提升性能和降低数据库负载的重要技术手段。无论是 Web 应用、桌面应用还是移动应用,缓存都能够帮助减少重复的数据查询和处理,从而提高系统的响应速度。然而,管理缓存并不简单,尤其是当你需要处…...
【数学分析笔记】第4章第2节 导数的意义和性质(2)
4. 微分 4.2 导数的意义与性质 4.2.3 单侧导数 f ′ ( x ) lim Δ x → 0 f ( x Δ x ) − f ( x ) Δ x lim x → x 0 f ( x ) − f ( x 0 ) x − x 0 f(x)\lim\limits_{\Delta x\to 0}\frac{f(x\Delta x)-f(x)}{\Delta x}\lim\limits_{x\to x_0}\frac{f(x)-f(x_0)…...
深度学习:迁移学习
目录 一、迁移学习 1.什么是迁移学习 2.迁移学习的步骤 1、选择预训练的模型和适当的层 2、冻结预训练模型的参数 3、在新数据集上训练新增加的层 4、微调预训练模型的层 5、评估和测试 二、迁移学习实例 1.导入模型 2.冻结模型参数 3.修改参数 4.创建类ÿ…...
Footprint Growthly Quest 工具:赋能 Telegram 社区实现 Web3 飞速增长
作者:Stella L (stellafootprint.network) 在 Web3 的快节奏世界里,社区互动是关键。而众多 Web3 社区之所以能够蓬勃发展,很大程度上得益于 Telegram 平台。正因如此,Footprint Analytics 精心打造了 Growthly —— 一款专为 Tel…...
进入xwindows后挂起键盘鼠标没有响应@FreeBSD
问题: 在升级pkg包后,系统无法进入xfce等xwindows,表现为黑屏和看见鼠标,左上角有一个白字符块,键盘鼠标没有反应,整个系统卡住。但是可以ssh登录,内部的服务一切正常。 表现 处理过程…...
CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...
MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
今日科技热点速览
🔥 今日科技热点速览 🎮 任天堂Switch 2 正式发售 任天堂新一代游戏主机 Switch 2 今日正式上线发售,主打更强图形性能与沉浸式体验,支持多模态交互,受到全球玩家热捧 。 🤖 人工智能持续突破 DeepSeek-R1&…...
SpringTask-03.入门案例
一.入门案例 启动类: package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
