[TypeScript]手撸LFU

[TypeScript]手撸LFU

最近做笔试的时候遇到了要手撸LFU的题目,LFU在vue源码里还是有使用的,例如keep-alive的实现机制就是基于它来搞的。不多说了,直接上代码。

代码

// 双向链表node
class DoubleLinkNode {
    key: number;
    val: number;
    freq: number;
    prev?: DoubleLinkNode | null;
    next?: DoubleLinkNode | null;

    constructor(key, val, freq) {
        this.freq = freq;
        this.key = key;
        this.val = val;
    }
}

// 双向链表
class DoubleLink {
    size: number;
    head: DoubleLinkNode | null;
    tail: DoubleLinkNode | null;

    constructor() {
        this.size = 0;
        this.head = null;
        this.tail = null;
    }

    // 删除一个节点
    remove(node: DoubleLinkNode) {
        // 空链表
        if (this.size < 1) {
            return;
        }

        // 只有一个元素
        if (this.size === 1) {
            this.size = 0;
            this.head = null;
            this.tail = null;
            return;
        }

        // 删的是头结点
        if (node === this.head) {
            const nextNode = node.next;
            nextNode.prev = null;
            this.head = nextNode;

            this.size--;
            return;
        }

        // 删的是尾结点
        if (node === this.tail) {
            const prevNode = node.prev;
            prevNode.next = null;
            this.tail = prevNode;
            this.size--;
            return;
        }

        // 正常删中间节点
        const prevNode = node.prev;
        const nextNode = node.next;

        prevNode.next = nextNode;
        nextNode.prev = prevNode;
        this.size--;
    }

    // 在尾部插入一个节点
    addLast(node: DoubleLinkNode) {
        // 空链表
        if (this.size === 0) {
            this.head = node;
            this.tail = node;
        } else {
            // 非空链表
            const curTail = this.tail;
            curTail.next = node;
            node.prev = curTail;
            this.tail = node;
        }
        this.size++;
    }

    // 是否是空链表
    isEmpty() {
        return this.size === 0;
    }
}

// 实现LFU缓存
class LFUCache {
    keyToNodeMap: Map<number, DoubleLinkNode>;
    freqToKeysMap: Map<number, DoubleLink>;
    capacity: number;
    minFreq: number;

    constructor(capacity: number) {
        this.keyToNodeMap = new Map();
        this.freqToKeysMap = new Map();

        this.capacity = capacity;
        this.minFreq = 0;
    }

    get(key: number): number {
        if (!this.keyToNodeMap.has(key)) {
            return -1;
        }

        const node = this.keyToNodeMap.get(key);

        // 增加频次
        this.increaseFreq(node);

        return node.val;
    }

    put(key: number, value: number): void {
        // key已经存在
        if (this.keyToNodeMap.has(key)) {
            // 修改对应的node的val即可
            const node = this.keyToNodeMap.get(key);
            node.val = value;
            this.increaseFreq(node);
        } else {
            // key不存在
            // 容量满了
            if (this.keyToNodeMap.size >= this.capacity) {
                // 删除最小频次中最久没使用的
                this.removeMinFreqKey();
            }

            // ------------正式开始插入------------
            const newNode = new DoubleLinkNode(key, value, 1);
            this.keyToNodeMap.set(key, newNode);

            if (!this.freqToKeysMap.get(1)) {
                this.freqToKeysMap.set(1, new DoubleLink());
            }

            // 维护频次表
            const link = this.freqToKeysMap.get(1);
            link.addLast(newNode);

            // 插入新 key 后最小的 freq 肯定是 1
            this.minFreq = 1;
        }
    }

    // 增加频次
    increaseFreq(node: DoubleLinkNode) {
        const oldFreq = node.freq;
        const newFreq = node.freq + 1;
        node.freq = newFreq;

        // 维护频次表
        const oldFreqLink = this.freqToKeysMap.get(oldFreq);

        // 从旧频次表中删除这个node
        oldFreqLink.remove(node);
        if (oldFreqLink.isEmpty()) {
            this.freqToKeysMap.delete(oldFreq);

            // 如果这个频次正好是最低频次,记得更新最小频次
            if (this.minFreq === oldFreq) {
                this.minFreq = newFreq;
            }
        }

        // 维护新频次表
        if (!this.freqToKeysMap.get(newFreq)) {
            this.freqToKeysMap.set(newFreq, new DoubleLink());
        }
        const newFreqLink = this.freqToKeysMap.get(newFreq);
        newFreqLink.addLast(node);
    }

    // 删除最小频次中最久没使用的
    removeMinFreqKey() {
        const minFreqLink = this.freqToKeysMap.get(this.minFreq);

        // 其中最先被插入的那个 node 就是该被淘汰的 node
        const deletedNode = minFreqLink.head;

        // 维护最小频次map
        minFreqLink.remove(deletedNode);
        if (minFreqLink.isEmpty()) {
            this.freqToKeysMap.delete(this.minFreq);
        }

        // key表中删除对应Node
        this.keyToNodeMap.delete(deletedNode.key);
    }

    // log调试方法
    print() {
        console.log('keyToNodeMap: ', this.keyToNodeMap);
        console.log('freqToKeysMap: ', this.freqToKeysMap);
    }
}

思路

LFU(Least Frequently Used)缓存是一种缓存淘汰策略,它根据元素的使用频率来决定哪些元素应该被淘汰。在你提供的代码中,LFUCache 类实现了这种策略,下面是它的工作原理的详细描述:

  1. 数据结构

    • DoubleLinkNode:表示双向链表中的节点,包含键(key)、值(val)和频率(freq)。
    • DoubleLink:表示双向链表,包含头尾节点以及链表的大小。
    • MapkeyToNodeMap 用于存储键到节点的映射,freqToKeysMap 用于存储频率到键的映射。
  2. 构造函数

    • 初始化 LFUCache 时,设置缓存的容量(capacity),并初始化键到节点的映射和频率到键的映射。
  3. get 方法

    • 检查键是否存在于 keyToNodeMap 中。
    • 如果存在,找到对应的节点,并调用 increaseFreq 方法来增加节点的使用频率。
    • 返回节点的值。
  4. put 方法

    • 检查键是否已存在:
      • 如果存在,更新节点的值,并增加频率。
      • 如果不存在,并且缓存已满,则调用 removeMinFreqKey 方法来删除最小频率的键。
    • 创建新节点,并将其添加到 keyToNodeMapfreqToKeysMap 中。
  5. increaseFreq 方法

    • 增加节点的使用频率。
    • 从旧频率的链表中删除节点,并检查是否需要更新最小频率。
    • 将节点添加到新频率的链表中。
  6. removeMinFreqKey 方法

    • 找到最小频率链表的头节点,即最久未使用的节点。
    • 从链表中删除该节点,并更新 freqToKeysMap
    • keyToNodeMap 中删除对应的键。
  7. print 方法

    • 用于调试,打印当前的键到节点映射和频率到键的映射。

这种实现方式确保了:

  • 每个键的使用频率都被跟踪。
  • 当缓存达到容量限制时,最少使用频率的键将被淘汰。
  • 通过双向链表,可以快速地添加和删除节点,同时保持链表的顺序。

这种缓存策略适用于那些需要平衡访问频率和最近使用情况的场景。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值