数据结构-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.1 LRU原理图

2.2 实现思路

2.2.1 双向链表

为了方便双向链表的插入和删除操作,可以使用两个辅助结点,一个伪头部和一个伪尾部,实现了每个真实结点都有前驱和后继
在这里插入图片描述

图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.2.1 散列表

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);
}

总结

看到题目通过那瞬间,真的非常开心,但我知道,代码还有很多大优化的空间,希望能够持续不断地学习!
仅仅用这篇文章记录本人解题的过程,希望对读者有所帮助吧!

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

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

相关文章

【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次

文章目录 2.2 该问题的函数式解 A functional solution to our problem1. 高阶函数解 A higher-order solution2. 高阶函数解的手动测试 Testing the solution manually3. 高阶函数解的自动测试 Testing the solution automatically4. 更好的解决方案 Producing an even better…

idea创建springboot模块

1.点击file->新建->model server url&#xff1a;如果倒数第二个java选项没有11&#xff0c;就把这里改为阿里云的 name&#xff1a;模块名字 location&#xff1a;文件存放的位置 其他的根据图片自行填写 2. 3.验证 如果没有iml文件(不影响&#xff0c;可以不弄)&#…

LabVIEW提高开发效率技巧----属性节点优化

在LabVIEW开发中&#xff0c;优化代码的效率和性能是非常重要的&#xff0c;尤其是在涉及前面板控件的属性节点时。频繁使用属性节点可能会导致程序执行速度的明显下降&#xff0c;特别是在处理大量数据或高频率操作时。下面详细介绍一些在LabVIEW开发中优化属性节点使用的技巧…

数据结构--线性表(顺序结构)

1.线性表的定义和基本操作 1.1线性表以及基本逻辑 1.1.1线性表 &#xff08;1&#xff09;n(>0)个数据元素的有限序列&#xff0c;记作&#xff08;a1,a2,...an&#xff09;&#xff0c;其中ai是线性表中的数据元素&#xff0c;n是表的长度。 &#xff08;2&#xff09;…

4个顶级的大模型推理引擎

LLM 在文本生成应用中表现出色&#xff0c;例如具有高理解度和流畅度的聊天和代码完成模型。然而&#xff0c;它们的庞大规模也给推理带来了挑战。基本推理速度很慢&#xff0c;因为 LLM 会逐个生成文本标记&#xff0c;需要对每个下一个标记进行重复调用。随着输入序列的增长&…

ElasticSearch 备考 -- 备份和恢复

一、题目 备份集群下的索引 task&#xff0c;存储快照名称为 snapshot_1 二、思考 这个涉及的是集群的备份&#xff0c;主要是通过创建快照&#xff0c;涉及到以下2步骤 Setp1&#xff1a;注册一个备份 snapshot repository Setp2&#xff1a;创建 snapshot 可以通过两种方…

MindSearch 部署到Github Codespace 和 Hugging Face Space

conda init后需要重开终端&#xff0c;不然一键复制会导致后续pip install会安装错环境 还是报错 ImportError: cannot import name AutoRegister from class_registry (/opt/conda/envs/mindsearch/lib/python3.10/site-packages/class_registry/__init__.py)pip install --…

【技术分析】嘉楠科技SoC芯片K230

概述 K230是嘉楠科技Kendryte系列AIoT芯片中的最新一代SoC芯片&#xff0c;该芯片采用全新的多异构单元加速计算架构&#xff0c;集成的玄铁C908具有2个高能效RISCV计算核心&#xff0c;内置新一代KPU&#xff08;Knowledge Process Unit&#xff09;智能计算单元&#xff0c;…

Unity初识+面板介绍

Unity版本使用 小版本号高&#xff0c;出现bug可能性更小&#xff1b;一台电脑可以安装多个版本的Unity&#xff0c;但是需要安装在不同路径&#xff1b;安装Unity时不能有中文路径&#xff1b;Unity项目路径也不要有中文。 Scene面板 相当于拍电影的片场&#xff0c;Unity程…

Go基础学习11-测试工具gomock和monkey的使用

文章目录 基础回顾MockMock是什么安装gomockMock使用1. 创建user.go源文件2. 使用mockgen生成对应的Mock文件3. 使用mockgen命令生成后在对应包mock下可以查看生成的mock文件4. 编写测试代码5. 运行代码并查看输出 GomonkeyGomonkey优势安装使用对函数进行monkey对结构体中方法…

Chat登录时出现SSO信息出错的解决方法

目录 1. 问题所示2. 问题所示3. 解决方法 1. 问题所示 此贴主要是总结回顾&#xff0c;对此放置在运维专栏 出现如下问题&#xff0c;很懵&#xff0c;以为是节点挂了还是网址蹦了 一直刷新&#xff0c;登录之后就出现这个问题 2. 问题所示 对于SSO&#xff0c;也就是单点登…

深度学习项目----用LSTM模型预测股价(包含LSTM网络简介,代码数据均可下载)

前言 前几天在看论文&#xff0c;打算复现&#xff0c;论文用到了LSTM&#xff0c;故这一篇文章是小编学LSTM模型的学习笔记&#xff1b;LSTM感觉很复杂&#xff0c;但是结合代码构建神经网络&#xff0c;又感觉还行&#xff1b;本次学习的案例数据来源于GitHub&#xff0c;在…

4.4章节python中循环结构得互相嵌套:常用于属于图形(长方形、三角形、菱形)

一、定义和注意事项 在Python中&#xff0c;循环结构&#xff08;如for循环和while循环&#xff09;可以互相嵌套。嵌套循环意味着一个循环内部包含另一个循环。这在处理多维数据或需要执行多次迭代的任务时非常有用。 注意&#xff1a; 1.缩进&#xff1a;在Python中&…

实施威胁暴露管理、降低网络风险暴露的最佳实践

随着传统漏洞管理的发展&#xff0c;TEM 解决了因攻击面扩大和安全工具分散而产生的巨大风险。 主动式 TEM 方法优先考虑风险并与现有安全工具无缝集成&#xff0c;使组织能够在威胁被有效利用之前缓解威胁。 为什么威胁暴露管理 (TEM) 在现代网络安全策略中变得至关重要&…

商家营销工具架构升级总结

今年以来&#xff0c;商家营销工具业务需求井喷&#xff0c;需求数量多且耗时都比较长&#xff0c;技术侧面临很大的压力。因此这篇文章主要讨论营销工具前端要如何应对这样大规模的业务需求。 问题拆解 我们核心面对的问题主要如下&#xff1a; 1. 人力有限 我们除了要支撑存量…

C语言 | Leetcode C语言题解之题451题根据字符出现频率排序

题目&#xff1a; 题解&#xff1a; #define HASH_FIND_CHAR(head, findint, out) HASH_FIND(hh, head, findint, sizeof(char), out) #define HASH_ADD_CHAR(head, intfield, add) HASH_ADD(hh, head, intfield, sizeof(char), add)struct HashTable {char key;int val;UT_ha…

《数据密集型应用系统设计》笔记——第二部分 分布式数据系统(ch5-9)

第5章 数据复制 目的&#xff1a; 地理位置更近&#xff0c;降低延迟故障冗余提高读吞吐量 主节点与从节点&#xff08;主从复制&#xff09; 主从复制&#xff1a; 写请求发送给主节点&#xff0c;主节点将新数据写入本地存储&#xff1b;主节点将数据更改作为复制的日志发送…

SAP学习笔记 - Basis01 - 创建Client ,拷贝Client

最近工作当中用到了Client间数据移送的内容&#xff0c;想把自己的虚机给弄两个Client。 最后也没完全弄成&#xff0c;先把过程整理一下&#xff0c;以后有空接着弄。 目录 1&#xff0c;SALE - 新建逻辑系统 2&#xff0c;SCC4 - 分配Client到集团 3&#xff0c;RZ10 - 取…

python-FILIP/字符串p形编码/数字三角形

一&#xff1a;FILIP 题目描述 给你两个十进制正整数 a,b​&#xff0c;输出将这两个数翻转后的较大数。 「翻转」在本题中的定义详见「说明 / 提示」部分。输入 第一行&#xff0c;两个十进制正整数 a,b。输出 第一行&#xff0c;a 和 b 翻转后的较大数。样例输入1 734 893 样…

Microsoft Edge 五个好用的插件

&#x1f423;个人主页 可惜已不在 &#x1f424;这篇在这个专栏 插件_可惜已不在的博客-CSDN博客 &#x1f425;有用的话就留下一个三连吧&#x1f63c; 目录 Microsoft Edge 一.安装游览器 ​编辑 二.找到插件商店 1.打开游览器后&#xff0c;点击右上角的设置&#…