LRU:Least Recently Used,最少最近使用算法。

算法描述

leetcode 146 [LRU缓存机制] 有关于这个数据结构的设计的描述,下面我直接copy一下,实现LRUCache类:

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

注意:put & get方法的都需要是O(1) 的时间复杂度

  1. LRUCache lRUCache = new LRUCache(2);
  2. lRUCache.put(1, 1); // 缓存是 cache = [1=>1]
  3. lRUCache.put(2, 2); // 缓存是 cache = [2=>2, 1=>1]
  4. lRUCache.get(1); // 返回 1,访问了1就会将1提到队列前头 cache = [1=>1, 2=>2],
  5. lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 cache = [3=>3, 1=>1]
  6. lRUCache.get(2); // 返回 -1 (未找到)
  7. lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 cache = [4=>4, 3=>3]
  8. lRUCache.get(1); // 返回 -1 (未找到)
  9. lRUCache.get(3); // 返回 3 cache = [3=>3, 4=>4]
  10. lRUCache.get(4); // 返回 4 cache = [4=>4, 3=>3]

算法设计

上面我们知道有一个条件是put & get方法的时间复杂度要是O(1),那么构建的cache缓存需要满足以下条件
1、cache中的元素,应该是有时序的,来区分哪些是最近访问的/较久未访问的数据,容量满的时候还需要删掉末尾的元素腾出空间。
2、在cache需要快速找到某个key是否存在并且获得对应的value O(1),这个点我们很容易想到hashMap
3、每次访问cache中的某个key,需要将这个元素变成队列最开头的位置,也就是说cache要支持在任意位置快速的删除/插入元素 O(1)
我们知道hashMap查找快,但是数据没有固定顺序;链表有顺序,插入删除快,但查找慢。结合一下基本就满足了我们上面的需求,一般也是采用这个结构实现的即哈希链表:LinkedHashMap

image.png
注意这里用到的是双向链表,关于这个为什么是双链表不是单链表

代码实现:双向链表+hashMap

双向链表部分

  1. /*
  2. * 双链表Node
  3. */
  4. class DoubleLinkedListNode {
  5. public key: number;
  6. public value: number;
  7. public next: any = null;
  8. public prev: any = null;
  9. constructor(key: number, value: number) {
  10. this.key = key;
  11. this.value = value;
  12. }
  13. }
  14. class DoubleLinkedList {
  15. // 链表头节点, Number.POSITIVE_INFINITY -> 正无穷大
  16. public head = new DoubleLinkedListNode(
  17. Number.POSITIVE_INFINITY,
  18. Number.POSITIVE_INFINITY
  19. );
  20. // 链表尾节点, Number.NEGATIVE_INFINITY -> 负无穷大
  21. public tail = new DoubleLinkedListNode(
  22. Number.NEGATIVE_INFINITY,
  23. Number.NEGATIVE_INFINITY
  24. );
  25. private size: number = 0; //链表长度
  26. constructor() {
  27. this.head.next = this.tail;
  28. this.tail.prev = this.head;
  29. }
  30. public addFirstNode(node: DoubleLinkedListNode): any {
  31. // node链起来
  32. node.prev = this.head;
  33. node.next = this.head.next;
  34. // 处理node
  35. this.head.next.prev = node;
  36. this.head.next = node;
  37. // size ++
  38. this.size += 1;
  39. }
  40. public remove(node: any): void {
  41. node.prev.next = node.next;
  42. node.next.prev = node.prev;
  43. // size --
  44. this.size -= 1;
  45. }
  46. public removeLastNode() {
  47. // 空链表上,其实这里也可以通过size判断
  48. if (this.head.next === this.tail) return null;
  49. const last = this.tail.prev;
  50. this.remove(last);
  51. return last;
  52. }
  53. public getSize(): number {
  54. return this.size;
  55. }
  56. }

LRUCache实现部分

  1. class LRUCache {
  2. private hashMap = new Map();
  3. private cache: DoubleLinkedList = new DoubleLinkedList();
  4. private capacity: number; //容量
  5. constructor(capacity: number) {
  6. this.capacity = capacity;
  7. }
  8. // key访问提升到最近使用
  9. private getRecently(key: number): void {
  10. const node: DoubleLinkedListNode = this.hashMap.get(key);
  11. // 移除node
  12. this.cache.remove(node);
  13. // 放到队头
  14. this.cache.addFirstNode(node);
  15. }
  16. // 移除最久未使用的元素
  17. private removeLastRecently(): void {
  18. const lastNode: any = this.cache.removeLastNode();
  19. // map中也要删掉改元素哦
  20. this.hashMap.delete(lastNode?.key);
  21. }
  22. // 添加最近使用的元素
  23. private addRecently(key: number, value: number): void {
  24. const node: DoubleLinkedListNode = new DoubleLinkedListNode(key, value);
  25. this.cache.addFirstNode(node);
  26. this.hashMap.set(key, node);
  27. }
  28. /*
  29. * 判断hashMap是否有该元素
  30. * 没有: 返回 -1
  31. * 有: 将元素提升到链表头,返回对应的元素
  32. */
  33. public get(key: number) {
  34. if (this.hashMap.has(key)) {
  35. this.getRecently(key);
  36. return this.hashMap.get(key).value;
  37. } else {
  38. return -1;
  39. }
  40. }
  41. /*
  42. * 判断是否超过容器的极限了
  43. * 有: 将链表最后一个元素移除,后再将该元素放在链表头
  44. * 没有: 放到链表头
  45. */
  46. public put(key: number, value: number) {
  47. if (this.cache.getSize() === this.capacity) {
  48. this.removeLastRecently();
  49. }
  50. this.addRecently(key, value);
  51. console.log("map", this.hashMap);
  52. }
  53. }

验证部分的代码

  1. const lRUCache = new LRUCache(2);
  2. console.log("-----");
  3. lRUCache.put(1, 1); // 缓存是 {1=1}
  4. lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
  5. console.log("-----");
  6. lRUCache.get(1); // 返回 1
  7. lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
  8. console.log("-----");
  9. lRUCache.get(2); // 返回 -1 (未找到)
  10. lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
  11. console.log("-----");
  12. lRUCache.get(1); // 返回 -1 (未找到)
  13. lRUCache.get(3); // 返回 3
  14. lRUCache.get(4); // 返回 4

代码实现:迭代器+hashMap

ES6中的Map本身是可迭代的,并且key有序,这样我们可以利用这个特性替代双向链表的功能
image.png

  1. /*
  2. * map的key是一个有序(迭代器)
  3. * 可以替代LinkedListHashMap的功能
  4. */
  5. class LRUCacheMap<T, U> {
  6. private cache: Map<T, U> = new Map();
  7. private capacity: number;
  8. constructor(capacity: number) {
  9. this.capacity = capacity;
  10. }
  11. /*
  12. * 判断cache是否有该元素
  13. * 没有: 返回 null
  14. * 有: 将元素提升到链表头,返回对应的元素
  15. */
  16. public get(key: T): U | null {
  17. if (this.cache.get(key)) {
  18. // 先删除该元素,再提升到头部
  19. const value: U = this.cache.get(key) as U;
  20. this.cache.delete(key);
  21. this.cache.set(key, value);
  22. return value;
  23. } else {
  24. return null;
  25. }
  26. }
  27. // 获取map中最久没有访问一个元素(最先插入)
  28. private getFirstKey() {
  29. const keys = this.cache.keys();
  30. return keys.next().value;
  31. }
  32. /*
  33. * 判断是否超过容器的极限了
  34. * 有: 将map最后一个元素移除,后再将该元素放在链表头
  35. * 没有: 放到链表头
  36. */
  37. public put(key: T, value: U) {
  38. // 已经存在的删除: map的key如果是引用类型并不会被覆盖的
  39. if (this.cache.has(key)) {
  40. this.cache.delete(key);
  41. }
  42. if (this.capacity === this.cache.size) {
  43. const lastKey = this.getFirstKey();
  44. this.cache.delete(lastKey);
  45. }
  46. this.cache.set(key, value);
  47. }
  48. toString() {
  49. console.table(this.cache);
  50. }
  51. }
  1. const lruCacheMap = new LRUCacheMap(4);
  2. lruCacheMap.put(2, 2); // 入 2,剩余容量3
  3. lruCacheMap.put(3, 3); // 入 3,剩余容量2
  4. lruCacheMap.put(4, 4); // 入 4,剩余容量1
  5. lruCacheMap.put(5, 5); // 入 5,已满 从头至尾 2-3-4-5
  6. lruCacheMap.toString();
  7. lruCacheMap.put(4, 4); // 入4,已存在 ——> 置队尾 2-3-5-4
  8. lruCacheMap.toString();
  9. lruCacheMap.put(1, 1); // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
  10. lruCacheMap.toString();
  11. lruCacheMap.get(3); // 获取3,刷新3——> 置队尾 5-4-1-3
  12. lruCacheMap.toString();