上周末同学问了一些操作系统的问题,涉及到LRU cache,顺便复习了一下。
LRU是least recently used的缩写,意思是最近最少使用,是一种内存页面置换算法。根据程序设计局部性的原则,如果一个页面在最近的一段时间一直没有被使用,那这个页面在将来一段时间内不会被用到的概率就比较大;同样的,如果一个页面最近频繁的被使用,那它在接下来也有很大概率会被用到(例如循环)。
LRU 算法的基本原理很简单,为每个物理页面绑定一个计数器,用以标识该页面的访问频度。操作系统内核进行页面回收的时候就可以根据页面的计数器的值来确定要回收哪些页面。然而,在硬件上提供这种支持的体系结构很少,Linux 操作系统没有办法依靠这样一种页计数器去跟踪每个页面的访问情况,所以,Linux 在页表项中增加了一个 Accessed 位,当页面被访问到的时候,该位就会被硬件自动置位。该位被置位表示该页面还很年轻,不能被换出去。此后,在系统的运行过程中,该页面的年龄会被操作系统更改。在 Linux 中,相关的操作主要是基于两个 LRU 链表以及两个标识页面状态的标志符。
后来Leetcode根据LRU cache出了一道【146. LRU Cache】,也因此使得这个算法被更多非体系结构方向的人所知。不过个人感觉这道#146的题目远没有Hard难度。
背景介绍到此结束,先看题:
Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get
and set.
get(key)
Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.set(key, value)
Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.
这道题的就是要实现一个LRU支持set和get的函数,分析一下:
如果直接用数组来实现LRU,数组的每个位置除了保存数据外维护一个时间戳,这样做比较简单,但是插入、删除都会以O(n)的复杂度去维护这个时间戳。
更常用的做法是使用链表和HashMap。每次访问数据线检查HashMap,如果命中,则通过hash函数找到在链表的位置,pop出来追加到表尾;如果没命中,则取决于当前链表长度,如果长度小于capacity,直接追加到表尾,如果长度已经达到上限了,则要把表头元素pop出去,并把新元素追加到表尾,最后更新HashMap。
啊,下面是code时间,最近写Python比较多,还是以Python实现为例吧(可能真实情况是...半年没用过C++,代码不会写了/(ㄒoㄒ)/~~)。
首先是链表节点的结构,比较经典,记住就好了:
class LinkedNode(object):
def __init__(self, key=None, value=None, next=None):
self.key = key
self.value = value
self.next = next
接下来是LRU的对象,应该保存一个哈希表(对于python来说常用的实现好像就是字典TAT),一对表头、表尾,还要有一个标记cache容量的capacity变量:
class LRUCache(object):
def __init__(self, capacity):
self.hash_table = {}
self.head = LinkedNode()
self.tail = self.head
self.capacity = capacity
数据结构实现之后应该清楚出了题目要求的get和set,应该实现一个visit()函数以及_pop()、_push()两个内部工具函数,其中当cache容量满了请求未命中的时候,_pop()负责把表头元素弹出去,类似的,_push(node)将未命中的元素追加到表尾。
def push(self, node):
self.hash_table[node.key] = self.tail
self.tail.next = node
self.tail = node
def pop(self):
del self.hash_table[self.head.next.key]
self.head.next = self.head.next.next
self.hash_table[self.head.next.key] = self.head
def visit(self, prev):
node = prev.next
if node == self.tail:
return
prev.next = node.next
if node.next:
self.hash_table[node.next.key] = prev
node.next = None
self.push(node)
def get(self, key):
if key not in self.hash_table.keys():
return -1
self.visit(self.hash_table[key])
return self.hash_table[key].next.value
def set(self, key, value):
if key in self.hash_table.keys():
self.visit(self.hash_table[key])
self.hash_table[key].next.value = value
else:
self.push(LinkedNode(key, value))
if len(self.hash_table) > self.capacity:
self.pop()
leetcode提交后跑了1296 ms...果然还是比cxx慢好多...
然后给出用C++ standard STL的实现版本,63ms:
class LRUCache{
size_t m_capacity;
unordered_map>::iterator> m_map; //m_map_iter->first: key, m_map_iter->second: list iterator;
list> 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 -1;
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
}
};