【力扣】2622. 有时间限制的缓存

【力扣】2622. 有时间限制的缓存

文章目录

一、题目

编写一个类,它允许获取和设置键-值对,并且每个键都有一个 过期时间

该类有三个公共方法:

set(key, value, duration) :接收参数为整型键 key 、整型值 value 和以毫秒为单位的持续时间 duration 。一旦 duration 到期后,这个键就无法访问。如果相同的未过期键已经存在,该方法将返回 true ,否则返回 false 。如果该键已经存在,则它的值和持续时间都应该被覆盖。

get(key) :如果存在一个未过期的键,它应该返回这个键相关的值。否则返回 -1

count() :返回未过期键的总数。

示例 1:

复制代码
输入: 
actions = ["TimeLimitedCache", "set", "get", "count", "get"]
values = [[], [1, 42, 100], [1], [], [1]]
timeDelays = [0, 0, 50, 50, 150]
输出: [null, false, 42, 1, -1]
解释:
在 t=0 时,缓存被构造。
在 t=0 时,添加一个键值对 (1: 42) ,过期时间为 100ms 。因为该值不存在,因此返回false。
在 t=50 时,请求 key=1 并返回值 42。
在 t=50 时,调用 count() ,缓存中有一个未过期的键。
在 t=100 时,key=1 到期。
在 t=150 时,调用 get(1) ,返回 -1,因为缓存是空的。

示例 2:

复制代码
输入:
actions = ["TimeLimitedCache", "set", "set", "get", "get", "get", "count"]
values = [[], [1, 42, 50], [1, 50, 100], [1], [1], [1], []]
timeDelays = [0, 0, 40, 50, 120, 200, 250]
输出: [null, false, true, 50, 50, -1]
解释:
在 t=0 时,缓存被构造。
在 t=0 时,添加一个键值对 (1: 42) ,过期时间为 50ms。因为该值不存在,因此返回false。
当 t=40 时,添加一个键值对 (1: 50) ,过期时间为 100ms。因为一个未过期的键已经存在,返回 true 并覆盖这个键的旧值。
在 t=50 时,调用 get(1) ,返回 50。
在 t=120 时,调用 get(1) ,返回 50。
在 t=140 时,key=1 过期。
在 t=200 时,调用 get(1) ,但缓存为空,因此返回 -1。
在 t=250 时,count() 返回0 ,因为缓存是空的,没有未过期的键。

提示:

  • 0 <= key, value <= 109
  • 0 <= duration <= 1000
  • 1 <= actions.length <= 100
  • actions.length === values.length
  • actions.length === timeDelays.length
  • 0 <= timeDelays[i] <= 1450
  • actions[i] 是 "TimeLimitedCache"、"set"、"get" 和 "count" 中的一个。
  • 第一个操作始终是 "TimeLimitedCache" 而且一定会以 0 毫秒的延迟立即执行

二、解决方案

概述

这个问题要求你实现一个键值存储,但其中的每个条目在一定时间后会过期。

具有时间限制的缓存的用途

想象一下,你正在维护一个从数据库中获取文件的缓存。你可以加载每个文件一次并将其无限期地保存在内存中。问题是,如果数据库中的文件发生了更新,缓存将包含过时的数据。另一个选择是每次访问文件时都不断重新下载文件(或至少发送一个请求询问文件是否更改)。但这可能效率低下且速度较慢,特别是如果遇到了文件很少更改的情况。

如果可以接受数据有时会有点过时,一个很好的折衷方案是为数据设置一个到期时间。这在性能和拥有最新数据之间提供了良好的平衡。这种类型的缓存在同一键被快速连续访问时最有效。

以下是如何使用这种类型的缓存的一些代码示例:

复制代码
const cache = new TimeLimitedCache();

async function getFileWithCache(filename) {
  let content = cache.get(filename);
  if (content !== -1) return content;
  content = await loadFileContents(filename);
  const ONE_HOUR = 60 * 60 * 1000;
  cache.set(filename, content, ONE_HOUR);
  return content;
}

在上面的代码中,getFileWithCache 首先尝试从缓存中加载数据。如果缓存命中,它会立即返回结果。否则,它会下载数据并在返回下载的数据之前填充缓存。

方法 1:setTimeout + clearTimeout + 类语法

每次将键值对放入缓存时,我们还可以创建一个定时器,用于在过期时间到期后删除该键。然而,我们需要谨慎处理这种方法,因为如果在时间到期之前覆盖了现有键会发生什么?这将导致新键过早被删除。因此,我们需要保留对定时器的引用,以便在键被覆盖时清除它。

  1. 当创建 TimeLimitedCache 的新实例时,会创建一个新的 Map。请注意,这里省略了构造函数功能。或者,我们可以将 this.cache = new Map(); 放在构造函数中,它将导致相同的行为。
  2. set 方法中,我们获取键的相关值。如果键不存在,则valueInCache undefined。如果 valueInCache 不是 undefined,我们会取消定时器,该定时器将删除旧键,以便它不会删除新键。然后,我们创建一个新的定时器,用于删除新键。最后,我们将值和对定时器的引用存储在 Map 中,然后返回是否找到值。
  3. get方法中,我们使用三元表达式返回值(如果存在)或 -1(如果不存在)。
  4. count方法中,我们只需返回Map的大小。由于所有键一旦过期就会被删除,所以我们知道这个值是准确的。
JavaScript 复制代码
class TimeLimitedCache {
  cache = new Map();

  set(key, value, duration) {
    const valueInCache = this.cache.get(key);
    if (valueInCache) {
      clearTimeout(valueInCache.timeout);
    }
    const timeout = setTimeout(() => this.cache.delete(key), duration);
    this.cache.set(key, { value, timeout });
    return Boolean(valueInCache);
  }

  get(key) {
    return this.cache.has(key) ? this.cache.get(key).value : -1;
  }

  count() {
    return this.cache.size;
  }
};

方法 2:setTimeout + clearTimeout + 函数语法

这种方法的逻辑与方法 1 完全相同,但我们使用函数而不是类。类语法更现代,通常被认为是最佳实践,但了解在旧代码库中如何使用函数作为构造函数也很重要。你可以在这里了解有关这种较旧语法机制的更多信息。

TimeLimitedCache 构造函数中,我们创建了一个新的 Map。

set方法中,我们获取键的相关值。如果键存在,我们需要稍后返回这个值,以便后面使用。如果键已经存在,我们需要将 overwritten 标志设置为 true,以便handleExpiredData不会错误地删除数据。然后,我们可以定义一个包含所有相关信息的对象(键、值、覆盖标志)并将其添加到队列和缓存中。

get 方法中,我们首先需要调用 handleExpiredData。然后,只需返回缓存中的值(如果存在),否则返回 -1。

count方法中,与往常一样,我们首先需要调用 handleExpiredData。由于我们一直在维护一个大小属性,所以只需返回它。

这种实现非常高效。所有方法调用的平均时间复杂度为 O(logN)。但在最坏的情况下,即所有键都已过期且需要删除时,时间复杂度为 O(NlogN)。

虽然这种实现有助于解决内存泄漏问题,但它并不能完全解决它。为了理解为什么,想象一下,你一遍又一遍地写入相同的键值对并给它们一个长时间的到期时间。队列将不断变得不必要地更长。解决这个问题的方法可能是使用二叉搜索树,它允许在队列中删除元素,而不仅仅是在队列的前面。然而,在 JavaScript 中,LeetCode 并没有内置实现这种功能。

JavaScript 复制代码
function TimeLimitedCache() {
  this.cache = new Map();
}

TimeLimitedCache.prototype.set = function(key, value, duration) {
    const valueInCache = this.cache.get(key);
    if (valueInCache) {
      clearTimeout(valueInCache.timeout);
    }
    const timeout = setTimeout(() => this.cache.delete(key), duration);
    this.cache.set(key, { value, timeout });
    return Boolean(valueInCache);
}

TimeLimitedCache.prototype.get = function(key) {
    return this.cache.has(key) ? this.cache.get(key).value : -1;
}

TimeLimitedCache.prototype.count = function() {
    return this.cache.size;
}

方法 3:维护到期时间

实现思路
  • 与维护删除键的定时器不同,我们可以存储到期时间。当要求计算未过期键的数量时,我们只需筛选出其中满足 Date.now() < expirationTime 条件的键。
具体步骤
  1. 创建一个 TimeLimitedCache 类。
  2. 类中使用一个普通对象来存储键值对,其中键是缓存的键,值是一个对象,包括值和到期时间。
  3. 实现set方法,用于将键值对存入缓存。在该方法中,首先通过检查值是否存在且未过期来将 hasUnexpiredValue 设置为布尔值。然后,添加一个新值,确保包括到期时间。
  4. 实现get方法,用于获取键的值。如果键不存在或已过期,立即返回 -1。否则,返回与键关联的值。
  5. 实现 count 方法,首先需要检查每个条目以查看它是否已过期。Object.values 返回与对象中的键关联的值的列表。
类实现示例
JavaScript 复制代码
class TimeLimitedCache {
     cache = {};

  set(key, value, duration) {
    const hasUnexpiredValue = key in this.cache && Date.now() < this.cache[key].expiration;
    this.cache[key] = { value, expiration: Date.now() + duration };
    return hasUnexpiredValue;
  }

  get(key) {
    if (this.cache[key] === undefined) return -1;
    if (Date.now() > this.cache[key].expiration) return -1;
    return this.cache[key].value;
  }

  count() {
    let count = 0;
    for (const entry of Object.values(this.cache)) {
        if (Date.now() < entry.expiration) {
            count += 1;
        }
    }
    return count;
  }
}
类实现示例(TypeScript)
TypeScript 复制代码
type Entry = { value: any, expiration: number };

class TimeLimitedCache {
  cache: Record<string, Entry> = {};

  set(key: string, value: any, duration: number) {
    const hasUnexpiredValue = key in this.cache && Date.now() < this.cache[key].expiration;
    this.cache[key] = { value, expiration: Date.now() + duration };
    return hasUnexpiredValue;
  }

  get(key: string) {
    if (this.cache[key] === undefined) return -1;
    if (Date.now() > this.cache[key].expiration) return -1;
    return this.cache[key].value;
  }

  count() {
    let count = 0;
    for (const entry of Object.values(this.cache)) {
        if (Date.now() < entry.expiration) {
            count += 1;
        }
    }
    return count;
  }
}

方法 4:维护到期时间 + 优先级队列

实现思路

方法 3 有一些问题:

  • 即使在过期后,值仍然存储在内存中,这可能被视为内存泄漏。

计算未过期键的数量是一个 O(N) 操作,与缓存的大小成正比。我们可以通过在优先级队列中维护所有到期时间的排序列表来解决这些问题,从而允许以 O(logN) 的时间移除过期元素。

具体步骤
  1. 创建一个 TimeLimitedCache 类。
  2. 类中使用一个普通对象来存储键值对,其中键是缓存的键,值是一个对象,包括值和到期时间。
  3. 类中使用一个优先级队列来存储到期时间,从而实现按到期时间排序的列表。这允许我们以 O(logN) 的时间移除已过期的元素。
  4. 实现 handleExpiredData 方法,用于从队列的前端弹出所有已过期的元素。然后删除这些已过期的元素。但如果键在队列中,我们知道它在某个时间点被覆盖,那么我们不应该从缓存中删除它。处理已覆盖的标志将在 set 方法中处理。
  5. 实现set方法,首先必须调用 handleExpiredData。然后,检查键是否存在于缓存中,以便稍后返回这个值。如果键已存在,我们需要将overwritten标志设置为 true,以避免 handleExpiredData 错误地删除数据。然后,定义一个包含所有相关信息的对象(键、值、到期时间、覆盖标志)并将其添加到队列和缓存中。
  6. 实现 get 方法,首先必须调用 handleExpiredData。然后,只需返回缓存中的值(如果存在),否则返回 -1。
  7. 实现 count 方法,与往常一样,首先必须调用 handleExpiredData。由于一直在维护一个大小属性,只需返回它。
类实现示例
JavaScript 复制代码
class TimeLimitedCache {
  cache = {};
  queue = new MinPriorityQueue();
  size = 0;

  handleExpiredData() {
    const now = Date.now();
    while (this.queue.size() > 0 && this.queue.front().priority < now) {
      const entry = this.queue.dequeue().element;
      if (!entry.overwritten) {
        delete this.cache[entry.key];
        this.size -= 1;
      }
    }
  };

  set(key, value, duration) {
    this.handleExpiredData();
    const hasVal = key in this.cache
    if (hasVal) {
      this.cache[key].overwritten = true
    } else {
      this.size += 1;
    }
    const expiration = Date.now() + duration;
    const entry = { key, value, expiration, overwritten: false }
    this.cache[key] = entry
    this.queue.enqueue(entry, expiration);
    return hasVal;
  }

  get(key) {
    this.handleExpiredData();
    if (this.cache[key] === undefined) return -1;
    return this.cache[key].value;
  }

  count() {
    this.handleExpiredData();
    return this.size;
  }
}
类实现示例(TypeScript)
TypeScript 复制代码
type Entry = { key: string, value: any, expiration: number, overwritten: boolean };

class TimeLimitedCache {
  cache: Record<string, Entry> = {};
  queue = new MinPriorityQueue();
  size = 0;

  handleExpiredData() {
    const now = Date.now();
    while (this.queue.size() > 0 && this.queue.front().priority < now) {
      const entry = this.queue

.dequeue().element;
      if (!entry.overwritten) {
        delete this.cache[entry.key];
        this.size -= 1;
      }
    }
  }

  set(key: string, value: any, duration: number) {
    this.handleExpiredData();
    const hasVal = key in this.cache
    if (hasVal) {
      this.cache[key].overwritten = true
    } else {
      this.size += 1;
    }
    const expiration = Date.now() + duration;
    const entry = { key, value, expiration, overwritten: false }
    this.cache[key] = entry
    this.queue.enqueue(entry, expiration);
    return hasVal;
  }

  get(key: string) {
    this.handleExpiredData();
    if (this.cache[key] === undefined) return -1;
    return this.cache[key].value;
  }

  count() {
    this.handleExpiredData();
    return this.size;
  }
}

方法 5:Map + Generator

实现思路
  • 在方法 3 的基础上,使用一个 Map 存储到期时间,并使用一个生成器函数来迭代并清理过期元素。
具体步骤
  1. 创建一个 TimeLimitedCache 类。
  2. 类中使用一个 Map 来存储键值对,其中键是缓存的键,值是一个对象,包括值和到期时间。
  3. 实现 handleExpiredData 方法,它包括一个生成器函数,该生成器函数会迭代缓存中的所有元素,并清理过期元素。生成器函数遍历 Map 来获取键和到期时间,然后检查到期时间是否小于当前时间。如果是,表示元素已过期,因此删除它。
  4. 实现 set 方法,首先必须调用 handleExpiredData。然后,检查键是否存在于缓存中,以便稍后返回这个值。如果键已存在,我们需要将 overwritten 标志设置为 true,以避免handleExpiredData错误地删除数据。然后,定义一个包含所有相关信息的对象(键、值、到期时间、覆盖标志)并将其添加到 Map 中。
  5. 实现 get 方法,首先必须调用 handleExpiredData。然后,只需返回缓存中的值(如果存在),否则返回 -1。
  6. 实现 count 方法,与往常一样,首先必须调用 handleExpiredData。由于一直在维护Map的大小属性,只需返回它。
类实现示例
JavaScript 复制代码
class TimeLimitedCache {
  cache = new Map();

  *handleExpiredData() {
    const now = Date.now();
    for (const [key, { expiration, overwritten }] of this.cache) {
      if (!overwritten && expiration < now) {
        this.cache.delete(key);
      }
    }
  }

  set(key, value, duration) {
    this.handleExpiredData().next();
    const hasVal = this.cache.has(key);
    this.cache.set(key, { value, expiration: Date.now() + duration, overwritten: false });
    return hasVal;
  }

  get(key) {
    this.handleExpiredData().next();
    if (!this.cache.has(key) || this.cache.get(key).expiration < Date.now()) {
      return -1;
    }
    return this.cache.get(key).value;
  }

  count() {
    this.handleExpiredData().next();
    return this.cache.size;
  }
}
类实现示例(TypeScript)
TypeScript 复制代码
type Entry = { value: any, expiration: number, overwritten: boolean };

class TimeLimitedCache {
  cache = new Map<string, Entry>();

  *handleExpiredData() {
    const now = Date.now();
    for (const [key, { expiration, overwritten }] of this.cache) {
      if (!overwritten && expiration < now) {
        this.cache.delete(key);
      }
    }
  }

  set(key: string, value: any, duration: number) {
    this.handleExpiredData().next();
    const hasVal = this.cache.has(key);
    this.cache.set(key, { value, expiration: Date.now() + duration, overwritten: false });
    return hasVal;
  }

  get(key: string) {
    this.handleExpiredData().next();
    if (!this.cache.has(key) || this.cache.get(key).expiration < Date.now()) {
      return -1;
    }
    return this.cache.get(key).value;
  }

  count() {
    this.handleExpiredData().next();
    return this.cache.size;
  }
}

性能分析

每种实现方法的性能取决于不同的因素,例如缓存中的数据量和操作的频率。以下是每种实现方法的性能特点:

  • 方法 1 和方法 2 使用了setTimeout clearTimeout,但在删除过期数据时性能较差。它们的时间复杂度在最坏情况下为 O(NlogN)。
  • 方法 3 使用了一个普通对象和线性搜索来删除过期数据,因此性能较差。时间复杂度为 O(N),其中 N 是未过期数据的数量。
  • 方法 4 和方法 5 都使用了优先级队列或 Map 与生成器函数,因此可以高效地删除过期数据。它们的时间复杂度在删除过期数据时为 O(logN),其中 N 是未过期数据的数量。

你应根据具体的使用情况和性能要求选择适合你的实现方法。

总结

在本教程中,我们介绍了四种不同的实现方法来创建具有时间限制的缓存。每种方法都有自己的特点和性能特点。你可以根据你的需求选择适合你的方法。无论你选择哪种方法,确保正确处理缓存中的数据过期,以免出现内存泄漏问题。同时,测试你的实现以确保其性能满足你的需求。

此外,对于不同的编程语言和环境,还可以使用各种工具和库来实现具有时间限制的缓存,这些工具和库通常已经经过了广

泛测试和优化。根据你的项目需求和可用工具,你还可以考虑使用第三方库或框架来实现缓存。

相关推荐
Hcoco_me2 小时前
大模型面试题71: DPO有什么缺点?后续对DPO算法有哪些改进?
人工智能·深度学习·算法·自然语言处理·transformer·vllm
mit6.8242 小时前
dfs|bfs建图|hash贪心
算法
辰风沐阳2 小时前
JavaScript 的 WebSocket 使用指南
开发语言·javascript·websocket
短剑重铸之日2 小时前
《7天学会Redis》Day 3 - 持久化机制深度解析
java·redis·后端·缓存
jason_yang2 小时前
这5年在掘金的感想
前端·javascript·vue.js
qq_435139572 小时前
多级缓存(Caffeine+Redis)技术实现文档
数据库·redis·缓存
冰暮流星2 小时前
javascript如何转换为字符串与布尔型
java·开发语言·javascript
独自破碎E2 小时前
【新视角】输出二叉树的右视图
leetcode
罗湖老棍子2 小时前
团伙(group)(信息学奥赛一本通- P1385)
算法·图论·并查集