C++ 实现LRU缓存机制

对于开发者而言,缓存的接触必不可少,无论是浏览器缓存(如果是chrome浏览器,可以通过chrome:://cache查看),还是服务端的缓存(通过memcached或者redis等内存数据库)。缓存不仅可以加速用户的访问,同时也可以降低服务器的负载和压力。那么,了解常见的缓存淘汰算法的策略和原理就显得特别重要。

常见的缓存算法:

  • LRU (Least recently used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。
  • LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。
  • FIFO (Fist in first out) 先进先出, 如果一个数据最先进入缓存中,则应该最早淘汰掉。

LRU缓存:

关于LRU缓存的原理,很多资料已经介绍的非常全面了,这里不再赘述。如:

https://zhuanlan.zhihu.com/p/34133067--LRU原理和Redis实现——一个今日头条的面试题

https://www.cnblogs.com/cpselvis/p/6272096.html--常见缓存算法和LRU的C++实现

设计思路:

C++ 实现LRU缓存机制_第1张图片

  如上图所示:我们可以使用hash表来存储key值,这样可以实现get(key)和put(key)两种操作的时间复杂度都为O(1),而hash表中的value指向双向链表实现的 LRU 的 Node 节点。这里采用双向链表的原因是:如果采用普通的单链表,则删除节点的时候需要从表头开始遍历查找,效率为O(n),采用双向链表可以直接改变节点的前驱的指针指向进行删除达到O(1)的效率。

  下面的图演示了它的原理。其中 head 代表双向链表的表头,tail 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。

C++ 实现LRU缓存机制_第2张图片

所以这里用到的数据结构为:

unordered_map:相较于map,底层实现为hash表,可以以O(1)时间复杂度实现数据的查询

通过ListNode来自己构造的双向链表

LRU算法的主要操作流程为:

put(key, value),首先在 Hash找到 Key 对应的节点。如果节点存在,更新节点的值,并把这个节点移到队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 Hash中移除 Key。

get(key),通过 Hash找到 LRU 链表节点,因为根据LRU 原理,这个节点是最新访问的,所以要把节点插入到队头,然后返回缓存的值。

下面看一下leetcode 146 LRU缓存机制的一道题目:

运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

进阶:

你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 该操作会使得密钥 2 作废
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得密钥 1 作废
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4

代码如下:

class LRUCache {
public:
	struct ListNode {//使用结构体建立双向链表,包含前驱、后继、key-value和构造函数
		ListNode *pre;
		ListNode *next;
		int key;
		int val;
		ListNode(int _key, int _val) : pre(NULL), next(NULL), key(_key), val(_val) {};
	};

	LRUCache(int capacity) : max_cnt(capacity), cnt(0) {
		head = new ListNode(-1, -1);
		tail = new ListNode(-1, -1);
		head->next = tail;//首尾相接
		tail->pre = head;
	}

	void update(ListNode *p) {//更新链表
		if (p->next == tail) 
            return;
        //将p与前后连接断开
		p->pre->next = p->next;
		p->next->pre = p->pre;
        //将p插入尾节点
		p->pre = tail->pre;
		p->next = tail;
		tail->pre->next = p;
		tail->pre = p;
	}

	int get(int key) {//获取值
		unordered_map::iterator it = m.find(key);
		if (it == m.end()) 
            return -1;
		ListNode *p = it->second;
        //提取p的value后更新p
		update(p);
		return p->val;
	}

	void put(int key, int value) {
		if (max_cnt <= 0) 
            return;
		unordered_map::iterator it = m.find(key);//查找key值是否存在
        //先延长链表再判断,如果超出,则删除节点
		if (it == m.end()) {//如果不存在,则放在双向链表头部,即链表尾
			ListNode *p = new ListNode(key, value);//初始化key和value
			m[key] = p;//建立新的map
            //在尾部插入新节点
			p->pre = tail->pre;
            tail->pre->next = p;
            tail->pre = p;
			p->next = tail;
			cnt++;//计数+1
			if (cnt > max_cnt) {//如果计数大于了缓存最大值
                //删除头结点
				ListNode *pDel = head->next;
				head->next = pDel->next;
				pDel->next->pre = head;
                //在链表中删除后,需要在map中也删除掉
				unordered_map::iterator itDel = m.find(pDel->key);
				m.erase(itDel);
				//delete pDel;
				cnt--;
			}
		}
		else {//如果存在
			ListNode *p = it->second;//因为map的second存储的是key对应的链表地址,所以将其赋给p
			p->val = value;//计算p内存块中的value值
			update(p);//更新p
		}
	}

private:
	int max_cnt;//最大缓存数量
	int cnt;//缓存计数
	unordered_map m;//记录数据和其链表地址
	ListNode *head;//链表头
	ListNode *tail;//链表尾
};

 

你可能感兴趣的:(算法与数据结构,leetcode)