LRU、LFU 内存淘汰算法的设计与实现

news2024/11/18 11:40:47

1、背景介绍

LRU、LFU都是内存管理淘汰算法,内存管理是计算机技术中重要的一环,也是多数操作系统中必备的模块。应用场景:假设 给定你一定内存空间,需要你维护一些缓存数据,LRU、LFU就是在内存已经满了的情况下,如果再次添加新的数据,该淘汰哪些数据来留出新数据的内存空间???

2、LRU(least recently used)

LRU(east recently used),即最近最少使用 ,也就是说 在内存满的情况下,将会淘汰很久都没有使用过的数据。例如 leetcode 146题。

需求:现在需要设计一个算法,使得插入新数据、获取已经存入的数据,使得平均时间复杂度O(1)。

设计思路:因为插入、获取数据时,都会更新时间戳,还需要使得平均时间复杂度O(1),可以使用双链表+哈希表的方式来存储。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如上图,哈希表里存储 键与双链表节点,双链表的head表示最近使用过的数据,tail表示很久都没使用过的数据。

  • 插入数据时,在哈希表建立key与Node节点,然后把Node节点插入双链表的头部位置。

  • 获取数据时,从哈希表拿到Node节点,然后把Node节点从双链表中分离出来,插入双链表的头部位置即可。

    在这里插入图片描述

以上两个步骤,时间复杂度都是O(1)。所以符合题意。

代码如下:

// lc146题 直接能过的代码
class LRUCache {
    // LRU,最近最少使用
    // 用双链表节点包装进行连接
    private HashMap<Integer, Node> map; // 哈希表,存储key与双链表节点
    private int maxSize; // 缓存的最大容量
    private Node head, tail; // 头部是最近使用的,尾部 是很久没使用的
    
    private static class Node { // 双链表节点
        int key, val;
        Node left, right;
        public Node(int k, int v) {
            key = k;
            val = v;
        }
    }
    
    public LRUCache(int capacity) {
        map = new HashMap<>();
        maxSize = capacity;
    }
    
    public int get(int key) {
        if(!map.containsKey(key)) return -1;
        Node node = map.get(key);
        if (node == head) return node.val;
        apart(node); //分离出node节点
        node.right = head; // 插入到双链表的头部位置
        head.left = node;
        head = node;
        return node.val;
    }
    
    public void put(int key, int value) {
        if (!map.containsKey(key)) {
            Node node = new Node(key, value);
            node.right = head;
            if (head != null) head.left = node;
            if (tail == null) tail = node;
            head = node;
            map.put(key, node);
            if (map.size() > maxSize) { // 容量超了,删除双链表的尾部元素
                if (tail.left != null) {
                    tail.left.right = null;
                }
                map.remove(tail.key);
                Node pre = tail.left;
                tail.left = null;
                tail = pre;
            }
        } else {
            Node node = map.get(key);
            node.val = value;
            get(key); // 调用get,使其向前移动
        }
    }

    // node的左右两边连接,将node抽离出
    private void apart(Node node) {
        node.left.right = node.right;
        if (node.right != null) {
            node.right.left = node.left;
        }
        if (node == tail) {
            tail = node.left;
        }
        node.left = node.right = null;
    }
}

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

3、LFU(least frequancy used)

LFU(least frequancy used), 即不常用算法,按照每个数据的访问次数来判断数据的使用情况。如果一个数据在近一段时间内没有被访问或者被访问的可能性小,则会被淘汰。(简单点说,就是按照“使用频率”来分级的)例题:leetcode 460题

需求:现在需要设计一个算法,使得插入、获取数据的平均时间复杂度O(1)。

设计思路:按照使用频率进行划分,相同频率的数据放在同一个“桶”内,从左往右频率逐渐升高;而桶内部是从上往下,按照插入桶内的时间来排序,新插入的节点在桶的顶部,很久之前插入的节点在桶的底部,如下图所示:

在这里插入图片描述

注意:当内存满了的时候,会删除 频率最低的桶内,最后的一个数据节点

代码实现如下:

public class LFUCache {
	public LFUCache(int K) {
		capacity = K;
		size = 0;
		records = new HashMap<>();
		heads = new HashMap<>();
		headList = null;
	}

	private int capacity; // 缓存的大小限制,即K
	private int size; // 缓存目前有多少个节点
	private HashMap<Integer, Node> records;// 表示key(Integer)由哪个节点(Node)代表
	private HashMap<Node, NodeList> heads; // 表示节点(Node)在哪个桶(NodeList)里
	private NodeList headList; // 整个结构中位于最左的桶

	// 节点的数据结构
	public static class Node {
		public Integer key;
		public Integer value;
		public Integer times; // 这个节点发生get或者set的次数总和
		public Node up; // 节点之间是双向链表所以有上一个节点
		public Node down;// 节点之间是双向链表所以有下一个节点

		public Node(int k, int v, int t) {
			key = k;
			value = v;
			times = t;
		}
	}

	// 桶结构
	public static class NodeList {
		public Node head; // 桶的头节点
		public Node tail; // 桶的尾节点
		public NodeList last; // 桶之间是双向链表所以有前一个桶
		public NodeList next; // 桶之间是双向链表所以有后一个桶

		public NodeList(Node node) {
			head = node;
			tail = node;
		}

		// 把一个新的节点加入这个桶,新的节点都放在顶端变成新的头部
		public void addNodeFromHead(Node newHead) {
			newHead.down = head;
			head.up = newHead;
			head = newHead;
		}

		// 判断这个桶是不是空的
		public boolean isEmpty() {
			return head == null;
		}

		// 删除node节点并保证node的上下环境重新连接
		public void deleteNode(Node node) {
			if (head == tail) {
				head = null;
				tail = null;
			} else {
				if (node == head) {
					head = node.down;
					head.up = null;
				} else if (node == tail) {
					tail = node.up;
					tail.down = null;
				} else {
					node.up.down = node.down;
					node.down.up = node.up;
				}
			}
			node.up = null;
			node.down = null;
		}
	}

	// removeNodeList:刚刚减少了一个节点的桶
	// 这个函数的功能是,判断刚刚减少了一个节点的桶是不是已经空了。
	// 1)如果不空,什么也不做
	//
	// 2)如果空了,removeNodeList还是整个缓存结构最左的桶(headList)。
	// 删掉这个桶的同时也要让最左的桶变成removeNodeList的下一个。
	//
	// 3)如果空了,removeNodeList不是整个缓存结构最左的桶(headList)。
	// 把这个桶删除,并保证上一个的桶和下一个桶之间还是双向链表的连接方式
	//
	// 函数的返回值表示刚刚减少了一个节点的桶是不是已经空了,空了返回true;不空返回false
	private boolean modifyHeadList(NodeList removeNodeList) {
		if (removeNodeList.isEmpty()) {
			if (headList == removeNodeList) {
				headList = removeNodeList.next;
				if (headList != null) {
					headList.last = null;
				}
			} else {
				removeNodeList.last.next = removeNodeList.next;
				if (removeNodeList.next != null) {
					removeNodeList.next.last = removeNodeList.last;
				}
			}
			return true;
		}
		return false;
	}

	// 函数的功能
	// node这个节点的次数+1了,这个节点原来在oldNodeList里。
	// 把node从oldNodeList删掉,然后放到次数+1的桶中
	// 整个过程既要保证桶之间仍然是双向链表,也要保证节点之间仍然是双向链表
	private void move(Node node, NodeList oldNodeList) {
		oldNodeList.deleteNode(node);
		// preList表示次数+1的桶的前一个桶是谁
		// 如果oldNodeList删掉node之后还有节点,oldNodeList就是次数+1的桶的前一个桶
		// 如果oldNodeList删掉node之后空了,oldNodeList是需要删除的,所以次数+1的桶的前一个桶,是oldNodeList的前一个
		NodeList preList = modifyHeadList(oldNodeList) ? oldNodeList.last : oldNodeList;
		// nextList表示次数+1的桶的后一个桶是谁
		NodeList nextList = oldNodeList.next;
		if (nextList == null) {
			NodeList newList = new NodeList(node);
			if (preList != null) {
				preList.next = newList;
			}
			newList.last = preList;
			if (headList == null) {
				headList = newList;
			}
			heads.put(node, newList);
		} else {
			if (nextList.head.times.equals(node.times)) {
				nextList.addNodeFromHead(node);
				heads.put(node, nextList);
			} else {
				NodeList newList = new NodeList(node);
				if (preList != null) {
					preList.next = newList;
				}
				newList.last = preList;
				newList.next = nextList;
				nextList.last = newList;
				if (headList == nextList) {
					headList = newList;
				}
				heads.put(node, newList);
			}
		}
	}

	public void put(int key, int value) {
		if (capacity == 0) {
			return;
		}
		if (records.containsKey(key)) {
			Node node = records.get(key);
			node.value = value;
			node.times++;
			NodeList curNodeList = heads.get(node);
			move(node, curNodeList);
		} else {
			if (size == capacity) {
				Node node = headList.tail;
				headList.deleteNode(node);
				modifyHeadList(headList);
				records.remove(node.key);
				heads.remove(node);
				size--;
			}
			Node node = new Node(key, value, 1);
			if (headList == null) {
				headList = new NodeList(node);
			} else {
				if (headList.head.times.equals(node.times)) {
					headList.addNodeFromHead(node);
				} else {
					NodeList newList = new NodeList(node);
					newList.next = headList;
					headList.last = newList;
					headList = newList;
				}
			}
			records.put(key, node);
			heads.put(node, headList);
			size++;
		}
	}

	public int get(int key) {
		if (!records.containsKey(key)) {
			return -1;
		}
		Node node = records.get(key);
		node.times++;
		NodeList curNodeList = heads.get(node);
		move(node, curNodeList);
		return node.value;
	}
}

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

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

相关文章

go语言 rune 类型

ASCII 码只需要 7 bit 就能完整地表示&#xff0c;但只能表示英文字母在内的 128 个字符&#xff0c;为了表示世界上大部分的文字系统&#xff0c;发明了 Unicode &#xff0c;它是 ASCII 的超集&#xff0c;包含世界上书写系统中存在的所有字符&#xff0c;并且为每个代码分配…

排队工会模式:电商营销的新趋势,让你的平台月流水过亿

排队工会模式是一种新型的电商营销模式&#xff0c;它利用产品利润分红的方式来吸引用户购买和推广&#xff0c;从而实现平台的流量和销量的增长。这种模式的核心是建立一个分红池&#xff0c;平台从每个产品的利润中拿出一定比例来充值分红池&#xff0c;然后按照用户的购买顺…

【yolov5】原理

Focus操作 anchors 先验框 其它 Yolov5的模型主要由Backbone、Neck和Head三部分组成。 Backbone&#xff1a;负责提取输入图像的特征。在Yolov5中&#xff0c;常见的Backbone网络包括CSPDarknet53或ResNet。这些网络都是相对轻量级的&#xff0c;能够在保证较高检测精度的同…

前端项目练习(练习-005-webpack-03)

学习前&#xff0c;首先&#xff0c;创建一个web-005项目&#xff0c;内容和web-004一样。&#xff08;注意将package.json中的name改为web-005&#xff09; 前面的代码中&#xff0c;打包工作已经基本完成了&#xff0c;下面开始在本地启动项目。这里需要用到webpack-dev-serv…

如何通过Gunicorn和Niginx部署Django

本文主要介绍如何配置Niginx加载Django的静态资源文件&#xff0c;也就是Static 1、首先需要将Django项目中的Settings.py 文件中的两个参数做以下设置&#xff1a; STATIC_URL /static/ STATIC_ROOT os.path.join(BASE_DIR, static) 然后在宝塔面板中执行python manage.…

Simulink仿真模块 - Digital Clock

Digital Clock:以指定的采样间隔输出仿真时间 在仿真库中的位置为:Simulink / Sources 模型为: 说明 Digital Clock 模块仅以指定的采样间隔输出仿真时间。在其他时间,此模块保留输出的上一个值。要控制此模块的精度,请使用模块对话框中的 Sample time 参数。 当需要离散系…

S09-录入的数据快速分列

选中某一列数据&#xff0c;数据-》分列 确定分隔符

孜然单授权系统V1.0[免费使用]

您还在为授权系统用哪家而发愁&#xff1f;孜然单授权系统为您解决苦恼&#xff0c;本系统永久免费。 是的&#xff0c;还是那个孜然&#xff0c;消失了一年不是跑路了是没有空&#xff0c;但是这些都是无关紧要的&#xff0c;为大家带来的孜然单授权系统至上我最高的诚意&…

论文研究有哪些方法?

在写论文的的时候&#xff0c;选择合适的研究方法至关重要。好的研究方法会增加你论文的可信度和更具有科学性&#xff0c;为你的研究成果增添色彩。下面将介绍几种常用的研究方法&#xff0c;供大家参考学习。 1.文献综述法 多用于理论研究类论文写作。文献综述法是对某一领域…

微信小程序:uniapp解决上传小程序体积过大的问题

概述 在昨天的工作中遇到了一个微信小程序上传代码过大的情况&#xff0c;在这里总结一下具体的解决步骤&#xff0c;首先介绍一下&#xff0c;技术栈是使用uniapp框架HBuilderX的开发环境。 错误提示 真机调试&#xff0c;提示包提交过大&#xff0c;不能正常生成二维码&…

为什么PDF打开没有密码,但是不能编辑?

PDF文件在PDF编辑器中打开之后应该是可以直接编辑了的&#xff0c;打开PDF文件的时候没有提出要输入密码&#xff0c;可是进入文件后&#xff0c;不能编辑&#xff0c;比如下图&#xff1a; 提示文件受到限制&#xff0c;无法编辑。这就是因为设置了限制编辑。我们将限制取消就…

web前端项目案例实战

之前也有使用vite2vue3electronc创建桌面端项目&#xff0c;不过 vue-cli-plugin-electron-builder 脚手架插件构建的项目electron版本只有13.x。如今electron版本都到了24&#xff0c;显然不能再用之前的方法创建项目了。于是闲暇时间就捣鼓了electron24vite4搭建桌面程序&…

第5讲:v-if与v-show的使用方法及区别

v-if条件判断 v-if是条件渲染指令&#xff0c;它根据表达式的真假来删除和插入元素&#xff0c;它的基本语法如下&#xff1a; v-if “expression” expression是一个返回bool值的表达式&#xff0c;表达式可以是一个bool属性&#xff0c;也可以是一个返回bool的运算式 &#…

接口自动化测试思路和实战(3):测试库框架

目录 测试库框架 步骤1、在common文件夹下新建common_api_info.py文件&#xff0c;把所有的api接口做个封装 步骤2、修改common_function.py文件 步骤3、修改test_get_access_token_api.py文件 步骤4、修改test_create_user_tag_api.py文件代码&#xff1b; 步骤5、再执行…

怒刷LeetCode的第15天(Java版)

目录 第一题 题目来源 题目内容 解决方法 方法一&#xff1a;哈希表双向链表 方法二&#xff1a;TreeMap 方法三&#xff1a;双哈希表 第二题 题目来源 题目内容 解决方法 方法一&#xff1a;二分查找 方法二&#xff1a;线性搜索 方法三&#xff1a;Arrays类的b…

JAVA学习-全网最详细

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

1960-2017年世界各国总和生育率数据

1960-2017年世界各国总和生育率数据 1、时间&#xff1a;1960-2017年 2、指标&#xff1a;生育率 3、范围&#xff1a;全球各国 4、来源&#xff1a;世界银行 5、指标解释&#xff1a; 总生育率表示假设妇女度过整个生育期并按照当期的年龄别生育率生育孩子所生育的孩子数…

第一次课进行分类代码

System32下的进程 #include <windows.h> #include <stdio.h> #include<TlHelp32.h> #include<psapi.h>int main() {HANDLE hProcessSnap;PROCESSENTRY32 pe32;// 获取进程快照hProcessSnap CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if (hPr…

微信编辑器自带导出功能,同步到公众号生成链接

​在微信编辑器里编辑好了文章&#xff0c;想把编辑器里的文章复制到其它网站或者分享给好友&#xff0c;怎么操作呢&#xff1f;其实很简单&#xff0c;可以通过编辑器自带的导出功能&#xff0c;或者同步到微信公众号生成链接&#xff0c;那今天小编先给大家说一说编辑器自带…

python setup.py egg_info“ failed with error code 1 in xxxxxxxx问题解决

python setup.py egg_info" failed with error code 1 in xxxxxxxx问题解决 一、问题描述&#xff1a;通过pip安装opencv-python时候&#xff0c;提示安装二、解决办法&#xff1a;升级pip三、结果 一、问题描述&#xff1a;通过pip安装opencv-python时候&#xff0c;提示…