哈希表题目:LFU 缓存

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

题目

标题和出处

标题:LFU 缓存

出处:460. LFU 缓存

难度

9 级

题目描述

要求

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

实现 LFUCache \texttt{LFUCache} LFUCache 类:

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

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

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

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

示例

示例 1:

输入:
["LFUCache",   "put",   "put",   "get",   "put",   "get",   "get",   "put",   "get",   "get",   "get"] \texttt{["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"]} ["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]] \texttt{[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]} [[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] \texttt{[null, null, null, 1, null, -1, 3, null, -1, 3, 4]} [null, null, null, 1, null, -1, 3, null, -1, 3, 4]
解释:
// cnt(x) \texttt{cnt(x)} cnt(x) 表示关键字 x \texttt{x} x 的使用计数
// cache=[] \texttt{cache=[]} cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
LFUCache   lfu   =   new   LFUCache(2); \texttt{LFUCache lfu = new LFUCache(2);} LFUCache lfu = new LFUCache(2);
lfu.put(1,   1); \texttt{lfu.put(1, 1);} lfu.put(1, 1); // cache=[1,_],   cnt(1)=1 \texttt{cache=[1,\_], cnt(1)=1} cache=[1,_], cnt(1)=1
lfu.put(2,   2); \texttt{lfu.put(2, 2);} lfu.put(2, 2); // cache=[2,1],   cnt(2)=1,   cnt(1)=1 \texttt{cache=[2,1], cnt(2)=1, cnt(1)=1} cache=[2,1], cnt(2)=1, cnt(1)=1
lfu.get(1); \texttt{lfu.get(1);} lfu.get(1); // 返回 1 \texttt{1} 1 cache=[1,2],   cnt(2)=1,   cnt(1)=2 \texttt{cache=[1,2], cnt(2)=1, cnt(1)=2} cache=[1,2], cnt(2)=1, cnt(1)=2
lfu.put(3,   3); \texttt{lfu.put(3, 3);} lfu.put(3, 3); // 去除关键字 2 \texttt{2} 2,因为 cnt(2)=1 \texttt{cnt(2)=1} cnt(2)=1,使用计数最小, cache=[3,1],   cnt(3)=1,   cnt(1)=2 \texttt{cache=[3,1], cnt(3)=1, cnt(1)=2} cache=[3,1], cnt(3)=1, cnt(1)=2
lfu.get(2); \texttt{lfu.get(2);} lfu.get(2); // 返回 -1 \texttt{-1} -1(未找到)
lfu.get(3); \texttt{lfu.get(3);} lfu.get(3); // 返回 3 \texttt{3} 3 cache=[3,1],   cnt(3)=2,   cnt(1)=2 \texttt{cache=[3,1], cnt(3)=2, cnt(1)=2} cache=[3,1], cnt(3)=2, cnt(1)=2
lfu.put(4,   4); \texttt{lfu.put(4, 4);} lfu.put(4, 4); // 去除关键字 1 \texttt{1} 1 1 \texttt{1} 1 3 \texttt{3} 3 cnt \texttt{cnt} cnt 相同,但 1 \texttt{1} 1 最久未使用, cache=[4,3],   cnt(4)=1,   cnt(3)=2 \texttt{cache=[4,3], cnt(4)=1, cnt(3)=2} cache=[4,3], cnt(4)=1, cnt(3)=2
lfu.get(1); \texttt{lfu.get(1);} lfu.get(1); // 返回 -1 \texttt{-1} -1(未找到)
lfu.get(3); \texttt{lfu.get(3);} lfu.get(3); // 返回 3 \texttt{3} 3 cache=[3,4],   cnt(4)=1,   cnt(3)=3 \texttt{cache=[3,4], cnt(4)=1, cnt(3)=3} cache=[3,4], cnt(4)=1, cnt(3)=3
lfu.get(4); \texttt{lfu.get(4);} lfu.get(4); // 返回 4 \texttt{4} 4 cache=[4,3],   cnt(4)=2,   cnt(3)=3 \texttt{cache=[4,3], cnt(4)=2, cnt(3)=3} cache=[4,3], cnt(4)=2, cnt(3)=3

数据范围

  • 0 ≤ capacity ≤ 10 4 \texttt{0} \le \texttt{capacity} \le \texttt{10}^\texttt{4} 0capacity104
  • 0 ≤ key ≤ 10 5 \texttt{0} \le \texttt{key} \le \texttt{10}^\texttt{5} 0key105
  • 0 ≤ value ≤ 10 9 \texttt{0} \le \texttt{value} \le \texttt{10}^\texttt{9} 0value109
  • 最多调用 2 × 10 5 \texttt{2} \times \texttt{10}^\texttt{5} 2×105 get \texttt{get} get put \texttt{put} put

前言

这道题是「LRU 缓存」的进阶,实现 LFU 缓存的方法和实现 LRU 缓存的方法相似,都是使用哈希表和双向链表。但是 LFU 缓存需要记录关键字的使用计数,因此情况更加复杂,需要使用两个哈希表记录,并对每个使用计数维护一个双向链表。

解法

思路和算法

实现 LFU 缓存需要使用哈希表和双向链表。双向链表中的每个结点存储键值对、该结点的使用计数以及该结点的前一个结点和后一个结点,结点顺序为从头结点到尾结点依次按照最近访问时间降序排列。两个哈希表中,关键字结点哈希表存储每个关键字对应的的结点,计数链表哈希表存储每个计数对应的双向链表。

为了便于操作,每个双向链表需要维护伪头结点和伪尾结点,链表的实际头结点为伪头结点的后一个结点,链表的实际尾结点为伪尾结点的前一个结点(只有当链表不为空时才存在实际头结点和实际尾结点)。初始时,伪头结点和伪尾结点相邻。

在 LFU 缓存中插入新的关键字时,如果缓存达到容量则需要首先移除使用计数最低的关键字,然后插入新的关键字,因此除了维护容量和两个哈希表之外,还需要维护最低使用计数。

构造方法中,将容量初始化为参数值,将最低使用计数初始化为 0 0 0,将两个哈希表初始化。

由于当容量为 0 0 0 时,缓存中不能存入任何关键字,因此 get \textit{get} get 操作返回 − 1 -1 1 put \textit{put} put 操作直接返回。以下对于 get \textit{get} get 操作和 put \textit{put} put 操作的说明只考虑容量大于 0 0 0 的情况。

对于 get \textit{get} get 操作,做法如下。

  • 如果关键字结点哈希表中存在关键字 key \textit{key} key,则在关键字结点哈希表中得到 key \textit{key} key 对应的结点,更新该结点的状态,返回结点的值。

  • 如果关键字结点哈希表中不存在关键字 key \textit{key} key,返回 − 1 -1 1

由于 get \textit{get} get 操作只是根据关键字 key \textit{key} key 从缓存中得到对应的值,虽然改变了关键字的使用计数和顺序,但是并没有添加新的关键字,因此关键字的数量不变,不会出现关键字数量超过容量的情况,不需要移除最不经常使用的关键字。

对于 put \textit{put} put 操作,首先判断关键字节点哈希表中是否存在关键字 key \textit{key} key。如果存在关键字 key \textit{key} key 则关键字的数量不变,不需要移除最不经常使用的关键字。如果不存在关键字 key \textit{key} key 则关键字的数量增加 1 1 1 个,如果在 put \textit{put} put 操作之前,关键字的数量已经达到容量,则需要移除最不经常使用的关键字。

移除最不经常使用的关键字的做法是:在计数链表哈希表中得到最低使用计数对应的双向链表,将该双向链表的实际尾结点(伪尾结点的前一个结点)从双向链表中删除,将实际尾结点的关键字从关键字结点哈希表中删除。

分析了移除最不经常使用的关键字的做法之后,可以得到 put \textit{put} put 操作的完整做法。

  • 如果关键字结点哈希表中存在关键字 key \textit{key} key,则在关键字结点哈希表中得到 key \textit{key} key 对应的结点,将该结点的值设为 value \textit{value} value,并更新该结点的状态。

  • 如果关键字结点哈希表中不存在关键字 key \textit{key} key,则需要执行两步操作,第一步是判断缓存容量是否已满并在容量已满的情况下移除最不经常使用的关键字,第二步是将新的键值对添加到缓存中。

    1. 如果关键字的数量已经达到容量,则需要移除最不经常使用的关键字;如果关键字的数量小于容量,则不移除任何关键字。

    2. 根据 key \textit{key} key value \textit{value} value 创建结点,由于新结点的使用计数是 1 1 1,因此将最低使用计数设为 1 1 1,在关键字结点哈希表中添加 key \textit{key} key 和该结点的对应关系,在计数链表哈希表中得到计数 1 1 1 对应的双向链表并将该结点添加到双向链表的头部。

get \textit{get} get put \textit{put} put 操作中,如果关键字结点哈希表中存在关键字 key \textit{key} key,则在得到对应的结点之后,都需要更新该结点的状态。更新结点状态的具体做法如下。

  1. 将结点的使用计数加 1 1 1,更新前的使用计数为原计数,更新后的使用计数为新计数。

  2. 在计数链表哈希表中得到原计数对应的双向链表,将结点从双向链表中删除。

  3. 在计数链表哈希表中得到新计数对应的双向链表,将结点添加到双向链表的头部。

  4. 上述操作之后,如果在计数链表哈希表中最低使用计数对应的双向链表为空,则该次更新操作将唯一的最低使用计数的关键字的使用计数加 1 1 1,因此将最低使用计数加 1 1 1

由于每次 get \textit{get} get 操作和 put \textit{put} put 操作都是对哈希表和双向链表的操作,哈希表操作和在链表中添加和删除结点操作的时间都是 O ( 1 ) O(1) O(1),因此 get \textit{get} get 操作和 put \textit{put} put 操作的时间复杂度都是 O ( 1 ) O(1) O(1)

代码

class LFUCache {
    private class Node {
        private int key;
        private int value;
        private int count;
        private Node prev;
        private Node next;

        public Node() {
            this(-1, -1);
        }

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
            count = 1;
            prev = null;
            next = null;
        }

        public int getKey() {
            return key;
        }

        public int getValue() {
            return value;
        }

        public void setValue(int value) {
            this.value = value;
        }

        public int getCount() {
            return count;
        }

        public void increaseCount() {
            count++;
        }

        public Node getPrev() {
            return prev;
        }

        public void setPrev(Node prev) {
            this.prev = prev;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }

    private class DoublyLinkedList {
        private Node pseudoHead;
        private Node pseudoTail;
        private int size;

        public DoublyLinkedList() {
            pseudoHead = new Node();
            pseudoTail = new Node();
            pseudoHead.setNext(pseudoTail);
            pseudoTail.setPrev(pseudoHead);
            size = 0;
        }

        public void addNode(Node node) {
            Node nextNode = pseudoHead.getNext();
            node.setNext(nextNode);
            nextNode.setPrev(node);
            pseudoHead.setNext(node);
            node.setPrev(pseudoHead);
            size++;
        }

        public void removeNode(Node node) {
            Node prev = node.getPrev(), next = node.getNext();
            prev.setNext(next);
            next.setPrev(prev);
            size--;
        }

        public Node getTail() {
            return pseudoTail.getPrev();
        }

        public boolean isEmpty() {
            return size == 0;
        }
    }

    private int capacity;
    private int minCount;
    private Map<Integer, Node> keyNodeMap;
    private Map<Integer, DoublyLinkedList> countListMap;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        minCount = 0;
        keyNodeMap = new HashMap<Integer, Node>();
        countListMap = new HashMap<Integer, DoublyLinkedList>();
    }

    public int get(int key) {
        if (capacity == 0) {
            return -1;
        }
        if (keyNodeMap.containsKey(key)) {
            Node node = keyNodeMap.get(key);
            update(node);
            return node.getValue();
        } else {
            return -1;
        }
    }

    public void put(int key, int value) {
        if (capacity == 0) {
            return;
        }
        if (keyNodeMap.containsKey(key)) {
            Node node = keyNodeMap.get(key);
            node.setValue(value);
            update(node);
        } else {
            if (keyNodeMap.size() == capacity) {
                DoublyLinkedList minCountList = countListMap.get(minCount);
                Node tail = minCountList.getTail();
                minCountList.removeNode(tail);
                keyNodeMap.remove(tail.getKey());
            }
            minCount = 1;
            Node node = new Node(key, value);
            keyNodeMap.put(key, node);
            countListMap.putIfAbsent(1, new DoublyLinkedList());
            DoublyLinkedList list = countListMap.get(1);
            list.addNode(node);
        }
    }

    private void update(Node node) {
        int prevCount = node.getCount();
        node.increaseCount();
        int currCount = node.getCount();
        DoublyLinkedList prevList = countListMap.get(prevCount);
        prevList.removeNode(node);
        countListMap.putIfAbsent(currCount, new DoublyLinkedList());
        DoublyLinkedList currList = countListMap.get(currCount);
        currList.addNode(node);
        if (countListMap.get(minCount).isEmpty()) {
            minCount++;
        }
    }
}

复杂度分析

  • 时间复杂度:构造方法和各项操作的时间复杂度都是 O ( 1 ) O(1) O(1)

  • 空间复杂度: O ( capacity ) O(\textit{capacity}) O(capacity),其中 capacity \textit{capacity} capacity 是 LFU 缓存的容量。每次 get \textit{get} get put \textit{put} put 操作过程中和操作结束之后,哈希表中的键值对数量和双向链表中的结点数量(不包括伪头结点和伪尾结点)不会超过 capacity \textit{capacity} capacity文章来源地址https://www.toymoban.com/news/detail-467445.html

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

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

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

相关文章

  • 哈希表题目:原子的数量

    标题:原子的数量 出处:726. 原子的数量 8 级 要求 给你一个表示化学式的字符串 formula texttt{formula} formula ,返回每种原子的数量。 原子总是以一个大写字母开始,跟随零个或任意个小写字母,表示原子的名字。 如果数量大于 1 texttt{1} 1 ,原子后会跟着数字表示原子的数量

    2024年02月06日
    浏览(26)
  • leetcode 哈希表相关题目

    题目:https://leetcode.cn/problems/valid-anagram/description/ 题解:https://leetcode.cn/problems/valid-anagram/solutions/2602947/shu-zu-ha-xi-biao-tong-ji-mei-ge-zi-mu-c-vhh5/ 题目:https://leetcode.cn/problems/intersection-of-two-arrays/description/ 题解:https://leetcode.cn/problems/intersection-of-two-arrays/solutions/2603171/jie-guo-shu-zu-un

    2024年01月21日
    浏览(29)
  • 哈希表题目:设计推特

    标题:设计推特 出处:355. 设计推特 7 级 要求 设计一个简化版的推特,可以让用户实现发送推文,关注/取消关注其他用户,能够看见关注人(包括自己)的最近 10 texttt{10} 10 条推文。 实现 Twitter texttt{Twitter} Twitter 类: Twitter() texttt{Twitter()} Twitter() 初始化简易版推特对象。

    2024年02月05日
    浏览(39)
  • 哈希表C++哈希表详解(知识点+相关LeetCode题目)

    目录 前言 一、什么是哈希表 二、哈希表的操作 2.1 操作时间复杂度 2.2 哈希表通用API 2.3 建立哈希表 2.3 哈希表常见结构介绍 Set(集合) Map(映射) unordered_map(哈希表) 三、哈希表的力扣经典题目 有效的字母异位词242 两个数组的交集 349 两数之和1 四数相加II 454 三数之和

    2024年03月20日
    浏览(38)
  • 哈希表题目:TinyURL 的加密与解密

    标题:TinyURL 的加密与解密 出处:535. TinyURL 的加密与解密 7 级 要求 TinyURL 是一种 URL 简化服务。当你输入一个 URL,例如 https://leetcode.com/problems/design-tinyurl 时,它将返回一个简化的 URL,例如 http://tinyurl.com/4e9iAk 。设计一个类对 URL 加密和对 TinyURL 解密。 你的加密和解密算法如

    2024年02月04日
    浏览(24)
  • 全面理解哈希,哈希的底层原理是如何实现的,哈希题型的做题思路与题目清单(不断更新)

    哈希(Hash)是一种算法,它接受一个输入(或“消息”),并返回一个固定大小的字符串。这个输出字符串的大小通常以字节为单位,输出的内容看起来是随机的且整个过程是单向的。 哈希的一些关键特性包括: 不管你输入的信息有多大,哈希值的大小总是固定的。 即使只

    2024年02月04日
    浏览(34)
  • 必刷算法题之哈希篇(题目及代码)---C++

    解法1 :(对于大规模数据,时间和空间复杂度会超出) 解题思路如下: 假设第一个数为a,用目标值c减去第一个数a,得到b,然后遍历后面的数,查看b是否在后面的数组中 解法2 :(利用哈希表) 解法1 :(排序) 由于多数元素是指在数组中出现次数大于 【n/2】 的元素,

    2023年04月18日
    浏览(29)
  • 【缓存中间件】Redis哈希槽的概念

    分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。。 需要重点关注的是数据分区规则。常见的分区规则有哈希分区和顺序分区两种,哈希分区离散度好、数据分布业务无关、无法顺

    2024年02月13日
    浏览(33)
  • 【leetcode100-035】【链表/哈希链表】LRU缓存

    【题干】 请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。 实现  LRUCache  类: LRUCache(int capacity)  以  正整数  作为容量  capacity  初始化 LRU 缓存 int get(int key)  如果  key  存在于缓存中,则返回的值,否则返回  -1  。 void put(int key, in

    2024年02月01日
    浏览(33)
  • 二刷LeetCode--146.LRU缓存(C++版本),必会题目

    本题思路:因为需要记录元素的出入顺序,并且每次访问之后需要将该节点提到最前面,因此需要使用双向链表(单链表不方便删除操作),而为了可以在常量时间复杂度内找到对应的元素,我们需要使用哈希表,将每一个插入的元素在哈希表中进行记录.哈希表的key就是插入的key,而哈希

    2024年02月13日
    浏览(26)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包