简单的LRU Cache设计与实现
get(key):若缓存中存在key,返回对应的value,否则返回-1
set(key,value):若缓存中存在key,替换其value,否则插入key及其value,如果插入时缓存已经满了,应该使用LRU算法把最近最久没有使用的key踢出缓存。
设计1:
cache使用数组,每个key再关联一个时间戳,时间戳可以直接用个long long类型表示,在cache中维护一个最大的时间戳:
- get的时候把key的时间戳变为最大时间戳+1
- set的时候,数据从前往后存储
如果key存在,更新key的时间戳为当前cache中最大的时间戳+1,并更新value;
如果key不存在,
若缓存满,在整个缓存中查找时间戳最小的key,其存储位置作为新key的存储位置,设置key的时间戳为最大时间戳+1
若缓存未满,设置key的时间戳为最大时间戳+1,存储位置为第一个空闲位置
分析下时间空间复杂度,get的时候,需要从前往后找key,时间为O(N),set的时候,也要从前往后找key,当缓存满的时候,还得找到时间戳最小的key,时间复杂度为O(N)。除了缓存本身,并没有使用其他空间,空间复杂度为O(1)。 这个速度显然是比较慢的,随着数据量的增大,get和set速度越来越慢。可能有人会想到用哈希表作为底层存储,这样get的时间复杂度确实可以减低为O(1),set的时候,只要缓存没有满,也可以在O(1)的时间完,但在缓存满的时候,依然需要每次遍历找时间戳最小的key,时间复杂度还是O(N)。
设计2:
cache底层使用单链表,同时用一个哈希表存储每个key对应的链表结点的前驱结点,并记录链表尾结点的key
- get时,从哈希表中找到key对应的链表结点,挪到链表头,更新指向尾结点的key
- set时,如果key存在,那么找到链表结点,并挪到链表头,更新指向尾结点的key
如果key不存在,
若缓存满,重用链表尾结点,设置新key和value,并挪到链表头,更新指向尾结点的key
若缓存未满,直接插入结点到链表头,若是第一结点,更新指向尾结点的key
get,set时间复杂度O(1),总的空间复杂度O(N)。比前面的设计好一点。下面的再来看下关于设计2的两个实现
实现1,自定义链表
为了方便链表的插入与删除,使用了带头结点head的链表,所以真正有效的第一个结点是head->next。另外,只是简单的实现,没有容错,不支持并发,简单的内存管理
ps. 用双向链表来实现会简单写,这里用单链表和哈希表共同实现了双向链表的功效,也就是哈希除了用来查找,还指示了key对应的结点的前驱结点。
struct Node{int _key;int _value;Node* _next;Node(int key,int value,Node* next):_key(key),_value(value),_next(next){} };class LRUCache{ public:LRUCache(int capacity) {_capacity = capacity;_size = 0;_last = 0;_cur_begin = _begin = (char *) malloc(sizeof(Node)*(capacity+1));_head = new (_cur_begin) Node(0,0,NULL);//在指定内存上构造对象_cur_begin += sizeof(Node);}~LRUCache(){if(_begin!=NULL){while(_cur_begin > _begin){_cur_begin -= sizeof(Node);((Node*)_cur_begin)->~Node();//先释放内存上的对象 }free(_begin);//再释放内存 }}int get(int key) {int value = -1;//初始时假设key对应的结点不存在 Node* pre_node_of_key = umap_prenodes[key];//key对应的结点的前驱结点if(pre_node_of_key !=NULL){//key结点存在 Node* node = pre_node_of_key->_next;//key对应的结点pre_node_of_key->_next = node->_next;if(pre_node_of_key->_next!=NULL){umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key;}node->_next = _head->_next;if(node->_next!=NULL){//node有后继,更新后继的前驱结点umap_prenodes[node->_next->_key] = node;}_head->_next = node;umap_prenodes[key] = _head; /*更新_last*/if(_last == key ){_last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); }value = node->_value;}return value;}void set(int key, int value) {Node* node = NULL;Node* pre_node_of_key = umap_prenodes[key];//key对应的结点的前驱结点if(pre_node_of_key != NULL){//key对应的结点存在,孤立key对应的结点,也就是从链表中把结点取出来,重新链接链表 node = pre_node_of_key->_next;//key对应的结点pre_node_of_key->_next = node->_next;if(pre_node_of_key->_next!=NULL){umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key;//更新前驱 }node->_value = value; //重置结点值/*更新_last*/if(_last == key ){_last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); }}else{//结点不存在if(_capacity == 0){//缓冲区为空return ;}if(_size == _capacity){//缓存满,重用最后一个结点 Node* pre_node_of_last = umap_prenodes[_last];//最后一个结点的前驱结点 umap_prenodes[pre_node_of_last->_next->_key] = NULL;node = new (pre_node_of_last->_next) Node(key,value,NULL);//重用最后一个结点 pre_node_of_last->_next = NULL;//移出最后一个结点 _last = ( pre_node_of_last == _head ? key : pre_node_of_last->_key ); //更新指向最后一个结点的key }else{//缓冲未满,使用新结点 node = new (_cur_begin) Node(key,value,NULL);_cur_begin += sizeof(Node);_size++;if(_size==1){_last = key;}}}/*把node插入到第一个结点的位置*/node->_next = _head->_next;if(node->_next!=NULL){//node有后继,更新后继的前驱结点umap_prenodes[node->_next->_key] = node;}_head->_next = node;umap_prenodes[key] = _head; }private:int _size;int _capacity;int _last;//_last是链表中最后一个结点的keyNode* _head;unordered_map<int,Node*> umap_prenodes;//存储key对应的结点的前驱结点,链表中第一个结点的前驱结点为_headchar* _begin;//缓存的起始位置 char* _cur_begin;//用于分配结点内存的起始位置 };
实现2,使用stl的list
这个版本的实现来自LeetCode discuss
class LRUCache{size_t m_capacity;unordered_map<int, list<pair<int, int>>::iterator> m_map; //m_map_iter->first: key, m_map_iter->second: list iterator;list<pair<int, int>> m_list; //m_list_iter->first: key, m_list_iter->second: value; public:LRUCache(size_t capacity):m_capacity(capacity) {}int get(int key) {auto found_iter = m_map.find(key);if (found_iter == m_map.end()) //key doesn't existreturn -1;m_list.splice(m_list.begin(), m_list, found_iter->second); //move the node corresponding to key to frontreturn found_iter->second->second; //return value of the node }void set(int key, int value) {auto found_iter = m_map.find(key);if (found_iter != m_map.end()) //key exists {m_list.splice(m_list.begin(), m_list, found_iter->second); //move the node corresponding to key to frontfound_iter->second->second = value; //update value of the nodereturn;}if (m_map.size() == m_capacity) //reached capacity {int key_to_del = m_list.back().first; m_list.pop_back(); //remove node in list;m_map.erase(key_to_del); //remove key in map }m_list.emplace_front(key, value); //create new node in listm_map[key] = m_list.begin(); //create correspondence between key and node } };
通过两个版本的实现,可以看到,使用stl的容器代码非常简洁,但也不是说自定义链表版本的实现就不好,如果从并发的角度来说,自定义的结构,在实现并发时,锁的粒度会小一点,而直接使用stl容器,锁的粒度为大一点,因为,使用stl,必须锁定一个函数,而使用自定义结构可以只锁定某个函数内部的某些操作,而且更方便实现无锁并发。另外,从leetcode的测试结果来看,这两个版本的性能差不多。
转载于:https://www.cnblogs.com/zengzy/p/5167827.html
简单的LRU Cache设计与实现相关推荐
- 一种简单的LRU cache设计 C++
最近在工作中需要用到LRU cache用作缓存来提高性能,经过查阅各种资料,了解了其运行的机制,如下: LRU cache可以用于在内存中保持当前的热点数据,下面实现一个有大小限制的lru cache ...
- 如何设计LRU Cache算法
前言 相信有的伙伴在面试的过程中,或多或少的会被问到redis的内存淘汰策略,可能大部分人都知道都有哪些对应的策略,毕竟对于八股文的套路大家肯定早已铭记于心.但是当面试官问你如何实现或者让你去写一个对 ...
- LeetCode:146_LRU cache | LRU缓存设计 | Hard
题目:LRU cache Design and implement a data structure for Least Recently Used (LRU) cache. It should su ...
- 使用线程安全型双向链表实现简单 LRU Cache 模拟
使用线程安全型双向链表实现简单 LRU Cache 模拟 目录
- 单机 “5千万以上“ 工业级 LRU cache 实现
文章目录 前言 工业级 LRU Cache 1. 基本架构 2. 基本操作 2.1 insert 操作 2.2 高并发下 insert 的一致性/性能 保证 2.3 Lookup操作 2.4 shar ...
- 代码写对了还挂了?程序媛小姐姐从 LRU Cache 带你看面试的本质
来源 | 码农田小齐 责编 | Carol 前言 在讲这道题之前,我想先聊聊「技术面试究竟是在考什么」这个问题. 技术面试究竟在考什么 在人人都知道刷题的今天,面试官也都知道大家会刷题准备面试,代码 ...
- 从 LRU Cache 带你看面试的本质
前言 在讲这道题之前,我想先聊聊「技术面试究竟是在考什么」这个问题. 技术面试究竟在考什么 在人人都知道刷题的今天,面试官也都知道大家会刷题准备面试,代码大家都会写,那面试为什么还在考这些题?那为什么 ...
- linux cache lru回收,LRU cache 算法
上周末同学问了一些操作系统的问题,涉及到LRU cache,顺便复习了一下. LRU是least recently used的缩写,意思是最近最少使用,是一种内存页面置换算法.根据程序设计局部性的原则 ...
- 【LeetCode】LRU Cache 解决报告
插话:只写了几个连续的博客,博客排名不再是实际"远在千里之外"该.我们已经进入2一万内. 再接再厉.油! Design and implement a data structure ...
最新文章
- 函数初识(文字总结)
- docker WARNING: bridge-nf-call-iptables is disabled 处理
- 如何在虚拟机上安装wsus服务器,如何在Hyper-V虚拟机上安装WSUS服务器技巧
- 从0到1写RT-Thread内核——临界段的保护
- 图像处理-图像增强(二)
- java 反射 静态成员_java 利用反射获取内部类静态成员变量的值
- 项目管理团队建设成功经验
- 关于UIControl响应事件说明
- 武士2复仇 Unity游戏工程+源码
- 寻找肇事汽车车牌号C语言,北京交通大学C语言综合程序的设计(黄宇班).doc
- 程序员前景一片灰暗?网友:不行找个班上吧
- Jeston NX ubuntu 搜狗拼音输入法安装
- java lifo_java:stack栈: Stack 类表示后进先出(LIFO)的对象堆栈
- 一些webGL地球的网址
- 金立Android版本,金立amigo为国内首个安卓5.0手机操作系统
- 情人节适合送礼的数码好物有哪些?心意满满的数码好物清单
- xmanager 5下载安装
- [Ubuntu 16.04] [Memos] install jupyterlab
- [DLX]HDOJ4069 Squiggly Sudoku
- [病毒分析]熊猫烧香(中)病毒释放机理
热门文章
- ASP.NET TreeView控件各个节点总是居中对齐,而不是左对齐的问题
- 订单管理中根据订单来源批量修改服务部门
- Python多线程报错之RuntimeError
- LeetCode(257)——二叉树的所有路径(JavaScript)
- LeetCode(976)——三角形的最大周长(JavaScript)
- Hbuilder启动夜神游模拟器失败,解决方案
- 计算机网络---计算机网络分层结构
- jQuery学习(一)—jQuery应用步骤以及ready事件和load事件的区别
- vue引入外部文件_vue3+typescript引入外部文件
- 一次性补缴17万元办社保,每月可以领1400多,可否办理?