什么是 LRU

LRU(Least Recently Used)即最近最少使用缓存,
在做性能优化的时候会经常用到使用到缓存,用以空间换时间的方式来达到性能优化目标。
最常用到的缓存库就是 lru-cache, 前端 SSR 框架 nuxt 就是使用 lru-cache 来实现其页面缓存、组件缓存和接口缓存的功能,以降低服务器 cpu 的负载提高 SSR 页面的并发数。

LRU 实现

先来看看 LRU 需要达到什么样的要求。

  1. 需要给定一个数据结构的长度,不能无限制的缓存数据;
  2. LRU 实例提供一个 get 方法,可通过关键字 key 获取缓存中数据,若没有则返回 -1;
  3. LRU 实例提供一个 put 方法,变更数据值,若数据存在则修改,不存在则插入一条新数据,插入时超过数据长度则删除最久未使用的关键字。
  4. get、put的时间复杂度必须是 O(1)

    基础数据结构选型

    问题一

    对于一个优秀的前端工程师来说,想要实现 1、2、3 的要求是很容易的,用纯数组就可以实现上述 3 点的需求,但是在时间复杂度的要求上达不到,用数组的话,不管是 get 还是 put 方法的时间复杂度都为 O(n)
    这里就不详细展开数组的实现了, 为什么时间复杂度为 O(n) ???
    所以为了实现对存储结构操作的,这里选择双向链表来时现缓存数据的存储。

    问题二

    在选择双向链表后又出现了一个新问题, 众所周知,对链表插入与删除时间复杂度为 O(1), 但是链表的查找时间复杂度却为 O(n),在实现 get 的过程中肯定会使用到查询操作。为了解决查询的时间复杂度问题,自然就想到了 Map 数据结构。es6 的 Map 数据结构是查询的时间复杂度正是为O(1)。

这里肯定又有小朋友问啦:用 Map 为什么不直接用对象呢,这多方便?
其实这里使用对象也是可以实现一些简单的需求的,如果 key 为 number 或者为 string,只需要操作时转化一下即可,也能实现需求。但是需要 key 类型更复杂的场景,直接使用对象时达不到 LRU 的要求的
这里通过 双向链表,Map 的数据结构的组合来实现 LRU,使用双向链表存储数据,使用Map标记链表中 key 的位置这样就可以很容易实现 LRUCache 的数据结构。
因为要实现 O(1) 的时间复杂度,首先想到的就是使用 es6 的 Map 数据结构来实现。

  1. /**
  2. * @param {number} capacity
  3. */
  4. var LRUCache = function(capacity) {
  5. this.map = new Map();
  6. this.capacity = capacity;
  7. };
  8. /**
  9. * @param {number} key
  10. * @return {number}
  11. */
  12. LRUCache.prototype.get = function(key) {
  13. if(this.map.has(key)){
  14. let value = this.map.get(key);
  15. this.map.delete(key); // 删除后,再 set ,相当于更新到 map 最后一位
  16. this.map.set(key, value);
  17. return value
  18. } else {
  19. return -1
  20. }
  21. };
  22. /**
  23. * @param {number} key
  24. * @param {number} value
  25. * @return {void}
  26. */
  27. LRUCache.prototype.put = function(key, value) {
  28. // 如果已有,那就要更新,即要先删了再进行后面的 set
  29. if(this.map.has(key)){
  30. this.map.delete(key);
  31. }
  32. this.map.set(key, value);
  33. // put 后判断是否超载
  34. if(this.map.size > this.capacity){
  35. this.map.delete(this.map.keys().next().value);
  36. }
  37. };
  38. /**
  39. * Your LRUCache object will be instantiated and called as such:
  40. * var obj = new LRUCache(capacity)
  41. * var param_1 = obj.get(key)
  42. * obj.put(key,value)
  43. */

简单用 Typescript 实现一下

  1. class LinkNode {
  2. key: number
  3. value: number
  4. _prev: LinkNode | null
  5. _next: LinkNode | null
  6. constructor(key: number, val: number) {
  7. this.key = key
  8. this.value = val
  9. this._prev = null
  10. this._next = null
  11. }
  12. }
  13. class LRUCache {
  14. head: LinkNode | null
  15. tail: LinkNode | null
  16. size: number
  17. map: Map<number, LinkNode>
  18. constructor(capacity: number) {
  19. this.size = capacity
  20. this.map = new Map()
  21. this.head = null
  22. this.tail = null
  23. }
  24. get(key: number): number {
  25. // 在 map 中查找是否存在有这条数据
  26. if (this.map.has(key)) {
  27. let node = this.map.get(key) as LinkNode
  28. let _prev = node._prev
  29. let _next = node._next
  30. // 判断是否为头节点,若为头节点则不需要对链表作任何操作,直接返回值,若部位头肩点,需要操作链表达到最近缓存的操作
  31. if (_prev) {
  32. // 不为节点需要将该节点移动到头节点的位置
  33. // 当前节点是尾部节点的情况
  34. if (!_next) {
  35. this.tail = _prev
  36. } else {
  37. _next._prev = _prev // 等价于 node._next._prev = node._prev
  38. }
  39. // 1. 链表操作,移除 node 节点
  40. _prev._next = _next // 等价于 node._prev._next = node._next
  41. // 2. 将 node 节点放到链表头部
  42. node._prev = null
  43. if (this.head) {
  44. node._next = this.head
  45. this.head._prev = node
  46. }
  47. this.head = node
  48. }
  49. // 返回想要查找的数据值
  50. return node.value
  51. }
  52. return -1
  53. }
  54. put(key: number, value: number): void {
  55. // put 功能包括两部分,修改和新增
  56. if (this.map.has(key)) {
  57. // 修改
  58. let node = this.map.get(key) as LinkNode
  59. node.value = value
  60. let _prev = node._prev
  61. let _next = node._next
  62. if (_prev) {
  63. if (!_next) {
  64. this.tail = _prev
  65. } else {
  66. _next._prev = _prev // 等价于 node._next._prev = node._prev
  67. }
  68. // 非头部节需要把节点提到链表头部
  69. _prev._next = _next
  70. node._next = this.head
  71. node._prev = null
  72. this.head && (this.head._prev = node)
  73. this.head = node
  74. }
  75. } else {
  76. let node = new LinkNode(key, value)
  77. this.map.set(key, node)
  78. if (this.head) {
  79. node._next = this.head
  80. this.head._prev = node
  81. this.head = node
  82. } else {
  83. this.head = this.tail = node
  84. }
  85. // 新增数据的场景
  86. if (this.map.size > this.size) {
  87. let _tail = this.tail as LinkNode
  88. this.map.delete(_tail.key)
  89. this.tail = _tail._prev
  90. if (this.tail) {
  91. this.tail._next = null
  92. }
  93. if (this.head.key === _tail.key) {
  94. this.head = null
  95. }
  96. }
  97. }
  98. }
  99. }