LeetCode - 460 LFU缓存(Java & JS & Python)

这篇具有很好参考价值的文章主要介绍了LeetCode - 460 LFU缓存(Java & JS & Python)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

题目来源

460. LFU 缓存 - 力扣(LeetCode)

题目描述

请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。

实现 LFUCache 类:

  • LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
  • int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。
  • void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。

为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。

当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。

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

示例

输入

["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]

输出

[null, null, null, 1, null, -1, 3, null, -1, 3, 4]

解释

// cnt(x) = 键 x 的使用计数
// cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
LFUCache lfu = new LFUCache(2);
lfu.put(1, 1);   // cache=[1,_], cnt(1)=1
lfu.put(2, 2);   // cache=[2,1], cnt(2)=1, cnt(1)=1
lfu.get(1);      // 返回 1
                 // cache=[1,2], cnt(2)=1, cnt(1)=2
lfu.put(3, 3);   // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
                 // cache=[3,1], cnt(3)=1, cnt(1)=2
lfu.get(2);      // 返回 -1(未找到)
lfu.get(3);      // 返回 3
                 // cache=[3,1], cnt(3)=2, cnt(1)=2
lfu.put(4, 4);   // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
                 // cache=[4,3], cnt(4)=1, cnt(3)=2
lfu.get(1);      // 返回 -1(未找到)
lfu.get(3);      // 返回 3
                 // cache=[3,4], cnt(4)=1, cnt(3)=3
lfu.get(4);      // 返回 4
                 // cache=[3,4], cnt(4)=2, cnt(3)=3

提示

  • 1 <= capacity <= 104
  • 0 <= key <= 105
  • 0 <= value <= 109
  • 最多调用 2 * 10^5 次 get 和 put 方法

题目解析

LFU缓存可以通过:两个哈希表(Map结构) + 双向链表来实现。

我们可以定义两个哈希表keyMap和freqMap,其中:

  • keyMap的键是"本题key",值为"双向链表节点node"
  • freqMap的键是"本题key的使用次数freq",值为”对应使用次数freq的key对应的node组成的双向链表“

而双向链表节点node用于记录key的如下信息:

  • key:本题的key
  • val:本题的value
  • freq:对应key使用次数

可能上面说法比较晦涩,下面通过图示来说:

java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言


java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言


java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言


java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言


java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言


java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言


java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言


java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言


java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言


java 实现lfu leetcode,算法与数据结构,LeetCode,LFU缓存,Java,JS,Python,C语言

以上就是两个哈希Map,以及双向链表完成LFU缓存的逻辑。

上面逻辑中,有一个问题,那么就是每次LFU容量不足时,我们需要删除掉最少、最远使用的key,那么首先如何找到最少使用次数的key呢?

上面图示中,我们是通过人眼识别出freqMap键列中最小的键,即为最少使用次数。

那么代码该如何实现呢?

我们可以定义一个全局变量(或者类静态变量)minFreq来记录最少使用次数,而minFreq的更新有如下时机:文章来源地址https://www.toymoban.com/news/detail-828555.html

  • 1、put新增操作一定会带来一个使用次数freq=1的键,且此新键的使用次数1一定是最少的,此时我们可以更新minFreq=1
  • 2、get和put更新操作会新增对应key的使用次数,因此在新增使用次数后,该key需要从对应使用次数的DLink中去除,如果对应DLink只有该key对应节点,且对应DLink是最少使用次数对应的容器链表,那么删除key后,该DLInk就为空,那么最少使用次数的key就没了。此时我们可以直接将minFreq++,因为如果当前key在新增使用次数前是唯一的最少使用次数key,那么当前key新增使用次数后,依旧是最少使用次数的key。而minFreq就是当前key新增使用次数前的使用次数。

Java算法源码

import java.util.HashMap;

class LFUCache {
  /** 双向链表节点 */
  static class Node {
    /** 记录本题的键 */
    int key;

    /** 记录本题的值 */
    int val;

    /** 记录该键被访问的次数 */
    int freq;

    /** 当前节点的上一个节点 */
    Node prev;

    /** 当前节点的下一个节点 */
    Node next;

    public Node(int key, int val, int freq) {
      this.key = key;
      this.val = val;
      this.freq = freq;
      this.prev = null;
      this.next = null;
    }
  }

  /** 双向链表 */
  static class Link {
    /** 链表中节点个数 */
    int size;

    /** 链表头节点 */
    Node head;

    /** 链表尾节点 */
    Node tail;

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

    /**
     * 尾插
     *
     * @param node 要被插入的节点
     */
    public void addLast(Node node) {
      if (this.size == 0) {
        // 空链表,则node节点插入后,即为头、尾节点
        this.head = node;
        this.tail = node;
      } else {
        // 非空链表,则node节点插入到tail节点后面
        this.tail.next = node;
        node.prev = this.tail;
        this.tail = node;
      }
      this.size++;
    }

    /**
     * 删除指定节点
     *
     * @param node 要删除的节点
     */
    public void remove(Node node) {
      // 空链表没有节点,所以无法删除
      if (this.size == 0) return;

      if (this.size == 1) {
        // 链表只有一个节点,则删除完后,变为空链表
        this.head = null;
        this.tail = null;
      } else if (this.head == node) {
        // 如果要删除的节点是头节点
        this.head = this.head.next;
        this.head.prev = null;
      } else if (this.tail == node) {
        // 如果要删除的节点是尾节点
        this.tail = this.tail.prev;
        this.tail.next = null;
      } else {
        // 如果要删除的节点是中间节点
        node.prev.next = node.next;
        node.next.prev = node.prev;
      }

      this.size--;
    }
  }

  /** keyMap用于记录key对应的node */
  HashMap<Integer, Node> keyMap;

  /** freqMap的key是访问次数,value是具有相同访问次数的key对应的node组成的链表,链表头是最远访问的,链表尾是最近访问的 */
  HashMap<Integer, Link> freqMap;

  /** LFU缓存中能记录的最多key的数量 */
  int capacity;

  /** LFU缓存中所有的key中最少的访问次数 */
  int minFreq;

  public LFUCache(int capacity) {
    this.keyMap = new HashMap<>();
    this.freqMap = new HashMap<>();
    this.capacity = capacity;
    this.minFreq = 0;
  }

  public int get(int key) {
    if (this.keyMap.containsKey(key)) {
      // 存在对应key,则返回对应val
      Node node = this.keyMap.get(key);
      incNodeFreq(node); // get操作会新增对应key的访问次数
      return node.val;
    } else {
      // 不存在对应key,则返回-1
      return -1;
    }
  }

  public void put(int key, int value) {
    // 对应key已存在,则为更新场景
    if (this.keyMap.containsKey(key)) {
      Node node = this.keyMap.get(key);
      incNodeFreq(node); // 更新操作会增加对应key的访问次数
      node.val = value;
    }
    // 对应key不存在,则为新增场景
    else {
      // 先判断容量是否超过,keyMap的key数量就是LFU缓存中记录的key数量
      if (this.keyMap.size() >= this.capacity) {
        Link link = this.freqMap.get(this.minFreq);
        int removeKey = link.head.key;
        link.remove(link.head); // 最少访问次数所在链表的头节点,即为:最少、最远访问的key,容量不足时,需要优先删除它
        this.keyMap.remove(removeKey); // 注意,不要遗漏将keyMap中该key删除
      }

      // 新增key,则对应key的访问次数为1,且为最少访问次数
      this.minFreq = 1;
      Node node = new Node(key, value, this.minFreq);

      // 将新增key对应的node加入到freqMap,和keyMap中
      this.freqMap.putIfAbsent(this.minFreq, new Link());
      this.freqMap.get(this.minFreq).addLast(node);
      this.keyMap.put(key, node);
    }
  }

  /**
   * 增加key的访问次数
   *
   * @param node key对应的node
   */
  public void incNodeFreq(Node node) {
    // 由于key的访问次数增加,因此要从原访问次数的链表中删除
    this.freqMap.get(node.freq).remove(node);

    // 如果原链表删除当前key对应的节点后为空,且原链表对应的访问次数就是最少访问次数
    if (this.freqMap.get(node.freq).size == 0 && node.freq == this.minFreq) {
      // 则最少访问次数对应的key没有了,因此最少访问次数++(即当前key访问次数++后,当前key的访问次数还是最少访问次数)
      this.minFreq++;
    }

    // 当前key访问次数++
    node.freq++;

    // 将当前key对应的node转移到对应增加后的访问次数对应的链表尾部(最近访问)
    this.freqMap.putIfAbsent(node.freq, new Link());
    this.freqMap.get(node.freq).addLast(node);
  }
}

JS算法源码

/** 双向链表节点 */
class Node {
  constructor(key, val, freq) {
    /** 记录本题的键 */
    this.key = key;
    /** 记录本题的值 */
    this.val = val;
    /** 记录该键被访问的次数 */
    this.freq = freq;
    /** 当前节点的上一个节点 */
    this.prev = null;
    /** 当前节点的下一个节点 */
    this.next = null;
  }
}

/** 双向链表 */
class Link {
  constructor() {
    /** 链表中节点个数 */
    this.size = 0;
    /** 链表头节点 */
    this.head = null;
    /** 链表尾节点 */
    this.tail = null;
  }

  /**
   * 尾插
   * @param {*} node 要被插入的节点
   */
  addLast(node) {
    if (this.size == 0) {
      // 空链表,则node节点插入后,即为头、尾节点
      this.head = node;
      this.tail = node;
    } else {
      // 非空链表,则node节点插入到tail节点后面
      this.tail.next = node;
      node.prev = this.tail;
      this.tail = node;
    }
    this.size++;
  }

  /**
   * 删除指定节点
   * @param {*} node 要删除的节点
   */
  remove(node) {
    // 空链表没有节点,所以无法删除
    if (this.size == 0) return;

    if (this.size == 1) {
      // 链表只有一个节点,则删除完后,变为空链表
      this.head = null;
      this.tail = null;
    } else if (this.head == node) {
      // 如果要删除的节点是头节点
      this.head = this.head.next;
      this.head.prev = null;
    } else if (this.tail == node) {
      // 如果要删除的节点是尾节点
      this.tail = this.tail.prev;
      this.tail.next = null;
    } else {
      // 如果要删除的节点是中间节点
      node.prev.next = node.next;
      node.next.prev = node.prev;
    }

    this.size--;
  }
}

/**
 * @param {number} capacity
 */
var LFUCache = function (capacity) {
  /** keyMap用于记录key对应的node */
  this.keyMap = new Map();
  /** freqMap的key是访问次数,value是具有相同访问次数的key对应的node组成的链表,链表头是最远访问的,链表尾是最近访问的 */
  this.freqMap = new Map();
  /** LFU缓存中能记录的最多key的数量 */
  this.capacity = capacity;
  /** LFU缓存中所有的key中最少的访问次数 */
  this.minFreq = 0;
};

/**
 * @param {number} key
 * @return {number}
 */
LFUCache.prototype.get = function (key) {
  if (this.keyMap.has(key)) {
    // 存在对应key,则返回对应val
    const node = this.keyMap.get(key);
    this.incNodeFreq(node); // get操作会新增对应key的访问次数
    return node.val;
  } else {
    // 不存在对应key,则返回-1
    return -1;
  }
};

/**
 * @param {number} key
 * @param {number} value
 * @return {void}
 */
LFUCache.prototype.put = function (key, value) {
  // 对应key已存在,则为更新场景
  if (this.keyMap.has(key)) {
    const node = this.keyMap.get(key);
    this.incNodeFreq(node); // 更新操作会增加对应key的访问次数
    node.val = value;
  }
  // 对应key不存在,则为新增场景
  else {
    // 先判断容量是否超过,keyMap的key数量就是LFU缓存中记录的key数量
    if (this.keyMap.size >= this.capacity) {
      const link = this.freqMap[this.minFreq];
      const removeKey = link.head.key;
      link.remove(link.head); // 最少访问次数所在链表的头节点,即为最少、最远访问的key,容量不足时,需要优先删除它
      this.keyMap.delete(removeKey); // 注意,不要遗漏将keyMap中该key删除
    }

    // 新增key,则对应key的访问次数为1,且为最少访问次数
    this.minFreq = 1;

    // 将新增key对应的node加入到freqMap,和keyMap中
    const node = new Node(key, value, this.minFreq);

    if (this.freqMap[this.minFreq] == undefined) {
      this.freqMap[this.minFreq] = new Link();
    }

    this.freqMap[this.minFreq].addLast(node);
    this.keyMap.set(key, node);
  }
};

/**
 * 增加key的访问次数
 * @param {*} node key对应的node
 */
LFUCache.prototype.incNodeFreq = function (node) {
  // 由于key的访问次数增加,因此要从原访问次数的链表中删除
  this.freqMap[node.freq].remove(node);

  // 如果原链表删除当前key对应的节点后为空,且原链表对应的访问次数就是最少访问次数
  if (this.freqMap[node.freq].size == 0 && node.freq == this.minFreq) {
    // 则最少访问次数对应的key没有了,因此最少访问次数++(即当前key访问次数++后,当前key的访问次数还是最少访问次数)
    this.minFreq++;
  }

  // 当前key访问次数++
  node.freq++;

  // 将当前key对应的node转移到对应增加后的访问次数对应的链表尾部(最近访问)
  if (this.freqMap[node.freq] == undefined) {
    this.freqMap[node.freq] = new Link();
  }

  this.freqMap[node.freq].addLast(node);
};

Python算法源码

# 双向链表节点
class Node:
    def __init__(self, key, val, freq):
        """
        :param key: 记录本题的键
        :param val: 记录本题的值
        :param freq: 记录该键被访问的次数
        """
        self.key = key
        self.val = val
        self.freq = freq
        self.prev = None
        self.next = None


# 双向链表
class Link:
    def __init__(self):
        self.size = 0
        self.head = None
        self.tail = None

    def addLast(self, node):
        """
        尾插
        :param node: 要被插入的节点
        """
        if self.size == 0:
            # 空链表,则node节点插入后,即为头、尾节点
            self.head = node
            self.tail = node
        else:
            # 非空链表,则node节点插入到tail节点后面
            self.tail.next = node
            node.prev = self.tail
            self.tail = node

        self.size += 1

    def remove(self, node):
        """
        删除指定节点
        :param node: 要删除的节点
        """
        if self.size == 0:
            # 空链表没有节点,所以无法删除
            return

        if self.size == 1:
            # 链表只有一个节点,则删除完后,变为空链表
            self.head = None
            self.tail = None
        elif self.head == node:
            # 如果要删除的节点是头节点
            self.head = self.head.next
            self.head.prev = None
        elif self.tail == node:
            # 如果要删除的节点是尾节点
            self.tail = self.tail.prev
            self.tail.next = None
        else:
            # 如果要删除的节点是中间节点
            node.prev.next = node.next
            node.next.prev = node.prev

        self.size -= 1


class LFUCache(object):

    def __init__(self, capacity):
        self.keyMap = {}  # keyMap用于记录key对应的node
        self.freqMap = {}  # freqMap的key是访问次数,value是具有相同访问次数的key对应的node组成的链表,链表头是最远访问的,链表尾是最近访问的
        self.capacity = capacity  # LFU缓存中能记录的最多key的数量
        self.minFreq = 0  # LFU缓存中所有的key中最少的访问次数

    def get(self, key):
        if key in self.keyMap:
            # 存在对应key,则返回对应val
            node = self.keyMap[key]
            self.incNodeFreq(node)  # get操作会新增对应key的访问次数
            return node.val
        else:
            # 不存在对应key,则返回-1
            return -1

    def put(self, key, value):
        if key in self.keyMap:
            # 对应key已存在,则为更新场景
            node = self.keyMap[key]
            self.incNodeFreq(node)  # 更新操作会增加对应key的访问次数
            node.val = value
        else:
            # 对应key不存在,则为新增场景
            # 先判断容量是否超过,keyMap的key数量就是LFU缓存中记录的key数量
            if len(self.keyMap) >= self.capacity:
                link = self.freqMap[self.minFreq]
                removeKey = link.head.key
                link.remove(link.head)  # 最少访问次数所在链表的头节点,即为:最少、最远访问的key,容量不足时,需要优先删除它
                self.keyMap.pop(removeKey)  # 注意,不要遗漏将keyMap中该key删除

            # 新增key,则对应key的访问次数为1,且为最少访问次数
            self.minFreq = 1
            node = Node(key, value, self.minFreq)

            # 将新增key对应的node加入到freqMap,和keyMap中
            self.freqMap.setdefault(self.minFreq, Link())
            self.freqMap.get(self.minFreq).addLast(node)
            self.keyMap[key] = node

    def incNodeFreq(self, node):
        """
        增加key的访问次数
        :param node: key对应的node
        """
        # 由于key的访问次数增加,因此要从原访问次数的链表中删除
        self.freqMap[node.freq].remove(node)

        # 如果原链表删除当前key对应的节点后为空,且原链表对应的访问次数就是最少访问次数
        if self.freqMap[node.freq].size == 0 and node.freq == self.minFreq:
            # 则最少访问次数对应的key没有了,因此最少访问次数++(即当前key访问次数++后,当前key的访问次数还是最少访问次数)
            self.minFreq += 1

        # 当前key访问次数++
        node.freq += 1

        # 将当前key对应的node转移到对应增加后的访问次数对应的链表尾部(最近访问)
        self.freqMap.setdefault(node.freq, Link())
        self.freqMap[node.freq].addLast(node)

到了这里,关于LeetCode - 460 LFU缓存(Java & JS & Python)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 页面置换算法(OPT、FIFO、LRU、时钟、LFU)

    在地址映射过程中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法

    2024年02月06日
    浏览(27)
  • [架构之路-188]-《软考-系统分析师》-3-操作系统 - 图解页面替换算法LRU、LFU

    目录 前言: 一、内存置换算法的缘由 二、算法详解 2.1  最佳页面置换算法(OPT) =》 理论上的最优,实际无法保证 2.2 先进先出置换算法(FIFO)-- 按加载时间/最早访问时间排序 2.3 最近最久未使用的置换算法(LRU)-- 按最后一次访问时间排序 2.4 时钟页面置换算法(Lock)

    2024年01月21日
    浏览(74)
  • 华为OD机试 - 文件缓存系统(Java & JS & Python & C++)

    题目描述 请设计一个文件缓存系统,该文件缓存系统可以指定缓存的最大值(单位为字节)。 文件缓存系统有两种操作: 存储文件(put) 读取文件(get) 操作命令为: put fileName fileSize get fileName 存储文件是把文件放入文件缓存系统中; 读取文件是从文件缓存系统中访问已

    2024年04月10日
    浏览(79)
  • 华为OD机试 - 文件缓存系统(Java & JS & Python & C & C++)

    哈喽,本题库完全免费,收费是为了防止被爬,大家订阅专栏后可以私信联系退款。感谢支持 请设计一个文件缓存系统,该文件缓存系统可以指定缓存的最大值(单位为字节)。 文件缓存系统有两种操作: 存储文件(put) 读取文件(get) 操作命令为: put fileName fileSize ge

    2024年04月08日
    浏览(45)
  • LeetCode - 1371 每个元音包含偶数次的最长子字符串(Java & JS & Python & C)

    题目来源 1371. 每个元音包含偶数次的最长子字符串 - 力扣(LeetCode) 题目描述 给你一个字符串  s  ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 \\\'a\\\',\\\'e\\\',\\\'i\\\',\\\'o\\\',\\\'u\\\' ,在子字符串中都恰好出现了偶数次。 示例 示例 1 输入:s = \\\"eleetminicoworoep\\\" 输出:

    2024年01月25日
    浏览(43)
  • 【华为OD机试】运维日志排序(Java&Python&C++&JS实现)

    本文收录于专栏:算法之翼 本专栏所有题目均包含优质解题思路,高质量解题代码(JavaPythonC++JS分别实现),详细代码讲解,助你深入学习,深度掌握!

    2024年04月27日
    浏览(42)
  • 【华为OD机试】叠积木(贪心算法—Java&Python&C++&JS实现)

    本文收录于专栏:算法之翼 本专栏所有题目均包含优质解题思路,高质量解题代码(JavaPythonC++JS分别实现),详细代码讲解,助你深入学习,深度掌握!

    2024年04月13日
    浏览(41)
  • 【华为OD机试】找单词(深度优先搜索—Java&Python&C++&JS实现)

    本文收录于专栏:算法之翼 本专栏所有题目均包含优质解题思路,高质量解题代码(JavaPythonC++JS分别实现),详细代码讲解,助你深入学习,深度掌握! 给一个字符串和一个二维字符数组,如果该字符串存在于该数组中,则按字符串的字符顺序输

    2024年04月10日
    浏览(62)
  • 【华为OD机试】数据单元的变化替换(Java&Python&C++&JS实现)

    本文收录于专栏:算法之翼 本专栏所有题目均包含优质解题思路,高质量解题代码(JavaPythonC++JS分别实现),详细代码讲解,助你深入学习,深度掌握!

    2024年04月08日
    浏览(60)
  • 【华为OD机试】芯片资源限制(贪心算法—Java&Python&C++&JS实现)

    本文收录于专栏:算法之翼 本专栏所有题目均包含优质解题思路,高质量解题代码(JavaPythonC++JS分别实现),详细代码讲解,助你深入学习,深度掌握!

    2024年04月11日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包