牛客题解 | 设计LFU缓存结构

news/2025/2/21 10:09:21/文章来源:https://www.cnblogs.com/wc529065/p/18728689

题目

题目链接

题目的主要信息:
  • 实现LFU的set与get函数,且复杂度为\(O(1)\)
  • 每次调用这两个函数会给一个频率赋值,超出长度则移除频率最少的,若有频率相同,则移除访问时间最早的
举一反三:

学习完本题的思路你可以解决如下题目:

BM100. 设计LRU缓存结构

方法:双哈希表(推荐使用)

知识点:哈希表

哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在\(O(1)\)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。

思路:

需要在\(O(1)\)时间内实现两个操作,我们第一时间想到的还是哈希表,利用哈希表保存LFU的key值,而哈希表的value值对应了另一边存着每个缓存需要的类的节点,这样就实现了直接访问。

但是我们还需要每次最快找到最久未使用的频率最小的节点,这时候我们可以考虑使用一个全局变量,跟踪记录最小的频率,有了最小的频率,怎样直接找到这个频率最小的节点,还是使用哈希表,key值记录各个频率,而value值就是后面接了一串相同频率的节点。如何保证每次都是最小频率的最久为使用,我们用双向链表将统一频率的节点连起来就好了,每次新加入这个频率的都在链表头,而需要去掉的都在链表尾。

这样我们方法就是双哈希表。

具体做法:

  • step 1:需要建立每个节点,每个节点代表加入LFU中的结构,需要频率,key值,val值。(C++代码中使用数组的三维实现)
class Node{ int freq;int key;int val;//初始化public Node(int freq, int key, int val) {this.freq = freq;this.key = key;this.val = val;}
}
  • step 2:建立第一个哈希表mp,在每个节点的key值与节点直接建立映射连接,建立第二个哈希表freq_mp,在频率与该频率下的处在双向链表中的节点们直接建立映射连接(即与这个双向链表建立映射)。这样可以根据key值直接访问每个节点,通过频率直接找到最少使用且最久未使用的节点。复杂度都是\(O(1)\)
//频率到双向链表的哈希表
Map<Integer, LinkedList<Node> > freq_mp = new HashMap<>();
//key到节点的哈希表
Map<Integer, Node> mp = new HashMap<>();
  • step 3:LFU的剩余容量和当前最小频率设置为全局变量,并初始化。
  • step 4:遍历函数的操作数组,检查第一个元素判断是属于set操作还是get操作。
  • step 5:对于set操作,如果哈希表中已经有了key值,说明它已经在LFU中了,直接修改节点频率和val即可。
//在哈希表中找到key值
if(mp.containsKey(key)) //若是哈希表中有,则更新值与频率update(mp.get(key), key, value);
  • step 6:如果哈希表中没有,此时要考虑还有没有容量,若是有容量,容量需要减1;若是没有容量,从第二个哈希表中根据当前最小频率取出最小频率的双向链表,链表最后一个就是使用频率最低且最久未使用的,将其取出:链表中删除,哈希表中也有删除,如果链表为空了,则相应频率在哈希表中也要删除。
//哈希表中没有,即链表中没有
if(size == 0){//满容量取频率最低且最早的删掉int oldkey = freq_mp.get(min_freq).getLast().key; //频率哈希表的链表中删除freq_mp.get(min_freq).removeLast(); if(freq_mp.get(min_freq).isEmpty()) freq_mp.remove(min_freq); //链表哈希表中删除mp.remove(oldkey); 
}
  • step 7:加入的时候,添加新的节点,频次为1,节点加入第一个哈希表,同时加入第二个哈希表相应频率链表的首部。
if(!freq_mp.containsKey(freq + 1))freq_mp.put(freq + 1, new LinkedList<Node>());
//插入频率加一的双向链表表头,链表中对应:freq key value
freq_mp.get(freq + 1).addFirst(new Node(freq + 1, key, value)); 
mp.put(key, freq_mp.get(freq + 1).getFirst());
  • step 8:对于get操作,直接看第一个哈希表key值有没有找到,有则可以访问,然后修改频率,没有则返回-1.
if(mp.containsKey(key)){ Node node = mp.get(key);//根据哈希表直接获取值res = node.val;//更新频率 update(node, key, res); 
}
  • step 9:修改频率的时候,将该频率下该节点取出,放入哈希表该频率加1的链表首部。若是该频率下链表没有节点了,则哈希表中删除这个频率,同时若是修改前的链表频率与最低频率相等,说明最低已经增长了。
//找到频率
int freq = node.freq;
//原频率中删除该节点
freq_mp.get(freq).remove(node); 
//哈希表中该频率已无节点,直接删除
if(freq_mp.get(freq).isEmpty()){ freq_mp.remove(freq);//若当前频率为最小,最小频率加1if(min_freq == freq) min_freq++;}
......//插入频率加一的双向链表表头,链表中对应:freq key value

图示:

alt

Java代码实现:

import java.util.*;
public class Solution {//设置节点结构static class Node{ int freq;int key;int val;//初始化public Node(int freq, int key, int val) {this.freq = freq;this.key = key;this.val = val;}}//频率到双向链表的哈希表private Map<Integer, LinkedList<Node> > freq_mp = new HashMap<>();//key到节点的哈希表private Map<Integer, Node> mp = new HashMap<>();//记录缓存剩余容量private int size = 0; //记录当前最小频次private int min_freq = 0;public int[] LFU (int[][] operators, int k) {//构建初始化连接//链表剩余大小this.size = k;//获取操作数int len = (int)Arrays.stream(operators).filter(x -> x[0] == 2).count();int[] res = new int[len];//遍历所有操作for(int i = 0, j = 0; i < operators.length; i++){if(operators[i][0] == 1)//set操作set(operators[i][1], operators[i][2]);else//get操作res[j++] = get(operators[i][1]);}return res;}//调用函数时更新频率或者val值private void update(Node node, int key, int value) { //找到频率int freq = node.freq;//原频率中删除该节点freq_mp.get(freq).remove(node); //哈希表中该频率已无节点,直接删除if(freq_mp.get(freq).isEmpty()){ freq_mp.remove(freq);//若当前频率为最小,最小频率加1if(min_freq == freq) min_freq++;}if(!freq_mp.containsKey(freq + 1))freq_mp.put(freq + 1, new LinkedList<Node>());//插入频率加一的双向链表表头,链表中对应:freq key valuefreq_mp.get(freq + 1).addFirst(new Node(freq + 1, key, value)); mp.put(key, freq_mp.get(freq + 1).getFirst());}//set操作函数private void set(int key, int value) {//在哈希表中找到key值if(mp.containsKey(key)) //若是哈希表中有,则更新值与频率update(mp.get(key), key, value);else{ //哈希表中没有,即链表中没有if(size == 0){//满容量取频率最低且最早的删掉int oldkey = freq_mp.get(min_freq).getLast().key; //频率哈希表的链表中删除freq_mp.get(min_freq).removeLast(); if(freq_mp.get(min_freq).isEmpty()) freq_mp.remove(min_freq); //链表哈希表中删除mp.remove(oldkey); }//若有空闲则直接加入,容量减1else size--; //最小频率置为1min_freq = 1; //在频率为1的双向链表表头插入该键if(!freq_mp.containsKey(1))freq_mp.put(1, new LinkedList<Node>());freq_mp.get(1).addFirst(new Node(1, key, value)); //哈希表key值指向链表中该位置mp.put(key, freq_mp.get(1).getFirst()); }}//get操作函数private int get(int key) {int res = -1;//查找哈希表if(mp.containsKey(key)){ Node node = mp.get(key);//根据哈希表直接获取值res = node.val;//更新频率 update(node, key, res); }return res;}
}

C++代码实现

class Solution {
public://用list模拟双向链表,双向链表中数组第0位为频率,第1位为key,第2位为val//频率到双向链表的哈希表unordered_map<int, list<vector<int> > > freq_mp; //key到双向链表节点的哈希表unordered_map<int, list<vector<int> > ::iterator> mp;//记录当前最小频次int min_freq = 0; //记录缓存剩余容量int size = 0; vector<int> LFU(vector<vector<int> >& operators, int k) {//记录输出vector<int> res; size = k; //遍历所有操作for(int i = 0; i < operators.size(); i++){auto op = operators[i];if(op[0] == 1)//set操作set(op[1], op[2]);else//get操作res.push_back(get(op[1]));}return res;}//调用函数时更新频率或者val值void update(list<vector<int> >::iterator iter, int key, int value) { //找到频率int freq = (*iter)[0];//原频率中删除该节点freq_mp[freq].erase(iter); //哈希表中该频率已无节点,直接删除if(freq_mp[freq].empty()){ freq_mp.erase(freq);//若当前频率为最小,最小频率加1if(min_freq == freq) min_freq++;}//插入频率加一的双向链表表头,链表中对应:freq key valuefreq_mp[freq + 1].push_front({freq + 1, key, value}); mp[key] = freq_mp[freq + 1].begin(); }//set操作函数void set(int key, int value) {//在哈希表中找到key值auto it = mp.find(key); if(it != mp.end())//若是哈希表中有,则更新值与频率update(it->second, key, value);else{ //哈希表中没有,即链表中没有if(size == 0){//满容量取频率最低且最早的删掉int oldkey = freq_mp[min_freq].back()[1]; //频率哈希表中删除freq_mp[min_freq].pop_back(); if(freq_mp[min_freq].empty()) freq_mp.erase(min_freq); //链表哈希表中删除mp.erase(oldkey); }//若有空闲则直接加入,容量减1else size--; //最小频率置为1min_freq = 1; //在频率为1的双向链表表头插入该键freq_mp[1].push_front({1, key, value}); //哈希表key值指向链表中该位置mp[key] = freq_mp[1].begin(); }}//get操作函数int get(int key) {int res = -1;//查找哈希表auto it = mp.find(key);if(it != mp.end()){ auto iter = it->second; //根据哈希表直接获取值res = (*iter)[2];//更新频率 update(iter, key, res); }return res;}
};

Python代码实现:

import collections
class Node:def __init__(self, freq, key, val):self.freq = freqself.key = keyself.val = valclass Solution:def __init__(self):#记录剩余空间self.size = 0#key到双向链表节点的哈希表self.mp = dict()#频率到链表的哈希表self.freq_mp = dict(collections.deque())#记录当前最小频次self.min_freq = 0#调用函数时更新频率或者val值def update(self, node: Node, key: int, value: int): #找到频率freq = node.freq#原频率中删除该节点self.freq_mp[freq].remove(node) #哈希表中该频率已无节点,直接删除if len(self.freq_mp[freq]) == 0: self.freq_mp.pop(freq)#若当前频率为最小,最小频率加1if self.min_freq == freq: self.min_freq += 1#插入频率加一的双向链表表头,链表中对应:freq key valuenode = Node(freq + 1, key, value)if freq + 1 not in self.freq_mp:self.freq_mp[freq + 1] = collections.deque()self.freq_mp[freq + 1].appendleft(node)self.mp[key] = self.freq_mp[freq + 1][0]#set操作函数def set(self, key:int, value: int):#在哈希表中找到key值if key in self.mp:#若是哈希表中有,则更新值与频率self.update(self.mp[key], key, value)else:#哈希表中没有,即链表中没有if self.size == 0:#满容量取频率最低且最早的删掉oldnode = self.freq_mp[self.min_freq].pop() #频率哈希表的链表中删除if len(self.freq_mp[self.min_freq]) == 0:self.freq_mp.pop(self.min_freq) #链表哈希表中删除self.mp.pop(oldnode.key)#若有空闲则直接加入,容量减1else: self.size -= 1#最小频率置为1self.min_freq = 1node = Node(1, key, value)if 1 not in self.freq_mp:self.freq_mp[1] = collections.deque()self.freq_mp[1].appendleft(node)#哈希表key值指向链表中该位置self.mp[key] = self.freq_mp[1][0]#get操作函数def get(self, key: int) -> int:res = -1#查找哈希表if key in self.mp:node = self.mp[key]#根据哈希表直接获取值res = node.val#更新频率 self.update(node, key, res)return resdef LFU(self , operators: List[List[int]], k: int) -> List[int]:res = []#构建初始化连接#链表剩余大小self.size = k#遍历所有操作for i in range(len(operators)):op = operators[i]if op[0] == 1: #set操作self.set(op[1], op[2])else:#get操作res.append(self.get(op[1]))return resnd(self.get(op[1]))return res

复杂度分析:

  • 时间复杂度:\(O(n)\),取决于操作数\(n\)
  • 空间复杂度:\(O(k)\),取决于缓存容量\(k\)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/887487.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

牛客题解 | 设计LRU缓存结构

牛客输入输出题单题解题目 题目链接 题目的主要信息:实现LRU缓存的模拟结构,包括加入函数set,访问函数get 结构有长度限制,加入新数时,超出长度则需要删除最不常访问的,其中set与get都访问 两个函数都是\(O(1)\)举一反三: 学习完本题的思路你可以解决如下题目: BM101. …

牛客题解 | 编辑距离(一)

牛客输入输出题单题解题目 题目链接 题目主要信息:给定两个长度可能不同的字符串,可以对第一个字符串增删改字符 求增删改的最少次数,让第一个字符串变成第二个字符串 字符串中只出现大小写字母举一反三: 学习完本题的思路你可以解决如下题目: BM65 最长公共子序列(二) BM6…

网上学习导航

推荐一个网上学习好玩的地方 学吧导航 网站截图

解决 windows 安装centos7虚拟机 每隔一段时间 虚拟机ip会变动 怎么固定他的ip

查看当前虚拟机的ip地址:我的是192.168.217.136然后获取到网关ip:打开 VMware Workstation,选中你要设置的虚拟机,点击 “编辑” -> “编辑虚拟机设置”。通过ipconfig 可以看到我的是ens160 我的文件就是 ifcfg-ens160 ,然后使用以下命令进行操作sudo nano /etc/sysco…

牛客题解 | 求二叉树的层序遍历

牛客输入输出题单题解题目 题目链接 题目的主要信息:将给定二叉树按行从上到下、从左到右的顺序输出 输出到一个二维数组中,数组中每行就是二叉树的一层举一反三: 学习完本题的思路你可以解决如下题目: BM27. 按之字形顺序打印二叉树 BM35. 判断是否是完全二叉树 方法一:非…

牛客题解 | 没有重复项数字的全排列

牛客输入输出题单题解题目 题目链接 题目主要信息:给定一个数组,求这组数字的全排列 数组无重复元素 以数字在数组中的位置靠前为优先级,按字典序排列输出举一反三: 学习完本题的思路你可以解决如下题目: BM56. 有重复项数字的全排列 BM58. 字符串的排列 BM60. 括号生成 递…

牛客题解 | 最长无重复子数组

牛客输入输出题单题解题目 题目链接 题目主要信息:题目给定一个数组,要找到其中最长的无重复的子数组的长度 子数组必须是数组中连续的一段举一反三: 学习完本题的思路你可以解决如下题目: BM90. 最小覆盖子串 方法:滑动窗口(推荐使用) 知识点1:滑动窗口 滑动窗口是指在数…

牛客题解 | 最长的括号子串

牛客输入输出题单题解题目 题目链接 题目主要信息:一个长度为\(n\)的仅包含左右括号的字符串 计算最长的格式正确的括号子串的长度举一反三: 学习完本题的思路你可以解决如下题目: BM65 最长公共子序列(二) BM66.最长公共子串 BM71.最长上升子序列(一) BM73 最长回文子串 BM…

牛客题解 | 最长公共前缀

牛客输入输出题单题解题目 题目链接 题目主要信息:给定一个字符串数组,其中有n个字符串,求所有字符串的最长公共前缀 公共前缀是指所有字符串都共有的前面部分的子串,从第一个字符开始举一反三: 学会了本题的思路,你将可以解决类似的字符串问题: BM83. 字符串变形 BM85. …

牛客题解 | 旋转数组

牛客输入输出题单题解题目 题目链接 题目主要信息:一个长度为\(n\)的数组,将数组整体循环右移\(m\)个位置(\(m\)可能大于\(n\)) 循环右移即最后\(m\)个元素放在数组最前面,前\(n-m\)个元素依次后移 不能使用额外的数组空间举一反三: 学习完本题的思路你可以解决如下题目:…

牛客题解 | 括号生成

牛客输入输出题单题解题目 题目链接 题目主要信息:求n对括号的全部合法组合,左右括号之间任意组合,只要合法就行 需要输出所有的结果举一反三: 学习完本题的思路你可以解决如下题目: BM55. 没有重复项数字的全排列 BM56. 有重复项数字的全排列 BM58. 字符串的排列 方法:递…

牛客题解 | 按之字形顺序打印二叉树

牛客输入输出题单题解题目 题目链接 题目的主要信息:给定一个二叉树,返回该二叉树的之字形层序遍 第一层从左向右,下一层从右向左,一直这样交替举一反三: 学习完本题的思路你可以解决如下题目: JZ32. 从上往下打印二叉树 JZ78. 把二叉树打印成多行 方法一:非递归层次遍历…