Least Recently Used Algorithm
LRU(_Least Recently Used)_算法是操作系统中的一种页面置换(在缓存系统中也会用到),思想就是:每次都把最近最少使用的那个页面置换出去,这个思想基于,当前使用的页面在不久的将来也会使用。
比如在内存为 3 的情况下,依次请求如下页面2,3,4,2,1,3,7,5,4,3.那么内存中保存的依次保存的页面会变成如下所示(每一行表示当前页面请求之后,内存中的页面情况,左边的页面比右边页面旧(也就是最后一次访问的时间早),这里有一个动态视频,给出每一次的情况(需要翻墙)
- 2
- 2 3
- 2 3 4
- 3 4 2
- 4 2 1
- 2 1 3
- 1 3 7
- 3 7 5
- 7 5 4
- 5 4 3
到这里基本想法就结束了,剩下的就是怎么实现的问题了。对于不同的要求,有不同的实现。
第一种:最简单的模拟,用一个单链表表示 LRU 的大小,表头存最旧的页面,表尾存最新的页面,然后每次 get 和 put 的时候,都遍历一次单链表进行相应操作。由于每次都要遍历单链表,所以每次操作都是 O(L)的复杂度,其中 L 表示 LRU 的大小。代码如下
typedef struct { int key; int val; } elem; class LRUCache{ public: elem *arr; // lru cache int sz; // total number of elements in the list currently. int cap; //capacity LRUCache(int capacity) { //init LRUCache arr = new elem[capacity]; // sz = 0; cap = capacity; } /* move the used element to the end of list */ void adjust(int a) { if (a == sz - 1) {//the last one return ; } elem cur = arr[a]; for (int i = a; i lt; sz - 1; i ++) { arr[i] = arr[i + 1]; // move all elements after position a 1 step left } arr[sz - 1] = cur; // move arr[a] to the end } //get the value of key, return -1 if it doesn't exit int get(int key) { //iterate the whole list to find if the key exits for (int i = 0; i lt; sz; i ++) { if (arr[i].key == key) { int a = arr[i].val; adjust(i); return a; // existent key } } return -1; } //update the key/value void set(int key, int value) { for (int i = 0; i lt; sz; i ++) { if (arr[i].key == key) { // existent arr[i].val = value; //update value ,and adjust the list adjust(i); return; } } if (sz == cap) { // check if reach the capacity for (int i = 0; i lt; sz - 1; i ++) { arr[i] = arr[i + 1]; // delete the least used element } arr[sz - 1].key = key; arr[sz - 1].val = value; } else { arr[sz].key = key; arr[sz].val = value; sz ++; // increase the size } } };
第二种写法就是用双链表存 LRU 中保存的实际内容,然后用 HASH 表保存每一个 key 所对应的内容在双链表中的位置,其中双链表还是表头存最旧的,表尾存最新的,用 HASH 就可以加速查找,用双链表则是更新的时候可以达到 O(1)[单链表不能获得前驱节点的信息],如果这里用 map 实现,而不是 hash_map 的话,那么复杂度是 log(L),这个是由 map 的复杂度决定的。代码如下:
#include#include #include using namespace std; using namespace stdext; template struct LRUCacheEntry { K key; T data; LRUCacheEntry* prev; LRUCacheEntry* next; }; template class LRUCache { private: hash_map< K, LRUCacheEntry<K,T>* > _mapping; vector< LRUCacheEntry<K,T>* > _freeEntries; LRUCacheEntry * head; LRUCacheEntry * tail; LRUCacheEntry * entries; public: LRUCache(size_t size){ entries = new LRUCacheEntry [size]; for (int i=0; i ; tail = new LRUCacheEntry ; head->prev = NULL; head->next = tail; tail->next = NULL; tail->prev = head; } ~LRUCache() { delete head; delete tail; delete [] entries; } void put(K key, T data) { LRUCacheEntry * node = _mapping[key]; if(node) { // refresh the link list detach(node); node->data = data; attach(node); } else{ if ( _freeEntries.empty() ) {// lru cache is full node = tail->prev; detach(node);//delete a node _mapping.erase(node->key); node->data = data; node->key = key; _mapping[key] = node; attach(node);//add the new node } else{ node = _freeEntries.back(); _freeEntries.pop_back(); node->key = key; node->data = data; _mapping[key] = node; attach(node); } } } T get(K key) { LRUCacheEntry * node = _mapping[key]; if(node) {//if node is already in, refresh the double-link-list detach(node); attach(node); return node->data; } else return NULL; } private: void detach(LRUCacheEntry * node) {// delete the node from the double-link-list node->prev->next = node->next; node->next->prev = node->prev; } void attach(LRUCacheEntry * node) {//add node to the head of double-link-list node->next = head->next; node->prev = head; head->next = node; node->next->prev = node; } };
第二种方法利用双链表保存实际的 cache 内容,然后用 hash 来加速查找,hash 存的是每一个 key/value 的地址,这样就可以直接找到相应的 key/value 元素了。这种方法中,查找的复杂度是 O(1),更新的复杂度,只需要进行一次查找,一次 detach,一次 attach,所以也是 O(1)的,较之第一种方法的优势就体现出来了。
最后,如果你想看下自己写的 LRU 是否正确,速度如何,可以在 Leetcode 上进行提交,地址:https://oj.leetcode.com/problems/lru-cache/,提交之后可以查看是否正确,正确的话,查看用时多少(第一种方法,可能可以在 Leetcode 上通过,也可能会得到一个 Time Limit Exceeded 的结果,这个就看你人品了)
Reference