数据结构-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;        //真实的key
    int 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是否存在
    //存在则修改value
    struct 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;

        //新建一个HashNode
        struct 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);
}

总结

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

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

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

相关文章

SigmaStudio控件Cross Mixer\Signal Merger算法效果分析

衰减与叠加混音算法验证分析一 CH2:输入源为-20dB正弦波1khz CH1叠加混音&#xff1a;参考混音算法https://blog.csdn.net/weixin_48408892/article/details/129878036?spm1001.2014.3001.5502 Ch0衰减混音&#xff1a;外部多个输入源做混音时&#xff0c;建议参考该算法控件&…

在VMware虚拟机上部署polardb

免密登录到我们的虚拟机之后&#xff0c;要在虚拟机上部署polardb数据库&#xff0c;首先第一步要先克隆源码&#xff1a; 为了进SSH协议进行传输源码需要先进行下面的步骤&#xff1a; 将宿主机上的私钥文件复制到虚拟机上 scp "C:\Users\waitw\.ssh\id_rsa" ann…

Azkaban:大数据任务调度与编排工具的安装与使用

在当今大数据时代&#xff0c;数据处理和分析任务变得越来越复杂。一个完整的大数据分析系统通常由大量任务单元组成&#xff0c;如 shell 脚本程序、mapreduce 程序、hive 脚本、spark 程序等。这些任务单元之间存在时间先后及前后依赖关系&#xff0c;为了高效地组织和执行这…

Leetcode 每日一题:Crack The Safe

写在前面&#xff1a; 学期真的忙起来了&#xff0c;我的两个社团也在上一周终于完成了大部分招新活动。虽然后面有即将到来的期中考试和求职&#xff0c;但希望能有时间将帖子的频率提上去吧&#xff08;真的尽量因为从做题思考到写博客讲解思路需要大量的时间&#xff0c;在…

当人工智能拥抱餐饮业,传统与创新的交融

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 今天我们要聊一个充满烟火气的行业&#x…

Threejs绘制圆锥体

上一章节实现了胶囊体的绘制&#xff0c;这节来绘制圆锥体&#xff0c;圆锥体就是三角形旋转获得的&#xff0c;如上文一样&#xff0c;先要创建出基础的组件&#xff0c;包括场景&#xff0c;相机&#xff0c;灯光&#xff0c;渲染器。代码如下&#xff1a; initScene() {this…

信息安全工程师(22)密码学网络安全应用

前言 密码学在网络安全中的应用极为广泛且深入&#xff0c;它通过多种技术手段确保数据的机密性、完整性和真实性。 一、数据加密 对称加密&#xff1a; 定义&#xff1a;使用相同的密钥进行加密和解密的过程。特点&#xff1a;加密和解密速度快&#xff0c;适用于大数据量的加…

网站集群批量管理-密钥认证与Ansible模块

一、集群批量管理-密钥认证 1、概述 管理更加轻松&#xff1a;两个节点,通过密钥形式进行访问,不需要输入密码,仅支持单向. 服务要求(应用场景)&#xff1a; 一些服务在使用前要求我们做秘钥认证.手动写批量管理脚本. 名字: 密钥认证,免密码登录,双机互信. 2、原理 税钥对…

websocket集群部署遇到的一些事

最近刚好有个场景&#xff0c;业务处理一份报告需要关注实时处理的进度。 本来打算使用前端轮训方式&#xff0c;但是考虑到这样效率比较低&#xff0c;也无法精确知道处理进度&#xff0c;就想到用websocket和前端实时交互&#xff0c;进度有更新就通知前端&#xff0c;避免了…

视频集成与融合项目中需要视频编码,但是分辨率不兼容怎么办?

在众多视频整合项目中&#xff0c;一个显著的趋势是融合多元化的视频资源&#xff0c;以实现统一监管与灵活调度。这一需求促使项目团队不断探索新的集成方案&#xff0c;确保不同来源的视频流能够无缝对接&#xff0c;共同服务于统一的调看与管理平台&#xff0c;进而提升整体…

MobaXterm基本使用 -- 服务器状态、批量操作、显示/切换中文字体、修复zsh按键失灵

监控服务器资源 参考网址&#xff1a;https://www.cnblogs.com/144823836yj/p/12126314.html 显示效果 MobaXterm提供有这项功能&#xff0c;在会话窗口底部&#xff0c;显示服务器资源使用情况 如内存、CPU、网速、磁盘使用等&#xff1a; &#xff08;完整窗口&#xff0…

GAMES101(17~18节,物理材质模型)

材质 BRDF 材质&#xff1a;决定了光线与物体不同的作用方式 BRDF定义了物体材质,包含漫反射和镜面部分 BSDF &#xff08;scattering散射&#xff09; BRDF&#xff08;reflect反射&#xff09; BTDF 光线打击到物体上会向四面八方散射 反射 光线打击到物体上反射出去…

MATLAB案例 | Copula的密度函数和分布函数图

本文介绍各种类型&#xff08;Gaussian、t、Gumbel、Clayton、Frank&#xff09;Copula的密度函数和分布函数图的绘制 完整代码 clc close all clear%% ********************计算Copula的密度函数和分布函数图************************ [Udata,Vdata] meshgrid(linspace(0,1…

C#自定义工具类-数组工具类

目录 数组工具类基本操作 1.排序&#xff1a;升序&#xff0c;降序 2.查找 1&#xff09;查找最值&#xff1a;最大值&#xff0c;最小值 2&#xff09;查找满足条件的单个对象 3&#xff09;查找满足条件的所有对象 4&#xff09;选取数组中所有对象的某一字段 完整代…

查缺补漏----程序查询方式和中断方式计算题

1.程序查询方式 总结下来就是&#xff1a; 必须在外设传输完端口大小的数据时访问端口&#xff0c;以防止数据未被及时读出而丢失。 占CPU总时间&#xff1a;就是某段时间内设备用了多少时钟周期/PCU有多少个时钟周期 CPU的时钟周期数&#xff1a;就看主频&#xff0c;主频表示…

大数据开发--1.1大数据概论

目录 一.大数据的概念 什么是大数据&#xff1f; 二. 大数据的特点 三. 大数据应用场景 四. 大数据分析业务步骤 大数据分析的业务流程&#xff1a; 五.大数据职业规划 职业方向 岗位技术要求 六. 大数据学习路线 一.大数据的概念 什么是大数据&#xff1f; 数据 世界…

Spring Boot技术:构建高效网上购物平台

第3章 系统分析 3.1 可行性分析 在系统开发之初要进行系统可行分析&#xff0c;这样做的目的就是使用最小成本解决最大问题&#xff0c;一旦程序开发满足用户需要&#xff0c;带来的好处也是很多的。下面我们将从技术上、操作上、经济上等方面来考虑这个系统到底值不值得开发。…

车辆重识别(注意力 U-Net:学习在哪些区域寻找胰腺)论文阅读2024/10/01

什么是注意力机制&#xff1f; 什么是加性注意力&#xff1f; 大致说一下流程&#xff1a; 对于一张特征图来说&#xff0c;对于这张图中的每一个像素向量&#xff08;例如a&#xff09;&#xff0c;计算该向量与所有像素向量的相似度&#xff0c;对这些相似度进行激活函数…

【重学 MySQL】四十五、数据库的创建、修改与删除

【重学 MySQL】四十五、数据库的创建、修改与删除 一条数据存储的过程数据输入数据验证数据处理数据存储数据持久化反馈与日志注意事项 标识符命名规则基本规则长度限制保留字与特殊字符命名建议示例 MySQL 中的数据类型创建数据库创建数据库时指定字符集和排序规则 查看数据库…

数据库重建索引的作用?

重建索引是数据库管理中的一个重要操作&#xff0c;主要用于优化数据库性能和提高查询效率。以下是重建索引的一些主要用途&#xff1a; 提高查询性能&#xff1a;随着时间的推移&#xff0c;数据的插入、更新和删除会导致索引碎片化&#xff0c;重建索引可以减少碎片&#xf…