设计一个Cache系统 【双向链表+哈希表】

本文介绍了一种经典的缓存算法——LRU(最近最少使用)算法的实现方法。通过结合哈希表与双向链表的数据结构,实现了高效的数据访问与更新。详细解释了LRU算法的工作原理及其实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     今天去面试,面试官让我设计一个cache系统,要求保证最近使用的数据不能被移除出cache,也就是每次添加一个cache项的时候,把最旧的cache项移除出去。

    我只记得操作系统里貌似有个差不多的cache算法,记不起名字来,更别提数据结构了。一开始我执着于用一种数据结构来实现,可是每说出一种,都被面试官指出这种方式的不足。最后终于开窍了,想出了 哈希表+双向链表 的方法。当时也不知道到底对不对,感觉这种实现还过的去吧,查询和增加cache项复杂度都是O(1)。

    回来后一查,原来就是LRU(Least Recent Used)算法。好到事实上就是采用了哈希表+双向链表的组合拳法。文末附上一篇别人写的博文。

   另外,我发现Java1.4中有一个LinkedHashMap,就是哈希表+双链表,有一个removeEldestEntry方法,里面没有实现,override这个方法后很容易实现FIFO的Cache,对其它几个方法也override后想必实现LRU也不难。

 http://blog.sina.com.cn/s/blog_567842410100nf1g.html

Cache(高速缓存), 一个在计算机中几乎随时接触的概念。CPU中Cache能极大提高存取数据和指令的时间,让整个存储器(Cache+内存)既有Cache的高速度,又能有内存的大容量;操作系统中的内存page中使用的Cache能使得频繁读取的内存磁盘文件较少的被置换出内存,从而提高访问速度;数据库中数据查询也用到Cache来提高效率;即便是Powerbuilder的DataWindow数据处理也用到了Cache的类似设计。Cache的算法设计常见的有FIFO(first in first out)和LRU(least recently used),FIFO对CPU的指令序列非常有效,但更多的,对于Memory或者磁盘文件的这种Cache,LRU就更有效多了。FIFO的算法设计很简单明了,就不做讨论,在此只是对LRU展开。

Cache中的存储空间被分成若干块,每一块对应内存或者磁盘文件的一段数据(当然也可以是指令数据),形成这种映射关系,当Cache中的存储块被用完,而需要把新的数据Load进Cache的时候,我们就需要设计一种良好的算法来完成数据块的替换。LRU的思想是基于“最近用到的数据被重用的概率比较早用到的大的多”这个设计规则来实现的。Cache中的所有块位置都用双向链表链接起来,当一个位置被命中后,就将通过调整链表的指向将该位置调整到链表的头位置,新加入的内容直接放在链表的头上。这样,在进行过多次查找操作后,最近被命中过的内容就向链表的头移动,而没有被命中的内容就向链表的后面移动。当需要替换时,链表最后的位置就是最近最少被命中的位置,我们只需要将新的内容放在链表前面,淘汰链表最后的位置就是想了。

对于双向链表的使用,基于两个考虑。首先是Cache中块的命中可能是随机的,和Load进来的顺序无关,所以我们需要用链表这种结构来保存位置队列,使得其可以灵活的调整相互间的次序。其次,双向链表使得在知道一个位置的情况下可以很迅速的移到其他的地方,时间复杂度为O(1)。

查找一个链表中元素的时间复杂度是O(n),每次命中的时候,我们就需要花费O(n)的时间来进行查找,如果不添加其他的数据结构,这个就是我们能实现的最高效率了。目前看来,整个算法的瓶颈就是在查找这里了,怎么样才能提高查找的效率呢?Hash表,对,就是它,数据结构中之所以有它,就是因为它的查找时间复杂度是O(1)。

梳理一下思路:对于Cache的每个位置,我们设计一个数据结构来储存Cache块的内容,并实现一个双向链表,其中属性next和prev是双向链表的两个指针,key用于存储对象的键值,value用于存储要cache块对象本身,然后用Hash表来查找具体被命中的Cache块。剩下的就是写Code的事了:我们使用一个hashmap作为cache,用hashmap的检索机制来实现cache查找;并用head和last两个属性来记录链表的头和尾。并提供putEntry(),getEntry()方法来操作该cache。

本文出自 “木又寸的技术博客” 博客,请务必保留此出处http://jianshusoft.blog.51cto.com/2380869/666653

基于哈希表双向链表的数据结构,可以创建一个简单的缓存模拟器,通常用于提高数据查找的效率。在C语言中,我们可以这样做: 首先,定义一个节点结构,包含键值对以及指向前驱和后继节点的指针: ```c typedef struct Node { void* key; // 存储键值的对象指针 void* value; // 存储对应值的对象指针 struct Node* prev; // 指向前一个节点 struct Node* next; // 指向后一个节点 } CacheNode; ``` 然后,创建一个哈希表(这里可以使用`std::unordered_map`或者自定义的哈希函数和数组): ```c #include <stdlib.h> // 对于malloc和free typedef struct HashTable { size_t capacity; CacheNode** table; void (*hash)(const void*, size_t&); } HashTable; // 初始化哈希表、计算散列函数等操作... ``` 接下来,定义插入、删除和获取操作: ```c void insert(HashTable* cache, void* key, void* value) { // 计算散列值并找到对应的桶 size_t index = hash(key, cache->capacity); CacheNode* newNode = (CacheNode*)malloc(sizeof(CacheNode)); newNode->key = key; newNode->value = value; newNode->prev = NULL; newNode->next = cache->table[index]; if (cache->table[index] != NULL) { // 如果桶非空 cache->table[index]->prev = newNode; } cache->table[index] = newNode; // 双向链表维护 if (newNode->next != NULL) newNode->next->prev = newNode; } // 删除操作类似,通过哈希查找并更新链表连接 void remove(HashTable* cache, void* key) { // ... } // 获取操作:检查缓存是否存在该键,存在则返回对应的值 void* get(HashTable* cache, void* key) { size_t index = hash(key, cache->capacity); if (cache->table[index] != NULL && cache->table[index]->key == key) { return cache->table[index]->value; } else { return NULL; // 或者抛出异常,表示未找到 } } ``` 最后,记得处理内存管理,当缓存满或者某个节点需要替换时,可以选择合适的淘汰策略,比如LRU(最近最少使用)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值