0%

leetcode-day-24

LeetCode 30 days Challenge - Day 24

本系列将对LeetCode新推出的30天算法挑战进行总结记录,旨在记录学习成果、方便未来查阅,同时望为广大网友提供帮助。


LRU Cache

Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and put.

get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(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.

The cache is initialized with a positive capacity.

Follow up:
Could you do both operations in O(1) time complexity?

Example:

1
2
3
4
5
6
7
8
9
10
11
LRUCache cache = new LRUCache( 2 /* capacity */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // returns 1
cache.put(3, 3); // evicts key 2
cache.get(2); // returns -1 (not found)
cache.put(4, 4); // evicts key 1
cache.get(1); // returns -1 (not found)
cache.get(3); // returns 3
cache.get(4); // returns 4

Solution

题目要求分析:要求设计一个缓存机制,并实现访问get操作、put插入操作。

  1. get操作:当元素不存在,返回-1
  2. put操作:当占满空间后再次插入内容时,将最近最少使用的元素删去,替换之。

解法:

本题的朴素解法较简单,本文介绍一种O(1)访存的设计:双向链表(带头尾指针)+ 哈希结构(键值->链表结点)

先分析,该设计如何实现O(1)的访存要求:

  1. get操作:按照键值访问哈希表是O(1)的,找到哈希表对应的元素后,访问结点的val元素即可。

  2. put操作:

    1. 检测是否已存在该键值(unordered_map的find函数实现如下,是O(1)的)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
      _H1, _H2, _Hash, _RehashPolicy, _Traits>::
      find(const key_type& __k) const
      -> const_iterator
      {
      __hash_code __code = this->_M_hash_code(__k);
      std::size_t __n = _M_bucket_index(__k, __code);
      __node_type* __p = _M_find_node(__n, __k, __code);
      return __p ? const_iterator(__p) : end();
    2. 检测缓存是否已满是O(1)的。

    3. 在哈希表中删除(erase)是O(1)的。

    4. 在链表尾部删除结点、头部插入结点也是O(1)的。

综上,能保证是O(1)的访存。

接下来介绍实现中需要注意的几点:

  1. 删除LRU数据时依据是:通过双向链表尾结点的前一个结点,确定哈希表应该删除哪一表项。因此,链表结点除了记录数据之外,还要记录键值。m.erase(m.find(rear->pre->key)); // 哈希表的删除语句
  2. put操作需要进行的判断有三种情况,勿遗漏:
    1. 缓存已满,插入的键值不存在:删除LRU,在头部插入新键值对(结点)。
    2. 插入的键值已存在,此时不需考虑缓存是否已满:查找键值对应数据,更新为新值,并移动到头部。
    3. 插入的键值不存在,缓存未满的情况:直接在同步插入新键值对(结点)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class LRUCache {
public:
// 双向链表结点定义
struct ListNode {
int key;
int val;
ListNode* pre;
ListNode* next;
ListNode(int x, int y) : key(x), val(y), pre(NULL), next(NULL) {}
};

int cap; // 容量
ListNode* head = new ListNode(0, 0); // 头指针
ListNode* rear = new ListNode(0, 0); // 尾指针
unordered_map<int, ListNode*> m; // 哈希表

// 构造函数
LRUCache(int capacity) {
cap = capacity;
head->next = rear;
rear->pre = head;
}

// 移动结点到头部
void move2front(ListNode* node) {
node->next->pre = node->pre;
node->pre->next = node->next;
node->next = head->next;
node->pre = head;
head->next->pre = node;
head->next = node;
}

// 删除LRU
void removeLRU() {
ListNode* tmp = rear->pre;
rear->pre = tmp->pre;
tmp->pre->next = rear;
delete tmp;
}

// 在头部插入新结点
ListNode* insertNode(int key, int val) {
ListNode* tmp = new ListNode(key, val);
tmp->next = head->next;
tmp->pre = head;
head->next->pre = tmp;
head->next = tmp;
return head->next;
}

// get 访问操作
int get(int key) {
if (m.find(key) != m.end()) {
move2front(m[key]);
return m[key]->val;
}
else {
return -1;
}
}

// put 插入键值对
void put(int key, int value) {
if (m.find(key) == m.end() && m.size() == cap) {
m.erase(m.find(rear->pre->key));
removeLRU();
m[key] = insertNode(key, value);
}
else if (m.find(key) != m.end()) {
m[key]->val = value;
move2front(m[key]);
}
else {
m[key] = insertNode(key, value);
}
}
};

传送门:LRU Cache

Karl