要求:
设计并实现一个LRU缓存的数据结构,支持get和set操作

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 = ;
_last = ;
_cur_begin = _begin = (char *) malloc(sizeof(Node)*(capacity+));
_head = new (_cur_begin) Node(,,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 = -;//初始时假设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 == ){//缓冲区为空
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==){
_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是链表中最后一个结点的key
Node* _head;
unordered_map<int,Node*> umap_prenodes;//存储key对应的结点的前驱结点,链表中第一个结点的前驱结点为_head char* _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 exist
return -;
m_list.splice(m_list.begin(), m_list, found_iter->second); //move the node corresponding to key to front
return 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 front
found_iter->second->second = value; //update value of the node
return;
}
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 list
m_map[key] = m_list.begin(); //create correspondence between key and node
}
};

通过两个版本的实现,可以看到,使用stl的容器代码非常简洁,但也不是说自定义链表版本的实现就不好,如果从并发的角度来说,自定义的结构,在实现并发时,锁的粒度会小一点,而直接使用stl容器,锁的粒度为大一点,因为,使用stl,必须锁定一个函数,而使用自定义结构可以只锁定某个函数内部的某些操作,而且更方便实现无锁并发。另外,从leetcode的测试结果来看,这两个版本的性能差不多。

简单的LRU Cache设计与实现的更多相关文章

  1. java基于Hash表和双向链表简单实现LRU Cache

    package lru; import java.util.HashMap; public class LRUCache2<K,V> { public final int capacity ...

  2. LRU Cache

    LRU Cache 题目链接:https://oj.leetcode.com/problems/lru-cache/ Design and implement a data structure for ...

  3. LeetCode之LRU Cache 最近最少使用算法 缓存设计

    设计并实现最近最久未使用(Least Recently Used)缓存. 题目描述: Design and implement a data structure for Least Recently ...

  4. LeetCode题解: LRU Cache 缓存设计

    LeetCode题解: LRU Cache 缓存设计 2014年12月10日 08:54:16 邴越 阅读数 1101更多 分类专栏: LeetCode   版权声明:本文为博主原创文章,遵循CC 4 ...

  5. 如何设计一个LRU Cache

    如何设计一个LRU Cache? Google和百度的面试题都出现了设计一个Cache的题目,什么是Cache,如何设计简单的Cache,通过搜集资料,本文给出个总结. 通常的问题描述可以是这样: Q ...

  6. LRU cache缓存简单实现

    LRU cache LRU(最近最少使用)是一种常用的缓存淘汰机制.当缓存大小容量到达最大分配容量的时候,就会将缓存中最近访问最少的对象删除掉,以腾出空间给新来的数据. 实现 (1)单线程简单版本 ( ...

  7. LRU Cache的简单c++实现

    什么是 LRU LRU Cache是一个Cache的置换算法,含义是“最近最少使用”,把满足“最近最少使用”的数据从Cache中剔除出去,并且保证Cache中第一个数据是最近刚刚访问的,因为这样的数据 ...

  8. LeetCode:146_LRU cache | LRU缓存设计 | Hard

    题目:LRU cache Design and implement a data structure for Least Recently Used (LRU) cache. It should su ...

  9. 设计并实现一个LRU Cache

    一.什么是Cache 1 概念 Cache,即高速缓存,是介于CPU和内存之间的高速小容量存储器.在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器.其容量远小于内存,但速度却可以接近CP ...

随机推荐

  1. 关于js中的事件

    这是第一篇技术性博客. 因为最近做的web版前端求职简历算是告一段落了(点此看简历).(稍微记录下吧:自从确定简历的简笔画风格后(因为刚开始设想的蓝天白云大树啥的不仅图片特难找而且做着做着就觉得有点俗 ...

  2. C#必须掌握的系统类

    系统类  Type类,Object类,String类, Arrary类,Console类, Exception类,GC类, MarshalByRefObject类, Math类. DateTime结构 ...

  3. iOS开发中的Html解析方法

    iOS开发中的Html解析方法 本文作者为大家介绍了在iOS开发中的Html解析方法,并同时提供了Demo代码的下载链接,Demo 解析了某个网站(具体可在代码中查看)的html网页,提取了图片以及标 ...

  4. pcduino连接OTG登录远程桌面

    由于没有HDMI的显示屏,为了方便起见,使用了pcduino的OTG来连接到虚拟桌面,可是发现连接上虚拟桌面后,电脑的外网就断了.下面这个方法让你既可以连接到pcduino,又可以让电脑能上外网. 打 ...

  5. 使用CPU的AVX指令

    arch:AVX 很抱歉GCC还不行……有……倒是 但是不是这么写的 我忘记了……官网上有 http://www.oschina.net/news/66980/kreogist-0-9

  6. logstash tag使用说明

    zjtest7-frontend:/usr/local/logstash-2.3.4/config# cat stdin04.conf input { stdin { } } filter { # d ...

  7. Jsvc安装,配置 常规用户使用tomcat的80端口

     Jsvc安装 一.下载安装包,地址如下: http://commons.apache.org/proper/commonsdaemon/download_daemon.cgi 二.安装步骤,参考链接 ...

  8. 【转】 ubuntu12.04更新源

    原文网址:http://blog.chinaunix.net/uid-26404477-id-3382633.html 摘 要:本文列出ubuntu 12.04 LTS更新源列表,内容为网友整理,此处 ...

  9. 2015第23周一SVN插件安装

    之前想把eclipse(3.7)的SVN插件版本从1.8.x降到1.6.x,上午折腾了好久没弄好,先是尝试在线安装,按网上说的1.6.x的url安装不成功(可能是网络问题,下载不到资源),然后尝试下载 ...

  10. 专注于HTTP的高性能高易用性网络库:Fslib.network库

    博客列表页:http://blog.fishlee.net/tag/fslib-network/ 原创FSLib.Network库(目前专注于HTTP的高性能高易用性网络库) FSLib.Networ ...