不简单!一篇让你彻底理解 LRU 的 JavaScript 实现指南

226 阅读4分钟

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

实现 LRUCache 类:

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

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

思路: 通过使用哈希表和双向链表实现一个满足LRU缓存约束的数据结构。

首先,你可以设计一个节点类来表示双向链表的节点,该节点包含key和value,并且具有指向前一个节点和后一个节点的指针。然后,你可以使用哈希表来存储key和对应节点的映射关系。

每当get或put操作发生时,你都需要将被访问的节点移动到双向链表的头部,表示最近被使用过。如果容量已满,在插入新节点之前,你需要从双向链表的尾部删除最久未使用的节点,并且在哈希表中也删除对应的映射关系。

运行代码:

// LRU 用到的结点
// Hash  key=> value
// 链表 结点 
// Hash链表 
class ListNode {
    constructor(key, val) {
        this.key = key; //? O(1) key->
        this.val = val; // val
        this.pre = null; // pre
        this.next = null; // next 
    }
}

class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.data = {} ; // hashMap  Map O(1)
        this.head = new ListNode();
        this.tail = new ListNode();
        this.head.next = this.tail;
        this.tail.pre = this.head;
    }
    put(key, val) {
        if (!this.data[key]) {
            // new
            let node = new ListNode(key, val);
            this.data[key] = node;
            // head 指针指向的是
            this.appendHead(node); // 
            this.size++;
            if (this.size > this.capacity) {
                const lastKey= this.removeTail()
                delete this.data[lastKey]
                this.size--
            }
        } else {
            // update
            let node = this.data[key];
            this.removeNode(node);
            node.val = val;
            this.appendHead(node);
        }
    }
    get(key) {
        if (!this.data[key]) return -1;
        else {
            let node = this.data[key];
            this.removeNode(node);
            this.appendHead(node);
            return node.val;
        }
    }
    appendHead(node) {
        let firstNode = this.head.next;
        this.head.next = node;
        node.pre = this.head;
        node.next = firstNode;
        firstNode.pre = node;
    }
    removeTail() {
        let key = this.tail.pre.key;
        this.removeNode(this.tail.pre)
        return key;
    }
    removeNode(node) {
        let preNode = node.pre;
        let nextNode = node.next;
        preNode.next = nextNode;
        nextNode.pre = preNode

    }
}

LRU作用以及应用场景:

  • LRU(Least Recently Used,最近最少使用)是一种缓存淘汰算法,它的核心思想是当缓存容量达到上限时,淘汰最久未使用的数据,以腾出空间来存储新的数据。该算法的目标是尽可能地保留那些被访问频率较高的数据,同时能够自动淘汰最久未使用的数据,以提高缓存的命中率。 上面代码可以在 O(1) 的时间复杂度内完成插入、查询和删除等操作,同时能够自动淘汰最久未使用的数据,适用于存储较小数据量但访问频率高的场景,如 Web 应用程序中的资源缓存、数据库查询结果缓存等。

  • 例如,当用户访问一个 Web 页面时,浏览器会将页面的资源(如图片、样式表、脚本等)缓存到本地,以提高页面加载速度。如果使用 LRU Cache 来管理这些缓存,可以有效地减少网络请求次数,提高用户体验。另外,数据库查询结果缓存也是常见的应用场景,通过将查询结果缓存到内存中,可以减少数据库查询的次数,提高系统响应速度。

总结: LRU - 蓦然回首,缓存已不复返!

在现代科技的浪潮中,缓存是提高系统性能的利器。而其中最受瞩目的就是 LRU Cache,这位于缓存江湖之巅的璀璨明星。

本文带你探索 LRU 的奥秘,就像解锁一个神秘宝盒。首先,让我们揭开它的面纱:LRU(Least Recently Used)算法,即最近最少使用算法。它以独特的方式管理数据,将最近被使用的数据保留在内存中,而最久未使用的数据则被无情地淘汰,为新数据腾出空间。

那么,LRU 又是如何运作的呢?它的灵魂由哈希表和双向链表组成,这两者默契配合,如同舞台上的黄金搭档。哈希表负责快速查找数据,而双向链表则维护数据的访问顺序。每当缓存被访问或修改,那些被触碰的节点就会翩然移动至链表的顶端,就像在人群中优雅起舞。而最遥远的回忆,则沉默地躺在链表的尽头,等待被无情地抹去。

LRU 的魅力不仅仅在于其高效的插入、查询和删除操作,更是它能够自动淘汰最久未使用数据的能力。不管是网络应用中的资源缓存,还是数据库查询结果的缓存,LRU 都能有效减少次数,为用户带来极速体验。

通过阅读本文,你将亲身感受到 LRU 的魔力。它带你穿越缓存世界的迷雾,一览性能优化的奥秘。现在,就让我们勇敢地踏上这场冒险之旅吧!与 LRU 约定一场,让你的系统焕发全新活力,让用户的体验飞跃时空!