KeepAlive 中的数据结构 LRU 缓存

简介: 前端的面试有时候会问 KeepAlive,而 KeepAlive 中运用到了 LRU 缓存,是它的原理之一,LRU 在 leetcode 中也对应了 146. LRU 缓存这道题目

前端的面试有时候会问 KeepAlive,而 KeepAlive 中运用到了 LRU 缓存,是它的原理之一,LRUleetcode 中也对应了 146. LRU 缓存,这篇文章主要讲解这道题目

题目链接: 146. LRU 缓存

一、题目描述:

请你设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。

实现 LRUCache 类:

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

函数 getput 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

提示:

  • 1 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 105
  • 最多调用 2 * 105getput

题目模板

/**
 * @param {number} capacity
 */
var LRUCache = function (capacity) {};

/**
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function (key) {};

/**
 * @param {number} key
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function (key, value) {};

二、思路分析:

学过操作系统的同学可能知道 LRU 缓存,是用在主存里面的一个页面调度算法,也可以说是它是一种数据结构,当时我还学了最佳置换 OPT、先进先出 FIFO、最不常用 LFU、时钟 CLOCK 几种页面调度算法,回到 LRU,它的全名叫做 LRU(least recently used) 最近最少使用页面调度算法,之前我舍友说 LRU 是用栈来实现的,但其实它的特性更像队列,用一个图来解释一下

46.png

可以看到

  1. 缓存里面不存在的并且要进入缓存的就会把最早进入缓存的页面删除
  2. 重新访问缓存中的页面会重新调到队列末尾,视为最新

它的思路很简单,但是 getput 也就是获取值和入队需要 O(1),然后上面的那个图也可以知道这个 LRU 是存在删除操作的,也就是在 put 当中存在 delete 也要实现 O(1),这个就比较麻烦,O(1) 的增删查改以我目前的经验来说,只有特殊的数组和哈希表能够做到,所以这道题的思路肯定是要围绕数组或者哈希来展开

数组实现增删查改有点麻烦,而且是要在一些特殊情况,LRU 就不是这种特殊情况,因此需要使用 JS ES6 的新特性 JS 中的哈希 Map 来完成 LRU 的实现

复习一下 LRU 的几个 API

  1. Map.prototype.has(key: string): boolean:判断 Map 中是否存在这个 key,本题中用于判断新插入的值是否存在缓存
  2. Map.prototype.delete(key: string): boolean:删除 Map 中这个 key-value 键值对,本题中用于超过缓存限制和再次访问时调到队尾
  3. Map.prototype.delete(key: string, value: any): boolean:设置缓存值
  4. Map.prototype.keys(): IterableIterator<any>:用于获取队列中的第一个值

代码可以说是信手捏来啊,看下图

47.png

48.png

需要注意的就是 Map.prototype.keys() 返回的是一个 Iterator,要使用 .next().value 的语法获取值

三、AC 代码:

var LRUCache = function (capacity) {
  this.max = capacity;
  this.map = new Map();
};

LRUCache.prototype.get = function (key) {
  const { map } = this;
  if (map.has(key)) {
    const res = map.get(key);
    map.delete(key);
    map.set(key, res);
    return res;
  } else {
    return -1;
  }
};

LRUCache.prototype.put = function (key, value) {
  const { map, max } = this;
  if (map.has(key)) {
    map.delete(key);
  }
  map.set(key, value);
  if (map.size > max) {
    map.delete(map.keys().next().value);
  }
};

四、总结:

LRU 和哈希有关,更是和 LinkedHashMap 有关,初始的哈希表是一个各个元素间没有顺序的一个数组,LinkedHashMap 给初始的哈希表加上了顺序,是哈希表与双向链表两种数据结构的结合题,哈希表的插入和查找时间复杂度都是为 O(1),而双向链表的删除是 O(1) 而且有顺序,因此能够解决 LRU 的问题,而 JSMap 本身就是能够记住键的原始插入顺序,因此算是粘了语言特性的光,感兴趣的可以去看一下双向链表的实现,用双向链表加 Map 去重新解决这道题

最后也是点一下题,就是 KeepAlive 这个 Vue 使用的缓存组件真的和 LRU 有关吗?让我们看一下它的一个源码实现

const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  // ...
  setup(props: KeepAliveProps, { slots }: SetupContext) {
    // 初始化缓存和键值
    const cache: Cache = new Map();
    const keys: Keys = new Set();
    let current: VNode | null = null;
    // ...
    return () => {
      // ...
      const key = vnode.key == null ? comp : vnode.key;
      const cachedVNode = cache.get(key);
      // ...
      pendingCacheKey = key;

      if (cachedVNode) {
        keys.delete(key);
        keys.add(key);
      } else {
        keys.add(key);
        if (max && keys.size > parseInt(max as string, 10)) {
          pruneCacheEntry(keys.values().next().value);
        }
      }
      // ...
    };
  },
};

上面就是 KeepAlive 初始化部分的源码(Vue3),可以看到和我们 LRU 中的 put 可以说是一摸一样

相关文章
|
3月前
|
缓存 算法 数据挖掘
深入理解缓存更新策略:从LRU到LFU
【10月更文挑战第7天】 在本文中,我们将探讨计算机系统中缓存机制的核心——缓存更新策略。缓存是提高数据检索速度的关键技术之一,无论是在硬件还是软件层面都扮演着重要角色。我们会详细介绍最常用的两种缓存算法:最近最少使用(LRU)和最少使用频率(LFU),并讨论它们的优缺点及适用场景。通过对比分析,旨在帮助读者更好地理解如何选择和实现适合自己需求的缓存策略,从而优化系统性能。
76 3
|
2月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
47 5
|
3月前
|
存储 缓存 索引
从底层数据结构和CPU缓存两方面剖析LinkedList的查询效率为什么比ArrayList低
本文详细对比了ArrayList和LinkedList的查询效率,从底层数据结构和CPU缓存两个方面进行分析。ArrayList基于动态数组,支持随机访问,查询时间复杂度为O(1),且CPU缓存对其友好;而LinkedList基于双向链表,需要逐个节点遍历,查询时间复杂度为O(n),且CPU缓存对其帮助不大。文章还探讨了CPU缓存对数组增删操作的影响,指出缓存主要作用于读取而非修改。通过这些分析,加深了对这两种数据结构的理解。
59 2
|
3月前
|
缓存 分布式计算 NoSQL
大数据-47 Redis 缓存过期 淘汰删除策略 LRU LFU 基础概念
大数据-47 Redis 缓存过期 淘汰删除策略 LRU LFU 基础概念
88 2
|
5月前
|
缓存 算法 前端开发
深入理解缓存淘汰策略:LRU和LFU算法的解析与应用
【8月更文挑战第25天】在计算机科学领域,高效管理资源对于提升系统性能至关重要。内存缓存作为一种加速数据读取的有效方法,其管理策略直接影响整体性能。本文重点介绍两种常用的缓存淘汰算法:LRU(最近最少使用)和LFU(最不经常使用)。LRU算法依据数据最近是否被访问来进行淘汰决策;而LFU算法则根据数据的访问频率做出判断。这两种算法各有特点,适用于不同的应用场景。通过深入分析这两种算法的原理、实现方式及适用场景,本文旨在帮助开发者更好地理解缓存管理机制,从而在实际应用中作出更合理的选择,有效提升系统性能和用户体验。
249 1
|
6月前
|
缓存 Python
在Python中,`functools`模块提供了一个非常有用的装饰器`lru_cache()`,它实现了最近最少使用(Least Recently Used, LRU)缓存策略。
在Python中,`functools`模块提供了一个非常有用的装饰器`lru_cache()`,它实现了最近最少使用(Least Recently Used, LRU)缓存策略。
|
5月前
|
存储 缓存 Java
|
5月前
|
存储 缓存 算法
Python 从零开始实现一个简单的LRU缓存
Python 从零开始实现一个简单的LRU缓存
50 0
|
20天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
164 85
|
3月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
87 6