本文阅读目录

1、缓存是什么

2、什么是LRU缓存

3、LRU缓存设计思路

4、LRU缓存接口

缓存是什么

其实缓存的思想在生活中处处可见,比如说我们的书架上摆满了各种书籍,但是我最近想集中精力攻克数据结构这门课,因次为了方便,我将与数据结构有关的书先从书架取下来,放在我们的书桌上,这样我们取书的时候就快很多了,伸手可得,而不用跑到书架旁取书了,在这个例子里,书桌所扮演的就是缓存的角色。

在计算机中,我们知道,访问磁盘的效率比访问内存的效率低得多,低多少了,大概是10W级别吧,如下图所示。 我们知道有个很著名的二八定律。其实计算机世界中也有很多情况遵守这个定律,比如说网站访问的特点:80% 的业务访问集中在20% 的数据上。那么很自然就想到:既然大部分业务访问集中在一小部分数据上,那么如果通过设计合理的数据结构把这一小部分数据存放在内存中,是不是极大的提高了系统的性能呢,这就是缓存。

从上面可以看出,无论是生活中还是计算机世界中的缓存,主要有以下两个特点:

  • 提高系统效率,

  • 容量小于外部存储容量,如书桌容量小于书架容量,内存容量小于硬盘容量

什么是LRU缓存

由于内存是有限的,因此缓存总有满的时候,那么当缓存满了的时候,这时候又有新的数据需要加入到缓存中时,我们该怎么办了?没什么别的办法,只有从缓存中删除旧数据为新数据腾出空间,那么究竟删除原来缓存的哪些数据了?这就涉及到缓存的替换策略,LRU就是一种缓存策略。

LRU,Least Recently Used的简写,翻译过来就是“最近最少使用”。该算法依据于程序的局部性原理, 其淘汰旧数据的策略是,距离当前最久没有被访问过的数据应该被淘汰。

举个例子:现在有一个能存放5个数据的缓存,刚开始中缓存中存放了3个数据A->B->C,如下所示: 接着又在缓存中插入2个数据D->E,此时缓存已经满了。如下: 好了,现在我们来访问数据A,现在A成为了最近访问的数据,B成了最久没有访问过的数据: 现在我们又有一个数据F想要插入到缓存中,由于缓存已经满了,需要从缓存中淘汰旧数据,此时缓存中的数据B是距离当前时间最久没有被访问过的数据,根据LRU算法,我们会淘汰掉数据B:将F插入到当前B的位置。 此时数据C成为了距离当前时间最久没有被访问过的数据,因此如果再来一个数据的话,就应该插入到C当前的位置,依次类推。

LRU缓存设计

理解LRU缓存的思想其实挺简单,但是要自己动手设计一个LRU缓存就并不那么简单了,这节主要讲讲LRU缓存的设计思路。

首先从前面的分析可以看出,LRU缓存主要的操作是插入删除以及查找,因此我们可以考虑用双链表来维护缓存,该链表将缓存数据按访问时间从新到旧排列起来。

如果我们只是单存将缓存中的数据维护在一个双向链表中, 那么当我们需要从缓存中查找数据时,需要遍历链表,其时间复杂度为O(n)。 这样的设计, 是一种比较低效率的做法。 因此, 我们除了将数据维护在双向链表中, 我们同时还将数据维护在一个哈希表中。 哈希表访问数据的时间复杂度为O(1)。

依据上述设计, 一个LRU缓存包含了一个双向链表和一个哈希表, 双向链表以及哈希表的一个节点代表缓存中的一个缓存单元, 因此我们可以这样定义我们缓存单元的数据结构:

//LRU缓存的缓存单元 
typedef struct cacheEntryS
{  
    char key;   //数据的key 
    char data;  // 数据的data
      
    struct cacheEntryS *hashListPrev; //指向哈希链表的前一个元素
    struct cacheEntryS *hashListNext; //指向哈希链表的后一个元素
      
    struct cacheEntryS *lruListPrev;  //指向链表的前一个元素
    struct cacheEntryS *lruListNext;  //指向链表后一个元素
}cacheEntryS;  

LRU缓存的数据结构如下:

//定义LRU缓存
typedef struct LRUCacheS
{  
    int cacheCapacity;  //缓存的容量
    cacheEntryS **hashMap;  //缓存的哈希表
      
    cacheEntryS *lruListHead;//缓存的双向链表表头
    cacheEntryS *lruListTail;//缓存的双向链表表尾
    int lruListSize;    //缓存的双向链表节点个数
}LRUCacheS;  

如果一大坨代码看的比较晕,那么对着下面的结构关系图应该能理解的比较透彻,注意下图中没有指向的指针都是NULL:

LRU缓存接口设计

通过前面的分析不难看出,对一个LRU缓存的最重要的操作无外乎是创建、销毁、插入数据、查找数据。下面给出这几个接口,至于具体实现,这里先不分析。后面有机会再一一分析。

/*********************************************
函数名:LRUCacheCreate
功能:创建LRU缓存
输入参数:capacity,缓存的数据容量
输出参数:lruCache,指向新建缓存的指针
针返回值:0---成功     -1---失败

*********************************************/
int LRUCacheCreate(int capacity, void **lruCache);
/*********************************************
函数名:LRUCacheDestory
功能:销毁LRU缓存
输入参数:lruCache指向新建缓存的指针
输出参数:无
针返回值:0---成功     -1---失败
*********************************************/
int LRUCacheDestory(void *lruCache);
/*********************************************
函数名:LRUCacheSet
功能:将数据插入到LRU缓存中
输入参数:key:数据索引   data:数据内容
输出参数:无
针返回值:0---成功     -1---失败
*********************************************/
int LRUCacheSet(void *lruCache, char key, char data);
/*********************************************
函数名:LRUCacheGet
功能:将数据插入到LRU缓存中
输入参数:key:数据索引
输出参数:无
针返回值:缓存中存在key对应的data,返回
        缓存中不存在key对应的data,返回'\0'
*********************************************/
int LRUCacheGet(void *lruCache, char key);

推荐阅读:

【福利】自己搜集的网上精品课程视频分享(上) 【协议森林】邮差与邮局 (网络协议概观)

【数据结构与算法】 通俗易懂讲解 位排序 【C++札记】C++11并发编程(一)开启线程之旅 【C++札记】C/C++指针使用常见的坑 【C++札记】静态库和动态库详解(上)

码农有道 coding

码农有道,为您提供通俗易懂的技术文章,让技术变的更简单!

专注服务器后台技术栈知识总结分享

欢迎关注交流共同进步