【LFU缓存机制】+双哈希表解法+排序解法

news2024/11/25 1:38:44

文章目录

  • Tag
  • 题目来源
  • 题目解读
  • 解题思路
    • 方法一:排序解法
    • 方法二:双哈希表
  • 知识回顾
    • 双向链表的操作
  • 写在最后

Tag

【LFU缓存】【哈希表】【设计数据结构】【2023-09-25】


题目来源

460. LFU 缓存


题目解读

为 LFU 缓存算法设计并实现数据结构。

LRU 缓存算法是一种页面缓存置换算法,有两个基本的操作:getput

  • get:通过 key 获得相应的页面缓存。如果获取的 key 存在于缓存中,则返回键的值,否则返回 -1
  • put:向缓存中更新或加入缓存页面。如果 key 已经存在,则变更其对应的缓存值;如果键不存在,就插入新的缓存页面的键值对。 当缓存达到了容量 capacity 时,应该在插入新的缓存页面之前删除最近不经常使用的项。这个最近不经常使用的项指的是使用频次最小,如果遇到使用频次相同的键,则去除最久未使用的键(最近一次使用的时间小的)。

本题要求实现 LFUCache 类:

  • LFUCache(int capacity):用数据结构的容量 capacity 初始化对象;
  • int get(int key):通过 key 获得相应的页面缓存;
  • void put(int key, int value):向缓存中更新或加入缓存页面。

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


解题思路

方法一:排序解法

基础数据结构的设计与选择

get 比较容易实现,使用一个哈希表存放键,值为对应的缓存数据结构。缓存数据结构如何定义呢?因为在插入新的键时可能需要移除最近不经常使用的键,因此缓存的数据结构需要包括:

  • cnt:统计缓存的使用次数;
  • time:缓存的最近一次使用时间;

因为还会牵涉到缓存数据内容的更新,所以需要表示缓存数据内容的 value。并且需要一个 key 变量,用来表示当前缓存内容的键。
为了统计最近不经常使用的键,需要对 < 运算符进行重载,于是缓存数据结构为:

struct Node {
    int cnt, time, key, value;

    Node(int _cnt, int _time, int _key, int _value):cnt(_cnt), time(_time), key(_key), value(_value){}
    
    bool operator < (const Node& rhs) const {
        return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;
    }
};

于是使用的基本数据包括:

  • 哈希表:键为 key,值为 Node (自己设计的数据结构),我们声明为 key_table
  • 集合:保存数据 Node,方便更新缓存内容的使用频次、时间戳以及具体内容,我们声明为 S

LFUCache 实现

LFUCache(int _capacity) {
	capacity = _capacity;  // 缓存容量
	time = 0;                    // 时间戳
	key_table.clear();
	S.clear();
}

get 实现

如果 capacity = 0,表示现在没有任何缓存数据,直接返回 -1

否则,再判断哈希表中是否有 key,如果没有,直接返回 -1。如果有,则表示找到了 key 对应的缓存,我们首先要将删除缓存,更新该缓存的使用频次和最近一次使用时间,然后将更新后的缓存存入哈希表和集合。

put 实现

如果,此时 capacity = 0,不需要进行任何操作,因为最大缓存容量为空。

否则,表示最大缓存容量非空,可以继续更新或者加入新的缓存:

  • 如果哈希表中没有 key,则需要向缓存中加入新的缓存数据,但是需要先判断当前的缓存容量是否已经达到最大的缓存容量:

    • 如果达到了,需要删除最近不经常使用的缓存即删除集合中的第一个缓存,并更新哈希表;
    • 如果未达到,则新建缓存并加入哈希表和集合。
  • 如果哈希表中有 key,则执行修改缓存内容的操作:

    • 获得缓存内容,并删除集合中的该缓存;
    • 更新缓存的使用频次、时间戳和值;
    • 使用新的缓存更新哈希表并加入到集合中。

实现代码

struct Node {
    int cnt, time, key, value;

    Node(int _cnt, int _time, int _key, int _value):cnt(_cnt), time(_time), key(_key), value(_value){}
    
    bool operator < (const Node& rhs) const {
        return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;
    }
};
class LFUCache {
    // 缓存容量,时间戳
    int capacity, time;
    unordered_map<int, Node> key_table;
    set<Node> S;
public:
    LFUCache(int _capacity) {
        capacity = _capacity;
        time = 0;
        key_table.clear();
        S.clear();
    }
    
    int get(int key) {
        if (capacity == 0) return -1;
        auto it = key_table.find(key);
        // 如果哈希表中没有键 key,返回 -1
        if (it == key_table.end()) return -1;
        // 从哈希表中得到旧的缓存
        Node cache = it -> second;
        // 从平衡二叉树中删除旧的缓存
        S.erase(cache);
        // 将旧缓存更新
        cache.cnt += 1;
        cache.time = ++time;
        // 将新缓存重新放入哈希表和平衡二叉树中
        S.insert(cache);
        it -> second = cache;
        return cache.value;
    }
    
    void put(int key, int value) {
        if (capacity == 0) return;
        auto it = key_table.find(key);
        if (it == key_table.end()) {
            // 如果到达缓存容量上限
            if (key_table.size() == capacity) {
                // 从哈希表和平衡二叉树中删除最近最少使用的缓存
                key_table.erase(S.begin() -> key);
                S.erase(S.begin());
            }
            // 创建新的缓存
            Node cache = Node(1, ++time, key, value);
            // 将新缓存放入哈希表和平衡二叉树中
            key_table.insert(make_pair(key, cache));
            S.insert(cache);
        }
        else {
            // 这里和 get() 函数类似
            Node cache = it -> second;
            S.erase(cache);
            cache.cnt += 1;
            cache.time = ++time;
            cache.value = value;
            S.insert(cache);
            it -> second = cache;
        }
    }
};

复杂度分析

时间复杂度:get 时间复杂度 O ( l o g ⁡ n ) O(log⁡n) O(logn)put 时间复杂度 O ( l o g ⁡ n ) O(log⁡n) O(logn),操作的时间复杂度瓶颈在于平衡二叉树(集合)的插入删除均需要 O ( l o g ⁡ n ) O(log⁡n) O(logn) 的时间。

空间复杂度: O ( c a p a c i t y ) O(capacity) O(capacity),其中 c a p a c i t y capacity capacityLFU 的缓存容量。哈希表和平衡二叉树(集合)不会存放超过缓存容量的键值对。


方法二:双哈希表

引入

该方法需要首先了解 460. LFU 缓存,可以参考 我的题解。

现在,我们假设所有的缓存内容使用的频次都一样,根据 LFU 缓存 的置换页面规则,接下来就会将最近不使用的缓存内容置换出去,就变成了 LRU 缓存问题。

于是想到维护一个哈希表 freq_table 来存放缓存内容的使用频率和双向链表。
每一个使用频率对应一个双向链表,这个链表里存放的是使用频率为 freq 的所有缓存内容。

接下来就是 LRU 缓存机制问题了,使用一个键为 key 索引,每个索引对应的是缓存节点,我们声明这个数据结构为 key_table

LRU 中的最经常等价于使用频次最高,最近等价于缓存处于双向链表的头部,置换操作移除的是最久最不经常使用的即为使用频次最小且处于双向链表尾部的缓存内容。

这样我们就能利用两个哈希表来使得两个操作的时间复杂度均为 O ( 1 ) O(1) O(1)

基础数据结构的设计与选择

自己设计的数据结构为 DLinkedNode,为对应缓存频次的双向链表。

class DLinkedNode {
public:
    int key, value;
		int freq = 1;  // 使用频次初始为 1
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(int k = 0, int v = 0): key(k), value(v) {};
};

选择现有的数据结构有:

  • 哈希表 key_table:存放键与缓存内容,用来保证 get O ( 1 ) O(1) O(1) 操作;
  • 哈希表 freq_table:存放缓存内容的使用频次和对应频次的使用内容,用来保证 put O ( 1 ) O(1) O(1) 操作。

LFUCache 实现

LFUCache(int _capacity): capacity(_capacity) {
	min_freq = 0;
	key_table.clear();
	freq_table.clear();
}

其中的 min_freq 表示最小的缓存使用频次,在删除缓存内容时会使用到。

get 实现

如果 capacity = 0,表示现在没有任何缓存数据,直接返回 -1

否则,再判断哈希表 key_table 中是否有 key,如果没有,直接返回 -1。如果有,则表示找到了 key 对应的缓存 node。接着,我们首先要将缓存内容从双向链表中删除,通过 remove(node) 完成;然后判断:如果删除这个缓存内容后,freq 对应的双向链表为空了,需要从 freq_table 中移除 freq 并更新 min_freq。最后,更新 node 的使用频次以及在双向链表中的位置(加入到双向链表的头部位置)。

在双向链表中,移除一个节点属于基本操作了,直接贴上代码:

// 删除双向链表中一个节点
void remove(DLinkedNode* node) {
	node->prev->next = node->next;
	node->next->prev = node->prev;
}

put 实现

如果,此时 capacity = 0,不需要进行任何操作,因为最大缓存容量为空。

否则,表示最大缓存容量非空,可以继续更新或者加入新的缓存:

  • 如果哈希表 key_table 中没有 key,则需要向使用频次为 1 的双向链表中加入新的缓存数据(通过 push_front() 实现),但是需要先判断当前的缓存容量是否已经达到最大的缓存容量:

    • 如果达到了,需要删除最近不经常使用的缓存即 freq_table[min_freq] 表示的双向链表中头部尾部节点,如果移除后的双向链表空了,还要移除 min_freq 这个键值对;
    • 如果未达到,则新建缓存并加入 key_tablefreq_table
  • 此时更新 min_freq = 1

  • 如果哈希表中有 key,则执行修改缓存内容的操作:

    • 删除节点,删除后双向链表空了,要从 freq_table 中移除 freq
    • 更新缓存的使用频次、时间戳和值;
    • 使用新的缓存更新哈希表 key_tablefreq_table

实现代码

class DLinkedNode {
public:
    int key, value, freq = 1;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(int k = 0, int v = 0): key(k), value(v) {};
};

class LFUCache {
private:
    int min_freq, capacity;
    unordered_map<int, DLinkedNode*> key_table;     // key 和双向链表映射
    unordered_map<int, DLinkedNode*> freq_table;    // 使用频次和双向链表映射


    DLinkedNode* new_list() {
        auto dummy = new DLinkedNode();
        dummy->prev = dummy;
        dummy->next = dummy;
        return dummy;
    }

    // 在链表头部增加一个节点
    void push_front(int freq, DLinkedNode* node) {
        auto it = freq_table.find(freq);
        if (it == freq_table.end()) {   // 没有 freq 对应的双向链表
            it = freq_table.emplace(freq, new_list()).first;    // 增加一个空的双向链表
        }
        auto dummy = it->second;
        node->prev = dummy;
        node->next = dummy->next;
        node->prev->next = node;
        node->next->prev = node;
    }

    // 删除双向链表中一个节点
    void remove(DLinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

public:
    LFUCache(int _capacity): capacity(_capacity) {
        min_freq = 0;
        key_table.clear();
        freq_table.clear();
    }
    
    int get(int key) {
        if (capacity == 0) return -1;
        auto it = key_table.find(key);
        if (it == key_table.end()) return -1;

        auto node = it->second;
        // 删除节点,删除后双向链表空了,要从 freq_table 中移除 freq
        remove(node);
        auto dummy = freq_table[node->freq];
        if (dummy->prev == dummy) {
            freq_table.erase(node->freq);
            delete dummy;
            if (min_freq == node->freq) {   // 当前的频次是最少使用的
                ++min_freq;
            }
        }
        // 更新这个节点的使用频次两个哈希表都要更新,node只是使用频次变了,内容不变,所以可以不更新到key_table
        push_front(++node->freq, node);
        return node->value;
    }
    
    void put(int key, int value) {
        if (capacity == 0) return;
        auto it = key_table.find(key);
        if (it == key_table.end()) {
            if (key_table.size() == capacity) {     // 缓存的最大容量已经满了
                auto dummy = freq_table[min_freq];  // 双向链表的头部节点
                auto back_node = dummy->prev;       // 双向链表的尾部节点
                key_table.erase(back_node->key);    // 移除使用频次最小的节点
                remove(back_node);
                delete back_node;
                if (dummy->prev == dummy) {   // 移除后空了
                    freq_table.erase(min_freq);
                    delete dummy;
                }
            }
            // 放入新书
            auto node = new DLinkedNode(key, value);
            key_table[key] = node;
            push_front(1, node);
            min_freq = 1;
        } 
        else {
            // 直接修改值
            auto node = it->second;
            // 删除节点,删除后双向链表空了,要从 freq_table 中移除 freq
            remove(node);
            auto dummy = freq_table[node->freq];
            if (dummy->prev == dummy) {
                freq_table.erase(node->freq);
                delete dummy;
                if (min_freq == node->freq) {   // 当前的频次是最少使用的
                    ++min_freq;
                }
            }
            node->value = value;
            push_front(++node->freq, node);
            key_table[key] = node;
        }
    }
};

/**
 * Your LFUCache object will be instantiated and called as such:
 * LFUCache* obj = new LFUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

复杂度分析

时间复杂度: O ( 1 ) O(1) O(1)

空间复杂度: O ( m i n ( p , c a p a c i t y ) ) O(min(p,capacity)) O(min(p,capacity)),其中 p p p p u t put put 的调用次数。


知识回顾

双向链表的操作

不同于 我的题解 知识回顾中有两个伪节点的双向链表操作,现在来讲述一下代码中只使用一个伪节点的双向链表操作。其实明白了原理之后,会发现一个伪节点(实际上是两个相同的伪头部)与两个伪节点的双向链表操作大同小异。

初始化

DLinkedNode* new_list() {
	auto dummy = new DLinkedNode();
	dummy->prev = dummy;
	dummy->next = dummy;
	return dummy;
}

不同于有伪头部和伪尾部的构造方法,以上代码中构造的是一个伪节点——伪头部(伪尾部)。

node 头插法

// 在链表头部增加一个节点
void push_front(int freq, DLinkedNode* node) {
	auto it = freq_table.find(freq);
	if (it == freq_table.end()) {   // 没有 freq 对应的双向链表
		it = freq_table.emplace(freq, new_list()).first;    // 增加一个空的双向链表
	}
	auto dummy = it->second;
	node->prev = dummy;
	node->next = dummy->next;
	node->prev->next = node;
	node->next->prev = node;
}

删除 node

// 删除双向链表中一个节点
void remove(DLinkedNode* node) {
	node->prev->next = node->next;
	node->next->prev = node->prev;
}

判空

使用一个伪节点的双向链表在判空上,只要判断 dummy->prev == dummy 即可。

Tips

当然也可以使用两个伪节点来实现本题的代码。


写在最后

如果文章内容有任何错误或者您对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。

如果大家有更优的时间、空间复杂度方法,欢迎评论区交流。

最后,感谢您的阅读,如果感到有所收获的话可以给博主点一个 👍 哦。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

k8s集群安装v1.20.9后-2-改造部署自己的服务k8sApp,增加istio

1.环境准备: K8s集群,已经实现了k8s-app小例子,可以正常访问。(已包含docker) 在此基础上对项目进行改进,实现istio流量切换。 2.安装部署istio 2.1 安装go 官网下载go和istio的安装包,上传到k8s集群虚拟机上(三个机器都安装) [root@node1 ~]# cd /root/Public/i…

CSS实现围绕按钮边框转圈的光线效果

CSS实现围绕按钮边框转圈的光线效果&#xff0c;可以自由改变按钮的光线渐变颜色和按钮边框颜色&#xff0c;背景色等。 效果图&#xff1a; 实现完整代码&#xff1a; <template><view class"btnBlock"><view class"btnBian"></vi…

MySQL MHA 高可用

目录 1 MySQL MHA 1.1 什么是 MHA 1.2 MHA 的组成 1.3 MHA 的特点 2 搭建 MySQL MHA 2.1 Master、Slave1、Slave2 节点上安装 mysql5.7 2.2 修改 Master、Slave1、Slave2 节点的主机名 2.3 修改 Master、Slave1、Slave2 节点的 Mysql主配置文件/etc/my.cnf 2.4 在 Mast…

现代架构设计:构建可伸缩、高性能的分布式系统

文章目录 第1节&#xff1a;引言第2节&#xff1a;架构设计的关键原则2.1 微服务架构2.2 异步通信2.3 数据分区和复制2.4 负载均衡 第3节&#xff1a;代码示例3.1 创建产品服务3.2 创建消息队列3.3 创建产品更新服务 第4节&#xff1a;性能优化和监控4.1 建立性能基准4.2 水平扩…

国内大语言模型的相对比较:ChatGLM2-6B、BAICHUAN2-7B、通义千问-6B、ChatGPT3.5

一、 前言 国产大模型有很多&#xff0c;比如文心一言、通义千问、星火、MOSS 和 ChatGLM 等等&#xff0c;但现在明确可以部署在本地并且开放 api 的只有 MOOS 和 ChatGLM。MOOS 由于需要的 GPU 显存过大&#xff08;不量化的情况下需要80GB&#xff0c;多轮对话还是会爆显存…

Spring整合RabbitMQ——生产者(利用配置类)

1.生产者配置步骤 2.引入依赖 3.编写配置 配置RabbitMQ的基本信息&#xff0c;用来创建连接工厂的 编写启动类 编写配置类 4. 编写测试类

C#(CSharp)入门教程

目录 C#的第一个程序 变量 折叠代码 变量类型和声明变量 获取变量类型所占内存空间&#xff08;sizeof&#xff09; 常量 转义字符 隐式转换 显示转换 异常捕获 运算符 算术运算符 布尔逻辑运算符 关系运算符 位运算符 其他运算符 字符串拼接 …

unity lua开发体系搭建

在前面的文章里面我们已经介绍了怎么样在unity里面配置lua的开发环境&#xff0c;我们可以通过C#代码装载lua的脚本并执行相应的处理&#xff0c;这次我们一步步搭建下lua的开发体系。 1.基于c#体系所有的类都继承MonoBehaviour在这里lua环境下我们也需要创建一个类似于这个类的…

Stm32_标准库_呼吸灯_按键控制

Stm32按键和输出差不多 PA1为LED供给正电&#xff0c;PB5放置按键&#xff0c;按键一端接PB5,另一端接负极 void Key_Init(void){RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //APB2总线连接着GPIOBGPIO_InitStructur.GPIO_Mode GPIO_Mode_IPU;GPIO_InitStructur.…

@vitejs/plugin-legacy 为你的 Vite 项目提供对旧版浏览器的支持

vitejs/plugin-legacy 是 Vite 生态系统中的一个插件&#xff0c;它的作用是为你的 Vite 项目提供对旧版浏览器的支持。 具体而言&#xff0c;该插件会根据你在项目配置中指定的目标浏览器列表&#xff08;通过 browserslist 字段&#xff09;&#xff0c;自动生成兼容旧版浏览…

FPGA 图像缩放 千兆网 UDP 网络视频传输,基于RTL8211 PHY实现,提供工程和QT上位机源码加技术支持

目录 1、前言版本更新说明免责声明 2、相关方案推荐UDP视频传输--无缩放FPGA图像缩放方案我这里已有的以太网方案 3、设计思路框架视频源选择ADV7611 解码芯片配置及采集动态彩条跨时钟FIFO图像缩放模块详解设计框图代码框图2种插值算法的整合与选择 UDP协议栈UDP视频数据组包U…

面试题08.05.递归算法

递归乘法。 写一个递归函数&#xff0c;不使用 * 运算符&#xff0c; 实现两个正整数的相乘。可以使用加号、减号、位移&#xff0c;但要吝啬一些。 示例1: 输入&#xff1a;A 1, B 10输出&#xff1a;10示例2: 输入&#xff1a;A 3, B 4输出&#xff1a;12提示: 保证乘法…

nodejs+vue 大学生就业管理系统

随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;学生就业管理系统也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&#xff0c;人工管理显然已无法应对时代的变化&#xff0c;而…

从MVC到DDD,该如何下手重构?

作者&#xff1a;付政委 博客&#xff1a;bugstack.cn 沉淀、分享、成长&#xff0c;让自己和他人都能有所收获&#xff01;&#x1f604; 大家好&#xff0c;我是技术UP主小傅哥。多年的 DDD 应用&#xff0c;使我开了技术的眼界&#xff01; MVC 旧工程腐化严重&#xff0c;…

云HIS 医院综合运营管理系统源码

医院管理信息系统&#xff08;HIS&#xff09;是医院基本、重要的管理系统&#xff0c;是医院大数据的基础。 基于云计算的云医疗信息系统&#xff08;云HIS&#xff09;。以SaaS的方式提供服务&#xff0c;系统遵循服务化、模块化原则开发&#xff0c;具有强大的可扩展性&…

vue实现移动端悬浮可拖拽按钮

需求&#xff1a; 按钮在页面侧边悬浮显示&#xff1b;点击按钮可展开多个快捷方式按钮&#xff0c;从下向上展开。长按按钮&#xff0c;则允许拖拽来改变按钮位置&#xff0c;按钮为非展开状态&#xff1b;按钮移动结束&#xff0c;手指松开&#xff0c;计算距离左右两侧距离…

喜迎中秋国庆双节,华为云Astro Canvas之我的中秋节设计大屏

目录 前言 前提条件 作品展示 薅羊毛 前言 大屏应用华为云Astro Canvas是华为云低代码平台Astro的子服务之一&#xff0c;是以数据可视化为核心&#xff0c;以屏幕轻松编排&#xff0c;多屏适配可视为基础&#xff0c;用户可通过图形化界面轻松搭建专业水准的数据可视化大屏…

面试:Spring中单例模式用的是哪种?

你好&#xff0c;我是田哥 需要简历优化、模拟面试、面试辅导、技术辅导......请联系我。10年码农24小时在线为你服务。 面试中被问到设计模式的概率还是蛮高的&#xff0c;尤其是问&#xff1a;你在项目中用过设计模式吗&#xff1f; 面对这个问题&#xff0c;我也在做模拟面试…

使用香橙派 在Linux环境中安装并学习Python

前言 在实际项目中&#xff0c;经常会遇到需要使用人工智能的场景&#xff0c;如人脸识别&#xff0c;车牌识别等...其一般的流程就是由单片机采集数据发送给提供人工智能算法模型的公司&#xff08;百度云&#xff0c;阿里云...&#xff09;&#xff0c;然后人工智能将结果回…

使用 Python 函数callable和isinstance的意义

一、说明 在这篇博客中&#xff0c;我们将探讨两个python函数&#xff1a;1 callable 中的函数及其有趣的应用程序。该callable函数用于检查对象是否可调用&#xff0c;这意味着它可以作为函数调用。2 isinstance这个内置函数允许我们比较两种不同的数据类型并确定它们是否相…