LRU Cache替换算法

目录

1.什么是LRU Cache?

2.LRU Cache 的底层结构

3.LRU Cache的实现

LRUCache类中的接口总览

构造函数

get操作

 put操作

打印

4.LRU Cache的测试

5.LRU Cache相关OJ题

6.LRU Cache类代码附录


1.什么是LRU Cache?

首先我想解释一下什么是cache。所谓cache,其实就是一段缓冲区,位于运行速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构。

比如计算机的存储结构:

我们知道程序都是在磁盘上存储的,但是程序都要放在CPU上执行,CPU的执行速度非常快,而磁盘是外设,代码从外设搬到CPU上的速度又非常慢,此时,CPU大部分时间都在等待代码和数据的到来,这样无疑是对CPU的浪费;于是在外设和CPU之间添加Cache 缓存用于进行预加载;CPU每次不再向磁盘 “索要” 要代码和数据,而是从Cache上拿,CPU还在执行当前的代码和数据时,磁盘上的代码和数据又可以往Cache上加载,于是,便协调了CPU和磁盘之间的数据加载不平衡的问题。

旧的问题解决了,又产生了新的问题:

Cache的容量是有限的,因此当Cache的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容,那应该舍弃那部分内容呢?经过科学家的研究,认为舍弃最近最少使用(Least Recently Used过的数据是最合理的,也就是舍弃LRU数据。

上面说的都是硬件层的东西,但是需要通过软件层的算法来解决,也就是设计如何舍弃最近最少使用的数据,这便是LRU Cache 替换算法

2.LRU Cache算法的底层结构

要设计出一个LRU Cache替换算法不难,但是要设计出一个高效的LRU Cache替换算法有难度,高效体现在任意操作的时间复杂度都是O(1)

说明一下:我们设计的LRU Cache中存储的数据为键值对的形式。

那LRU Cache替换算法都有哪些操作呢?其实也就两个主要操作。

  • 一个操作是往Cache中放数据(put操作)。要求:如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity (Cache的容量),则应该 逐出 最久未使用的关键字。
  • 一个操作是把数据从Cache中拿出来(get操作)。要求:如果关键字 key 存在于缓存中,则返回关键字的值,否则返回默认值并提示。

如何使这两个操作的时间复杂度都达到O(1)呢?此时,底层数据结构的选择尤为重要。

  • 无论是get还是put操作,都需要快速根据关键字的值查找一个元素是否存在于 Cache 中,我们知道,查找一个元素时间复杂度为O(1)的数据结构是哈希表,因此我们可以选择STL中的unordered_map作为其底层结构。
  • 但是这还不够,我们需要频繁的进行数据的增加和删除操作,并且要求数据的增加和删除操作的效率都是O(1)。因此,我们还可以借助STL中的 list(带头双向循环链表)来存储数据。
  • 同时,因为我们还需要保证Cache满了之后,替换的是最近最少使用的数据,我们可以让链表的末尾存放最近最少使用的数据,每次替换的时候删除末尾的数据即可。为了实现这一点,我们只需要将每次访问过的数据提取到 list 的头部即可,尾部自然而然就是最近最少使用的数据。

LRU Cache的底层存储示意图:

3.LRU Cache算法的实现

LRUCache类中的接口总览

说明一下:我们将LRUCache类设计为模版类,以便适应各种数据类型。

template<class Key, class Val>
class LRUCache
{
public:
	// 构造函数
	LRUCache(int capacity)
		:_capacity(capacity)
	{}

	// 通过key获取对应的val
	Val get(Key key);

	// 往Cache中插入<key,value>
	void put(Key key, Val value);
    
    // 输出list中的结点,便于调试分析
	void print();

private:
	// 对list的迭代器类型进行重命名
	using LtIte = typename std::list<std::pair<Key, Val>>::iterator;

	// 保证查找的效率是O(1)
	std::unordered_map<Key, LtIte> _hashMap;

	// 保证插入删除数据的效率是O(1)
	std::list<std::pair<Key, Val>> _LRU_list;
	
	// Cache的容量
	size_t _capacity;
};

构造函数

直接指定LRUCache中的容量即可。

// 构造函数
LRUCache(int capacity)
	:_capacity(capacity)
{}

get操作

  • 首先通过哈希表判断key是否存在,如果存在则返回对应的value,并且将访问过的元素提取到list的开头。
  • 如果不存在,返回该类型通过默认构造函数构造出的对象即可,并提示该元素不存在。

代码如下所示:

// 通过key获取对应的val
Val get(Key key)
{
	auto ret = _hashMap.find(key);
	if (ret != _hashMap.end())   // 获取val的同时,更改key所在结点的位置
	{
		// 获取元素在list中的结点
		LtIte it = ret->second;

		// 使用list的splice接口将当前访问的结点转移到链表的开头
		_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);

		// 返回list结点中val
		return it->second;
	}
		
	std::cout << "该元素不存在" << std::endl;

	return Val();
}
  • 注意:这里将访问过的结点提取到链表的开头也可以通过erase+push_front来实现,但是需要更新迭代器,防止迭代器失效的问题。因此,我们使用操作更加简单的list的splice接口。

 put操作

  • put数据的时候,需要判断该数据的key值是否存在,如果存在则更新对应的value,如果不存在则插入,并且两种情况都视为访问过当前结点,需要将访问过的结点转移到list的开头位置。
  • 在该数据不存在的情况下,需要判断Cache中数据是否满了,满了就要删除list末尾的数据(LRU数据)。
// 往Cache中插入<key,value>
void put(Key key, Val value)
{
	auto ret = _hashMap.find(key);
		
	if (ret == _hashMap.end()) // key不存在,需要插入<key,value>
	{
		// 如果满了,就要删除LRU的数据
		if (_capacity == _hashMap.size())
		{
			// 从list中获取尾部的数据
			std::pair<Key, Val> back_data = _LRU_list.back();
				
			// 删除哈希表中对应的数据
			_hashMap.erase(back_data.first);

			// 删除list中对应的数据
			_LRU_list.pop_back();
		}

		// 头插新来的<key,value>结点
		_LRU_list.push_front(std::make_pair(key, value));

		// 将新数据添加到哈希表中
		_hashMap[key] = _LRU_list.begin();
	}
	else  // list中存在key,需要更新key对应的value
	{
		// 获取list中结点的迭代器
		LtIte it = ret->second;

		// 修改结点中的value
		it->second = value;

		// 将访问过的结点转移到链表的头部
		_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);
	}
}

打印

这个接口不是必需的,只是为了检测、调试LRUCache类。

// 打印list中的数据
void print()
{
	for (auto e : _LRU_list)
	{
		std::cout << e.first << ":" << e.second << std::endl;
	}
}

4.LRU Cache算法的测试

测试代码:

#include <string>
#include "LRUCache.h"

int main()
{
	LRUCache<std::string, std::string> lc(5);

	lc.put("book", "书");
	lc.put("string", "字符串");
	lc.put("water", "水");
	lc.put("computer", "计算机");
	lc.put("glass", "玻璃");
	lc.print();
	std::cout << std::endl;

	lc.get("water");
	lc.print();
	std::cout << std::endl;

	lc.put("book", "预定");
	lc.print();
	std::cout << std::endl;

	return 0;
}

运行结果:

5.LRU Cache算法相关OJ题

题目链接:

146. LRU 缓存 - 力扣(LeetCode)146. LRU 缓存 - 请你设计并实现一个满足  LRU (最近最少使用) 缓存 [https://baike.baidu.com/item/LRU] 约束的数据结构。实现 LRUCache 类: * LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存 * int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 * void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。 示例:输入["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"][[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]输出[null, null, null, 1, null, -1, null, -1, 3, 4]解释LRUCache lRUCache = new LRUCache(2);lRUCache.put(1, 1); // 缓存是 {1=1}lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}lRUCache.get(1); // 返回 1lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}lRUCache.get(2); // 返回 -1 (未找到)lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}lRUCache.get(1); // 返回 -1 (未找到)lRUCache.get(3); // 返回 3lRUCache.get(4); // 返回 4 提示: * 1 <= capacity <= 3000 * 0 <= key <= 10000 * 0 <= value <= 105 * 最多调用 2 * 105 次 get 和 puticon-default.png?t=O83Ahttps://leetcode.cn/problems/lru-cache/题目描述:

请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

解题思路:

参考本篇博客即可。

运行代码:

class LRUCache {
    // 对list的迭代器类型进行重命名
	using LtIte = list<pair<int, int>>::iterator;
public:
	// 构造函数
	LRUCache(int capacity)
		:_capacity(capacity)
	{}

	// 通过key获取对应的val
	int get(int key)
	{
		auto ret = _hashMap.find(key);
		if (ret != _hashMap.end())   // 获取val的同时,更改key所在结点的位置
		{
			// 获取元素在list中的结点
			LtIte it = ret->second;

			// 使用list的splice接口将当前访问的结点转移到链表的开头
			_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);

			// 返回list结点中val
			return it->second;
		}
		
		std::cout << "该元素不存在" << std::endl;

		return -1;
	}

	// 往Cache中插入<key,value>
	void put(int key, int value)
	{
		auto ret = _hashMap.find(key);
		
		if (ret == _hashMap.end()) // key不存在,需要插入<key,value>
		{
			// 如果满了,就要删除LRU的数据
			if (_capacity == _hashMap.size())
			{
				// 从list中获取尾部的数据
				std::pair<int, int> back_data = _LRU_list.back();
				
				// 删除哈希表中对应的数据
				_hashMap.erase(back_data.first);

				// 删除list中对应的数据
				_LRU_list.pop_back();
			}

			// 头插新来的<key,value>结点
			_LRU_list.push_front(std::make_pair(key, value));

			// 将新数据添加到哈希表中
			_hashMap[key] = _LRU_list.begin();
		}
		else  // list中存在key,需要更新key对应的value
		{
			// 获取list中结点的迭代器
			LtIte it = ret->second;

			// 修改结点中的value
			it->second = value;

			// 将访问过的结点转移到链表的头部
			_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);
		}
	}

private:
	unordered_map<int, LtIte> _hashMap;

	list<pair<int, int>> _LRU_list;
	
	size_t _capacity;
};

运行结果:

6.LRU Cache类代码附录

#include <iostream>
#include <unordered_map>
#include <list>

template<class Key, class Val>
class LRUCache
{
public:
	// 构造函数
	LRUCache(int capacity)
		:_capacity(capacity)
	{}

	// 通过key获取对应的val
	Val get(Key key)
	{
		auto ret = _hashMap.find(key);
		if (ret != _hashMap.end())   // 获取val的同时,更改key所在结点的位置
		{
			// 获取元素在list中的结点
			LtIte it = ret->second;

			// 使用list的splice接口将当前访问的结点转移到链表的开头
			_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);

			// 返回list结点中val
			return it->second;
		}
		
		std::cout << "该元素不存在" << std::endl;

		return Val();
	}

	// 往Cache中插入<key,value>
	void put(Key key, Val value)
	{
		auto ret = _hashMap.find(key);
		
		if (ret == _hashMap.end()) // key不存在,需要插入<key,value>
		{
			// 如果满了,就要删除LRU的数据
			if (_capacity == _hashMap.size())
			{
				// 从list中获取尾部的数据
				std::pair<Key, Val> back_data = _LRU_list.back();
				
				// 删除哈希表中对应的数据
				_hashMap.erase(back_data.first);

				// 删除list中对应的数据
				_LRU_list.pop_back();
			}

			// 头插新来的<key,value>结点
			_LRU_list.push_front(std::make_pair(key, value));

			// 将新数据添加到哈希表中
			_hashMap[key] = _LRU_list.begin();
		}
		else  // list中存在key,需要更新key对应的value
		{
			// 获取list中结点的迭代器
			LtIte it = ret->second;

			// 修改结点中的value
			it->second = value;

			// 将访问过的结点转移到链表的头部
			_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);
		}
	}

	// 打印list中的数据
	void print()
	{
		for (auto e : _LRU_list)
		{
			std::cout << e.first << ":" << e.second << std::endl;
		}
	}

private:
	// 对list的迭代器类型进行重命名
	using LtIte = typename std::list<std::pair<Key, Val>>::iterator;

	// 保证查找的效率是O(1)
	std::unordered_map<Key, LtIte> _hashMap;

	// 保证插入删除数据的效率是O(1)
	std::list<std::pair<Key, Val>> _LRU_list;
	
	// Cache的容量
	size_t _capacity;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值