要求:
设计并实现一个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       = 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设计与实现相关推荐

  1. 一种简单的LRU cache设计 C++

    最近在工作中需要用到LRU cache用作缓存来提高性能,经过查阅各种资料,了解了其运行的机制,如下: LRU cache可以用于在内存中保持当前的热点数据,下面实现一个有大小限制的lru cache ...

  2. 如何设计LRU Cache算法

    前言 相信有的伙伴在面试的过程中,或多或少的会被问到redis的内存淘汰策略,可能大部分人都知道都有哪些对应的策略,毕竟对于八股文的套路大家肯定早已铭记于心.但是当面试官问你如何实现或者让你去写一个对 ...

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

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

  4. 使用线程安全型双向链表实现简单 LRU Cache 模拟

    使用线程安全型双向链表实现简单 LRU Cache 模拟 目录

  5. 单机 “5千万以上“ 工业级 LRU cache 实现

    文章目录 前言 工业级 LRU Cache 1. 基本架构 2. 基本操作 2.1 insert 操作 2.2 高并发下 insert 的一致性/性能 保证 2.3 Lookup操作 2.4 shar ...

  6. 代码写对了还挂了?程序媛小姐姐从 LRU Cache 带你看面试的本质

    来源 | 码农田小齐 责编 |  Carol 前言 在讲这道题之前,我想先聊聊「技术面试究竟是在考什么」这个问题. 技术面试究竟在考什么 在人人都知道刷题的今天,面试官也都知道大家会刷题准备面试,代码 ...

  7. 从 LRU Cache 带你看面试的本质

    前言 在讲这道题之前,我想先聊聊「技术面试究竟是在考什么」这个问题. 技术面试究竟在考什么 在人人都知道刷题的今天,面试官也都知道大家会刷题准备面试,代码大家都会写,那面试为什么还在考这些题?那为什么 ...

  8. linux cache lru回收,LRU cache 算法

    上周末同学问了一些操作系统的问题,涉及到LRU cache,顺便复习了一下. LRU是least recently used的缩写,意思是最近最少使用,是一种内存页面置换算法.根据程序设计局部性的原则 ...

  9. 【LeetCode】LRU Cache 解决报告

    插话:只写了几个连续的博客,博客排名不再是实际"远在千里之外"该.我们已经进入2一万内. 再接再厉.油! Design and implement a data structure ...

最新文章

  1. 函数初识(文字总结)
  2. docker WARNING: bridge-nf-call-iptables is disabled 处理
  3. 如何在虚拟机上安装wsus服务器,如何在Hyper-V虚拟机上安装WSUS服务器技巧
  4. 从0到1写RT-Thread内核——临界段的保护
  5. 图像处理-图像增强(二)
  6. java 反射 静态成员_java 利用反射获取内部类静态成员变量的值
  7. 项目管理团队建设成功经验
  8. 关于UIControl响应事件说明
  9. 武士2复仇 Unity游戏工程+源码
  10. 寻找肇事汽车车牌号C语言,北京交通大学C语言综合程序的设计(黄宇班).doc
  11. 程序员前景一片灰暗?网友:不行找个班上吧
  12. Jeston NX ubuntu 搜狗拼音输入法安装
  13. java lifo_java:stack栈: Stack 类表示后进先出(LIFO)的对象堆栈
  14. 一些webGL地球的网址
  15. 金立Android版本,金立amigo为国内首个安卓5.0手机操作系统
  16. 情人节适合送礼的数码好物有哪些?心意满满的数码好物清单
  17. xmanager 5下载安装
  18. [Ubuntu 16.04] [Memos] install jupyterlab
  19. [DLX]HDOJ4069 Squiggly Sudoku
  20. [病毒分析]熊猫烧香(中)病毒释放机理

热门文章

  1. ASP.NET TreeView控件各个节点总是居中对齐,而不是左对齐的问题
  2. 订单管理中根据订单来源批量修改服务部门
  3. Python多线程报错之RuntimeError
  4. LeetCode(257)——二叉树的所有路径(JavaScript)
  5. LeetCode(976)——三角形的最大周长(JavaScript)
  6. Hbuilder启动夜神游模拟器失败,解决方案
  7. 计算机网络---计算机网络分层结构
  8. jQuery学习(一)—jQuery应用步骤以及ready事件和load事件的区别
  9. vue引入外部文件_vue3+typescript引入外部文件
  10. 一次性补缴17万元办社保,每月可以领1400多,可否办理?