当前位置:   article > 正文

[LeetCode解题] -- LRU缓存机制_lru cache的实现伪代码

lru cache的实现伪代码

146. LRU缓存机制

LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used : 最近最少使用。

重写双向链表 + HashMap   [要求 put get 方法时间复杂度为O(1) ]

[ 题目描述 ]

  1. 运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
  2. 获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1
  3. 写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
  4. 进阶:
  5. 你是否可以在 O(1) 时间复杂度内完成这两种操作?
  6. 示例:
  7. LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
  8. cache.put(1, 1);
  9. cache.put(2, 2);
  10. cache.get(1); // 返回 1
  11. cache.put(3, 3); // 该操作会使得关键字 2 作废
  12. cache.get(2); // 返回 -1 (未找到)
  13. cache.put(4, 4); // 该操作会使得关键字 1 作废
  14. cache.get(1); // 返回 -1 (未找到)
  15. cache.get(3); // 返回 3
  16. cache.get(4); // 返回 4


[ 解题思路 ]

哈希链表。

问题:怎么保证“get 和 put 方法必须都是 O(1) 的时间复杂度”?

get 查找快 -- hashmap

put 插入快,删除快,有顺序之分 -- 双向链表

问题:“为什么必须要用双向链表”?

因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)

 

因为显然 cache 必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要在 cache 中查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。

那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。

 


[ 代码实现 ]
 

第一步 双链表的节点类

为了简化,key 和 val 都认为是 int 类型

  1. class Node {
  2. public int key, val;
  3. public Node next, prev;
  4. public Node(int k, int v) {
  5. this.key = k;
  6. this.val = v;
  7. }
  8. }

第二步 用Node 类型构建一个双链表。

        实现其【 头插 删除 删尾 获得size】 addFirst(Node x); remove(Node x); removeLast(); size();方法。将要实现的这几个需要的 API(这些操作的时间复杂度均为 O(1)。

  1. class DoubleList {
  2. // 在链表头部添加节点 x,时间 O(1)
  3. public void addFirst(Node x);
  4. // 删除链表中的 x 节点(x 一定存在)
  5. // 由于是双链表且给的是目标 Node 节点,时间 O(1)
  6. public void remove(Node x);
  7. // 删除链表中最后一个节点,并返回该节点,时间 O(1)
  8. public Node removeLast();
  9. // 返回链表长度,时间 O(1)
  10. public int size();
  11. }

到这里就能回答刚才“为什么必须要用双向链表”的问题了,因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。

第三步  双向链表和哈希表实现 get put方法

有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可。我们先把逻辑理清楚:

  1. // key 映射到 Node(key, val)
  2. HashMap<Integer, Node> map;
  3. // Node(k1, v1) <-> Node(k2, v2)...
  4. DoubleList cache;
  5. int get(int key) {
  6. if (key 不存在) {
  7. return -1;
  8. } else {
  9. 将数据 (key, val) 提到开头;
  10. return val;
  11. }
  12. }
  13. void put(int key, int val) {
  14. Node x = new Node(key, val);
  15. if (key 已存在) {
  16. 把旧的数据删除;
  17. 将新节点 x 插入到开头;
  18. } else {
  19. if (cache 已满) {
  20. 删除链表的最后一个数据腾位置;
  21. 删除 map 中映射到该数据的键;
  22. }
  23. 将新节点 x 插入到开头;
  24. map 中新建 key 对新节点 x 的映射;
  25. }
  26. }

第四步 伪代码逻辑实现

  1. class LRUCache {
  2. // key -> Node(key, val)
  3. private HashMap<Integer, Node> map;
  4. // Node(k1, v1) <-> Node(k2, v2)...
  5. private DoubleList cache;
  6. // 最大容量
  7. private int cap;
  8. public LRUCache(int capacity) {
  9. this.cap = capacity;
  10. map = new HashMap<>();
  11. cache = new DoubleList();
  12. }
  13. public int get(int key) {
  14. if (!map.containsKey(key))
  15. return -1;
  16. int val = map.get(key).val;
  17. // 利用 put 方法把该数据提前
  18. put(key, val);
  19. return val;
  20. }
  21. public void put(int key, int val) {
  22. // 先把新节点 x 做出来
  23. Node x = new Node(key, val);
  24. if (map.containsKey(key)) {
  25. // 删除旧的节点,新的插到头部
  26. cache.remove(map.get(key));
  27. cache.addFirst(x);
  28. // 更新 map 中对应的数据
  29. map.put(key, x);
  30. } else {
  31. if (cap == cache.size()) {
  32. // 删除链表最后一个数据
  33. Node last = cache.removeLast();
  34. map.remove(last.key);
  35. }
  36. // 直接添加到头部
  37. cache.addFirst(x);
  38. map.put(key, x);
  39. }
  40. }
  41. }

这里就能回答之前的问答题“为什么要在链表中同时存储 key 和 val,而不是只存储 val”,注意这段代码:

  1. if (cap == cache.size()) {
  2.     // 删除链表最后一个数据
  3.     Node last = cache.removeLast();
  4.     map.remove(last.key);
  5. }

当缓存容量已满,我们不仅仅要删除最后一个 Node 节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 Node 得到。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,就无法删除 map 中的键,造成错误。

至此,你应该已经掌握 LRU 算法的思想和实现了,很容易犯错的一点是:处理链表节点的同时不要忘了更新哈希表中对节点的映射。

第五步 完整代码

1. Node类。  int k,v ; Node next,prev;
2. DoubleList类-双向链表类。   int size  ; Node head,tail;
3. LRUCache 构造函数。  int cap;  DoubleList cache;    HashMap<Integer, Node> map;
4. get put 方法。

  1. class LRUCache {
  2. // Node 类
  3. class Node {
  4. public int key, val;
  5. public Node next, prev;
  6. public Node(int k, int v) {
  7. this.key = k;
  8. this.val = v;
  9. }
  10. }
  11. // DoubleList 类
  12. class DoubleList { // 对Node的操作,无 k v 操作
  13. private Node head, tail;
  14. private int size; // 双向链表 的实际size大小
  15. public void addFirst(Node node) {
  16. if (head == null) {
  17. head = tail = node;
  18. } else {
  19. Node n = head;
  20. n.prev = node;
  21. node.next = n;
  22. head = node;
  23. }
  24. size++;
  25. }
  26. public void remove(Node node) { // node tail head
  27. if (head == node && tail == node) {
  28. head = null;
  29. tail = null;
  30. } else if (tail == node) { // 最后 前下=null ; node.pre.next
  31. node.prev.next = null;
  32. tail = node.prev;
  33. } else if (head == node) { // 开头 下前=null ; node.next.pre
  34. node.next.prev = null;
  35. head = node.next;
  36. } else {
  37. node.prev.next = node.next;
  38. node.next.prev = node.prev;
  39. }
  40. size--;
  41. }
  42. public Node removeLast() {
  43. Node node = tail;
  44. remove(tail); // 调用了remove()
  45. return node;
  46. }
  47. public int size() {
  48. return size;
  49. }
  50. }
  51. // LRUCache 构造函数
  52. private int cap; // 双向链表 容量 capacity
  53. private DoubleList cache;
  54. private HashMap<Integer, Node> map; // map 的 k , v
  55. public LRUCache(int capacity) {
  56. this.cap = capacity;
  57. cache = new DoubleList();
  58. map = new HashMap<>();
  59. }
  60. // get put 方法
  61. public int get(int key) {
  62. if(!map.containsKey(key))
  63. return -1;
  64. int val = map.get(key).val;
  65. put(key, val); // 此处调用了 put()
  66. return val;
  67. }
  68. public void put(int key, int value) {
  69. Node x = new Node(key, value);
  70. if (map.containsKey(key)){
  71. cache.remove(map.get(key));
  72. cache.addFirst(x);
  73. map.put(key,x);
  74. } else {
  75. if (cap == cache.size()) {
  76. Node last = cache.removeLast();
  77. map.remove(last.key);
  78. }
  79. cache.addFirst(x);
  80. map.put(key,x);
  81. }
  82. }
  83. }

对put()的理解

put 方法主要有3个

1.remove 删除缓存cache或者删除缓存cache、map中的数据

2.缓存头插 cache.addFirst(x);

3.更新map map.put(key,x);

那么索性如下写法

  1. public void put(int key, int value) {
  2. Node x = new Node(key, value);
  3. if (map.containsKey(key)){
  4. cache.remove(map.get(key));
  5. } else {
  6. if (cap == cache.size()) {
  7. Node last = cache.removeLast();
  8. map.remove(last.key);
  9. }
  10. }
  11. cache.addFirst(x);
  12. map.put(key,x);
  13. }

 

写在最后,好久没白嫖别人思想了,还好自己得空实现了一版代码,此处奉上链接,以示敬意。

参考

https://leetcode-cn.com/problems/lru-cache/solution/lru-ce-lue-xiang-jie-he-shi-xian-by-labuladong/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/491715
推荐阅读
相关标签
  

闽ICP备14008679号