Java数据结构与算法——手撕LRULFU算法

news2024/10/2 20:38:42

LRU算法

力扣146:https://leetcode-cn.com/problems/lru-cache/

讲解视频:https://www.bilibili.com/video/BV1Hy4y1B78T?p=65&vd_source=6f347f8ae76e7f507cf6d661537966e8

LRU是Least Recently Used的缩写,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。(操作系统)

分析:

1 所谓缓存,必须要有读+写两个操作,按照命中率的思路考虑,写操作+读操作时间复杂度都需要为O(1)

2 特性要求分析
2.1 必须有顺序之分,以区分最近使用的和很久没用到的数据排序。
2.2 写和读操作 一次搞定。
2.3 如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头)

查找快,插入快,删除快,且还需要先后排序-------->什么样的数据结构满足这个问题?

你是否可以在O(1)时间复杂度内完成这两种操作?

如果一次就可以找到,你觉得什么数据结构最合适??

LRU的算法核心是哈希链表,本质就是HashMap+DoubleLinkedList 时间复杂度是O(1),哈希表+双向链表的结合体

利用JDK的LinkedHashMap实现:

在这里插入图片描述

LRU(The Least Recently Used,最近最久未使用算法)是一种常见的缓存算法,在很多分布式缓存系统(如Redis, Memcached)中都有广泛使用。

LRU算法的思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)

LRU算法的描述: 设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:

  1. set(key,value):将记录(key,value)插入该结构。当缓存满时,将最久未使用的数据置换掉。
  2. get(key):返回key对应的value值。

实现:最朴素的思想就是用数组+时间戳的方式,不过这样做效率较低。因此,我们可以用双向链表(LinkedList)+哈希表(HashMap)实现(链表用来表示位置,哈希表用来存储和查找),在Java里有对应的数据结构LinkedHashMap

利用JavaLinkedHashMap用非常简单的代码来实现基于LRU算法的Cache功能,代码如下:

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Title:力扣146 - LRU 缓存机制
 * Description:最近最久未使用算法
 *              双向链表+Hash实现,LinkedHashMap
 * @author WZQ
 * @version 1.0.0
 * @date 2020/12/24
 */
public class LRUCache{

     // 思路1 使用LinkedHashMap jdk自带
    public LinkedHashMap<Integer, Integer> map;

    public LRUCache(int capacity) {
        // true表示纪录访问的顺序,false的话,按第一次插入的顺序不变
        map = new LinkedHashMap(capacity, 0.75f, true){
            // 最近最久未使用删除
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return this.size() > capacity;
            }
        };
    }

    public int get(int key) {
        return map.get(key) == null ? -1 : map.get(key);
    }

    public void put(int key, int value) {
        map.put(key, value);
    }

    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(3);

        lruCache.put(1,"a");
        lruCache.put(2,"b");
        lruCache.put(3,"c");
        System.out.println(lruCache.keySet());

        lruCache.put(4,"d");
        System.out.println(lruCache.keySet());

        lruCache.put(3,"c");
        System.out.println(lruCache.keySet());
        lruCache.put(3,"c");
        System.out.println(lruCache.keySet());
        lruCache.put(3,"c");
        System.out.println(lruCache.keySet());
        lruCache.put(5,"x");
        System.out.println(lruCache.keySet());
    }

}

/**
 * true
 * [1, 2, 3]
 * [2, 3, 4]
 * [2, 4, 3]
 * [2, 4, 3]
 * [2, 4, 3]
 * [4, 3, 5]
 * false
 * [1, 2, 3]
 * [2, 3, 4]
 * [2, 3, 4]
 * [2, 3, 4]
 * [2, 3, 4]
 * [3, 4, 5]
 */

手写LRU:

import java.util.HashMap;
import java.util.Map;

/**
 * Title:146. LRU 缓存
 * Description:LRU
 * @author WZQ
 * @version 1.0.0
 * @date 2023/2/26
 */
class LRUCache {

    /**
     * 数据结点
     * @param <K>
     * @param <V>
     */
    class Node<K, V>{
        K key;
        V value;
        Node<K, V> prev;
        Node<K, V> next;

        public Node() {
            this.prev = this.next = null;
        }

        public Node(K key, V value){
            this.prev = this.next = null;
            this.key = key;
            this.value = value;
        }
    }

    /**
     * 双端链表
     * @param <K>
     * @param <V>
     */
    class DoubleLinkedList<K, V>{
        Node<K, V> head;
        Node<K, V> tail;

        public DoubleLinkedList() {
            // 头结点不删
            head = new Node<>();
            tail = new Node<>();
            head.next = tail;
            tail.prev = head;
        }

        // 头放最久未使用,尾放最新访问

        // 删除节点
        public void removeNode(Node<K, V> node){
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.prev = null;
            node.next = null;
        }

        // 添加到尾
        public void addTail(Node<K, V> node){
            node.prev = tail.prev;
            node.next = tail;
            tail.prev.next = node;
            tail.prev = node;
        }

        // 获取最久未使用节点
        public Node<K, V> getLast() {
            return head.next;
        }
    }

    private int capacity;
    private DoubleLinkedList<Integer, Integer> doubleLinkedList;
    private HashMap<Integer, Node<Integer, Integer>> map;

    // 思路2 手写 双端链表+哈希     时间复杂度: put O(1) get O(1) 
    public LRUCache(int capacity) {
        this.capacity = capacity;
        doubleLinkedList = new DoubleLinkedList();
        map = new HashMap<>();
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        Node<Integer, Integer> node = map.get(key);
        doubleLinkedList.removeNode(node);
        doubleLinkedList.addTail(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)){
            // 已存在节点
            // 删除节点,放到尾部
            Node<Integer, Integer> node = map.get(key);
            node.value = value;
            doubleLinkedList.removeNode(node);
            doubleLinkedList.addTail(node);
        }else {
            // 未存在节点
            if (capacity == map.size()){
                // 缓存数已满,需删除最久未使用
                Node<Integer, Integer> last = doubleLinkedList.getLast();
                doubleLinkedList.removeNode(last);
                map.remove(last.key);
            }
            Node<Integer, Integer> node = new Node<Integer, Integer>(key, value);
            doubleLinkedList.addTail(node);
            map.put(key, node);
        }
    }

    public static void main(String[] args) {
        LRUCache lruCacheDemo = new LRUCache(3);

        lruCacheDemo.put(1, 1);
        lruCacheDemo.put(2, 2);
        lruCacheDemo.put(3, 3);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(4, 1);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(3, 1);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(3, 1);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(3, 1);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(5, 1);
        System.out.println(lruCacheDemo.map.keySet());

    }
}

/**
 [1, 2, 3]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [3, 4, 5]
 */

LFU算法

力扣:https://leetcode.cn/problems/lfu-cache/description/

LFU(Least Frequently Used ,最近最少使用算法)也是一种常见的缓存算法。

顾名思义,LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰如果访问频率相同,则淘汰最久未访问的。

LFU 算法的描述:

设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:

  1. set(key,value):将记录(key,value)插入该结构。当缓存满时,将访问频率最低的数据置换掉。
  2. get(key):返回key对应的value值。

算法实现策略:考虑到 LFU 会淘汰访问频率最小的数据,我们需要一种合适的方法按大小顺序维护数据访问的频率。LFU 算法本质上可以看做是一个 top K 问题(K = 1),即选出频率最小的元素,因此我们很容易想到可以用二项堆来选择频率最小的元素,这样的实现比较高效。最终实现策略为小顶堆+哈希表,时间复杂度O(logn),代码如下:

import java.util.Arrays;
import java.util.HashMap;
import java.util.PriorityQueue;
import java.util.TreeSet;

/**
 * Title:leetcode --> 460. LFU 缓存
 * Description:LFU
 *
 * 方法1:哈希表 + 最小堆/平衡二叉树TreeSet   
 * 时间复杂度:put O(logn) get O(logn) 堆运算
 *
 * @author WZQ
 * @version 1.0.0
 * @date 2023/2/26
 */
class LFUCache {

    PriorityQueue<Node<Integer, Integer>> minHeap;
    HashMap<Integer, Node<Integer, Integer>> map;
    // 访问时间
    int visitTime;
    int capacity;

    /**
     * 数据结点
     * @param <K>
     * @param <V>
     */
    class Node<K, V> implements Comparable<Node>{
        K key;
        V value;
        // 访问次数
        int count;
        // 最新的时间,越小表示越久未访问
        int lastTime;

        public Node(K key, V value){
            this.key = key;
            this.value = value;
            this.count = 1;
        }

        public Node(){
        }

        @Override
        public int compareTo(Node node) {
            // 访问次数一样,则取最久未访问的
            return count == node.count ? lastTime - node.lastTime : count - node.count;
        }
    }

    public LFUCache(int capacity) {
        visitTime = 0;
        this.capacity = capacity;
        map = new HashMap<>();
        minHeap = new PriorityQueue<>();
    }

    public int get(int key) {
        if (!map.containsKey(key)){
            return -1;
        }
        Node<Integer, Integer> node = map.get(key);
        // 删除元素,重新入堆排序
        minHeap.remove(node);
        node.count ++;
        node.lastTime = ++ visitTime;
        minHeap.offer(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)){
            // 访问+1,置成最新
            Node<Integer, Integer> node = map.get(key);
            minHeap.remove(node);
            node.value = value;
            node.count++;
            node.lastTime = ++ visitTime;
            minHeap.offer(node);
        }else {
            // 容量已满,剔除最小元素(最久未访问)
            if (capacity == map.size()){
                Node<Integer, Integer> minNode = minHeap.poll();
                map.remove(minNode.key);
            }
            Node<Integer, Integer> node = new Node<>(key, value);
            node.lastTime = ++ visitTime;
            map.put(key, node);
            minHeap.offer(node);
        }
    }

}

双hash表思路,详细可见leetcode讲解视频:https://leetcode.cn/problems/lfu-cache/solutions/186348/lfuhuan-cun-by-leetcode-solution/

时间复杂度O(1),代码如下:

import java.util.HashMap;
import java.util.Map;

/**
 * Title:leetcode --> 460. LFU 缓存
 * Description:LFU
 *
 * 双Hash表  时间复杂度 O(1)
 *
 * @author WZQ
 * @version 1.0.0
 * @date 2023/2/26
 */
class LFUCache2 {

    /**
     * 数据结点
     * @param <K>
     * @param <V>
     */
    class Node<K, V>{
        K key;
        V value;
        // 访问次数
        int count;
        Node<K, V> prev;
        Node<K, V> next;

        public Node() {
            this.prev = this.next = null;
        }

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

    /**
     * 双端链表
     * @param <K>
     * @param <V>
     */
    class DoubleLinkedList<K, V>{
        Node<K, V> head;
        Node<K, V> tail;
        int size;

        public DoubleLinkedList() {
            // 头结点不删
            head = new Node<>();
            tail = new Node<>();
            head.next = tail;
            tail.prev = head;
            size = 0;
        }

        // 头放最久未使用,尾放最新访问

        // 删除节点
        public void removeNode(Node<K, V> node){
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.prev = null;
            node.next = null;
            size --;
        }

        // 添加到尾
        public void addTail(Node<K, V> node){
            node.prev = tail.prev;
            node.next = tail;
            tail.prev.next = node;
            tail.prev = node;
            size ++;
        }

        // 获取最久未使用节点
        public Node<K, V> getLast() {
            return head.next;
        }
    }

    // key:key, value:Node节点
    Map<Integer, Node<Integer, Integer>> keyTable;
    // key:访问次数, value:访问次数相同的组成链表,头是最久未访问的,新的插到尾部
    Map<Integer, DoubleLinkedList<Integer, Integer>> countTable;
    int capacity;
    int minCount;

    public LFUCache2(int capacity) {
        this.capacity = capacity;
        keyTable = new HashMap<>();
        countTable = new HashMap<>();
    }

    public int get(int key) {
        if (!keyTable.containsKey(key)){
            return -1;
        }
        Node<Integer, Integer> node = keyTable.get(key);
        resetNode(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (keyTable.containsKey(key)){
            // 存在,则改变值,访问次数+1, 重置节点
            Node<Integer, Integer> node = keyTable.get(key);
            resetNode(node);
            node.value = value;
        }else {
            // 容量已满,剔除最少访问节点
            if (capacity == keyTable.size()){
                // 通过minCount拿到最小访问的头节点(最久未访问)
                Node<Integer, Integer> node = countTable.get(minCount).getLast();
                keyTable.remove(node.key);
                countTable.get(minCount).removeNode(node);
                if (countTable.get(minCount).size == 0) {
                    countTable.remove(minCount);
                }
            }
            // 新节点添加
            DoubleLinkedList<Integer,Integer> linkedList = countTable.getOrDefault(1, new DoubleLinkedList());
            Node<Integer, Integer> node = new Node<>(key, value);
            linkedList.addTail(node);
            countTable.put(1, linkedList);
            keyTable.put(key, node);
            minCount = 1;
        }
    }

    /**
     * 访问次数+1,重置节点在countTable的位置
     * @param node
     */
    public void resetNode(Node<Integer, Integer> node){
        // 1. 原位置删除该节点,原位置链表为空,则删除
        int count = node.count;
        countTable.get(count).removeNode(node);
        if (countTable.get(count).size == 0){
            countTable.remove(count);
            if (count == minCount) {
                minCount ++;
            }
        }
        // 2. 访问次数+1
        node.count ++;
        count++;
        // 3. 新位置为空,则创建链表,节点添加进去
        DoubleLinkedList<Integer, Integer> nextLinkedList = countTable.getOrDefault(count, new DoubleLinkedList<>());
        nextLinkedList.addTail(node);
        countTable.put(count, nextLinkedList);
    }

}

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

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

相关文章

Typora图床配置:Typora + PicGo + 阿里云OSS

文章目录一、前景提要二、相关链接三、搭建步骤1. 购买阿里云对象存储OSS2. 对象存储OSS&#xff1a;创建Bucket3. 阿里云&#xff1a;添加OSS访问用户及权限4. 安装Typora5. 配置PicGo方法一&#xff1a;使用PicGo-Core (Command line)方法二&#xff1a;使用PicGo(app)6. 最后…

C语言深度剖析:关键字

C语言深度剖析:关键字C语言深度剖析:关键字前言定义与声明&#xff08;补充内容&#xff09;最宏大的关键字-auto最快的关键字-register关键字static被冤枉的关键字-sizeof整型在内存中的存储原码、反码、补码大小端补充理解变量内容的存储和取出为什么都是补码整型取值范围关于…

多线程的初识和创建

✨个人主页&#xff1a;bit me&#x1f447; ✨当前专栏&#xff1a;Java EE初阶&#x1f447; ✨每日一语&#xff1a;知不足而奋进&#xff0c;望远山而前行。 目 录&#x1f4a4;一. 认识线程&#xff08;Thread&#xff09;&#x1f34e;1. 线程的引入&#x1f34f;2. 线程…

【计算机网络:自顶向下方法】Chapter4 网络层:数据平面

Chapter44.1 网络层概述4.1.1 网络层服务4.1.2 网络层的主要功能转发&#xff08;局部&#xff09;路由选择&#xff08;全局&#xff09;4.1.3 控制平面和数据平面传统方式SDN方式4.1.4 网络服务模型4.2 路由器组成4.2.1 路由器结构概况4.2.2 转发方式4.2.3 输入端口处理与基于…

什么是api应用程序接口?

API:应用程序接口(API:Application Program Interface) 应用程序接口是一组定义、程序及协议的集合&#xff0c;通过 API 接口实现计算机软件之间的相互通信。API 的一个主要功能是提供通用功能集。程序员通过调用 API 函数对应用程序进行开发&#xff0c;可以减轻编程任务。…

【华为OD机试模拟题】用 C++ 实现 - TLV 编码(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 去重求和(2023.Q1) 文章目录 最近更新的博客使用说明TLV 编码题目输入输出描述示例一输入输出说明Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD 清单查看…

大数据技术之Hadoop

第1章 Hadoop概述1.1 Hadoop是什么1.2 Hadoop发展历史&#xff08;了解&#xff09;1.3 Hadoop三大发行版本&#xff08;了解&#xff09;Hadoop三大发行版本&#xff1a;Apache、Cloudera、Hortonworks。Apache版本最原始&#xff08;最基础&#xff09;的版本&#xff0c;对于…

模型类的编写有没有什么靠谱的优化方法?

模型类的编写需要私有属性&#xff0c;setter...getter...方法、toString方法 和构造函数。虽然这些内容不难&#xff0c;同时也都是通过IDEA工具生成的&#xff0c;但是过程还是必须得走一遍&#xff0c;那么对于模型类的编写有没有什么优化方法?可以通过Lombok来实现优化。L…

C语言--指针进阶2

目录前言函数指针函数指针数组指向函数指针数组的指针回调函数前言 本篇文章我们将继续学习指针进阶的有关内容 函数指针 我们依然用类比的方法1来理解函数指针这一全新的概念&#xff0c;如图1 我们用一段代码来验证一下&#xff1a; int Add(int x, int y) {return xy;…

idea报错idea start filed

今天遇到idea启动失败的问题 问题分析&#xff1a; address already in use&#xff1a;bind idea需要的端口被占用 解决 重启就行&#xff0c;重启会重新分配端口。 官方解决 查看给的网站地址&#xff0c;这里官方给出的原因&#xff08;访问好慢&#xff0c;搭梯子我才…

图节点嵌入相关算法学习笔记

引言 本篇笔记为coggle 2月打卡任务&#xff0c;正好也在学习cs224w&#xff0c;干脆就一起做了&#xff0c;以下是任务列表&#xff1a; 任务名称难度任务1&#xff1a;图属性与图构造低、1任务2&#xff1a;图查询与遍历低、2任务3&#xff1a;节点中心性与应用中、2任务4&…

Spark计算框架入门笔记

Spark是一个用于大规模数据处理的统一计算引擎 注意&#xff1a;Spark不仅仅可以做类似于MapReduce的离线数据计算&#xff0c;还可以做实时数据计算&#xff0c;并且它还可以实现类似于Hive的SQL计算&#xff0c;等等&#xff0c;所以说它是一个统一的计算引擎 既然说到了Spar…

js 拖动--动态改变div的宽高大小

index.html 如下&#xff1a;&#xff08;可以新建一个index.html文件直接复制&#xff0c;打开运行&#xff09; <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta http-equiv"X-UA-Compatible&qu…

Python tkinter -- 第18章 画布控件之窗口

18.2.22 create_window(position, **options) 可以在画布控件中放置其他tkinter控件。放置的方法就是使用窗口组件。一个窗口组件只能容纳一个控件。如果要放置多个控件&#xff0c;可以把这些控件作为Frame控件的子控件&#xff0c;将Frame控件放入窗口组件中&#xff0c;就可…

超简单 华为OD机试用Python实现 -【踢石头子,踢石子问题】(2023-Q1 新题)

华为OD机试题 华为OD机试300题大纲踢石头子,踢石子问题题目输入输出示例一输入输出Python 代码如下所示算法思路华为OD机试300题大纲 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD 清单查看地址:blog.csdn.net/hihell/categ…

ChatGPT似乎有的时候并不能搞懂Java的动态分派,你懂了吗?

目录 碎碎念 ChatGPT 中出现的问题 那么正确答案应该是什么呢&#xff1f; 分派的相关知识点总结&#xff1a; 分派是什么&#xff1f; 静态分派与动态分派&#xff1a; Java语言是静态多分派&#xff0c;动态单分派的&#xff1b; 静态分派&#xff1a;静态重载多分派…

追梦之旅【数据结构篇】——详解C语言实现二叉树

详解C语言实现二叉树~&#x1f60e;前言&#x1f64c;什么是二叉树&#xff1f;二叉树的性质总结&#xff1a;整体实现内容分析&#x1f49e;1.头文件的编写&#xff1a;&#x1f64c;2.功能文件的编写&#xff1a;&#x1f64c;1&#xff09;前序遍历的数值来创建树——递归函…

IGKBoard(imx6ull)-Input设备编程之按键控制

文章目录1- input子系统介绍2- input事件目录&#xff08;1&#xff09;struct input_event 结构体&#xff08;2&#xff09;type&#xff08;事件类型&#xff09;&#xff1a;&#xff08;3&#xff09;code&#xff08;事件编码&#xff09;&#xff08;4&#xff09;value…

【华为OD机试模拟题】用 C++ 实现 - 九宫格按键输入(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 去重求和(2023.Q1) 文章目录 最近更新的博客使用说明九宫格按键输入题目输入输出示例一输入输出说明示例二输入输出说明Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高…

webp格式转换成png怎么转

相对于png 图片&#xff0c;webp比png小了45%&#xff0c;但是缺点是你压缩的时候需要的时间更久了&#xff1b;优点是体积小巧&#xff1b;缺点是兼容性不太好, 只有opera,和chrome支持&#xff0c;不仅如此在后期的编辑修改上也很多软件无法打开。所以我们通常要将webp格式转…