LeetCode146 手撕LRU算法的2种实现方法

简介: LeetCode146 手撕LRU算法的2种实现方法

最近最久未使用 如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小

1.用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。


2.利用一个链表来实现,每次新插入数据的时候将新数据插到链表的头部;每次缓存命中(即数据被访问),则将数据移到链表头部;那么当链表满的时候,就将链表尾部的数据丢弃。


3.利用链表和hashmap。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。


对于第一种方法, 需要不停地维护数据项的访问时间戳,另外,在插入数据、删除数据以及访问数据时,时间复杂度都是O(n)。对于第二种方法,链表在定位数据的时候时间复杂度为O(n)。所以在一般使用第三种方式来是实现LRU算法。

实现方案


4.使用LinkedHashMap实现

LinkedHashMap底层就是用的HashMap加双链表实现的,而且本身已经实现了按照访问顺序的存储。此外,LinkedHashMap中本身就实现了一个方法removeEldestEntry用于判断是否需要移除最不常读取的数,方法默认是直接返回false,不会移除元素,所以需要重写该方法。即当缓存满后就移除最不常用的数。


class LRUCache extends LinkedHashMap<Integer, Integer>{
    private int capacity;
    public LRUCache(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
    public void put(int key, int value) {
        super.put(key, value);
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity; 
    }
}
class LRUCache {
    // 最大容量
    int cap;
    // LUR的关键:哈希链表 
    LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();
    public LRUCache(int capacity) {
        this.cap = capacity;
    }
    public int get(int key) {
        if (!cache.containsKey(key)){
            return -1;
        }
        // 调用了,所以把key置为最近使用
        makeRencently(key);
        return cache.get(key);
    }
    public void put(int key, int value) {
        // 如果已经包含key,先修改key的值,再将它变为最近使用
        if(cache.containsKey(key)){
            // 修改key的值(因为key的唯一性,放入的时候回自动覆盖原来的value)
            cache.put(key,value);
            // 再将它变为最近使用
            makeRencently(key);
            return;
        }
        // 如果超过了储存量,则删除头节点(因为实现的是尾插入,所以头结点就是最久没有用的,删去)
        if (cache.size() >= this.cap){
            // 获取头结点
            // Map中所有的键存入到set集合中。因为set具备迭代器。所有可以迭代方式取出所有的键
            // 使用.iterator()迭代器后的指针其实指向的是第一个元素的上方,即指向一个空
            // .next()指针下移一位,指向头节点。hasNext方法的,判断下一个元素的有无,并不移动指针
            int oldestKey = cache.keySet().iterator().next();
            //删去头结点
            cache.remove(oldestKey);
        }
        // 放入,将新的key添加到链表尾部
        cache.put(key,value);
    }
    // 将节点移到链表尾部,变为最近使用
    public void makeRencently(int key){
        // 获取key对应的值
        int val = cache.get(key);
        // 删除key,重新插入队尾
        cache.remove(key);
        cache.put(key,val);
    }
}


03e0fcceb0b942ad8f3f5b1ae555e8a3.png


注意、以上都不是面试官想要看到的,需要自己实现双向链表+hashmap的映射关系。


只需要三步:

1.构建Node节点
2.构建双向链表
3.构建hashmap映射


一、Node节点

// 一、节点类
class Node {
    public int key, val;
    public Node next, prev; //下一个和前面一个
    public Node (int key, int val) {
        this.key = key;
        this.val = val;
    }
}
// 二、双端链表
class DoubleLinked {
    private Node head, tail;//头结点和尾结点
    private int size;//链表的元素数目
    public DoubleLinked() {
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head.next = tail;
        tail.prev = head;
        size = 0;
    }
    // 在头部插入node结点
    public void addFirst(Node x) {
        x.prev = head;
        x.next = head.next;
        head.next.prev = x;
        head.next = x;
        size++;
    }
    // 移除指定结点
    public void remove(Node x) {
        x.prev.next = x.next;
        x.next.prev = x.prev;
        size--;
    }
    // 删除链表的第一个结点,并返回该结点
    public Node removeLast() {
        if(head.next == tail) return null;//返回空
        Node last = tail.prev;
        remove(last);//删除尾结点;
        return last;
    }
    public int size() {
        return size;
    }
}
// 三、构造hashMap映射
class LRUCache {
    HashMap<Integer, Node> map;
    DoubleLinked linked;
    int cap; //容量
    public LRUCache(int capacity) {
        map = new  HashMap<>();
        linked = new DoubleLinked();
        this.cap = capacity;
    }
    public int get(int key) {
        if(!map.containsKey(key)) return -1;
        int val = map.get(key).val;
        put(key, val);//放入头结点
        return val;
    }
    public void put(int key, int value) {
        Node x = new Node(key, value);
        if(map.containsKey(key)) {
            linked.remove(map.get(key));//移除结点
            linked.addFirst(x);
            map.put(key, x);
        }else {
            if(cap == cache.size()) {
                Node last = linked.removeLast();
                map.remove(last.key);
            }
            linked.addFirst(x);
            map.put(key, x);
        }
    }
}


目录
相关文章
|
26天前
|
机器学习/深度学习 算法 数据挖掘
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构。本文介绍了K-means算法的基本原理,包括初始化、数据点分配与簇中心更新等步骤,以及如何在Python中实现该算法,最后讨论了其优缺点及应用场景。
84 4
|
2月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
54 3
|
2月前
|
算法
Leetcode 初级算法 --- 数组篇
Leetcode 初级算法 --- 数组篇
43 0
|
24天前
|
存储 算法 安全
SnowflakeIdGenerator-雪花算法id生成方法
SnowflakeIdGenerator-雪花算法id生成方法
20 1
|
1月前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
|
28天前
|
JSON 算法 数据挖掘
基于图论算法有向图PageRank与无向图Louvain算法构建指令的方式方法 用于支撑qwen agent中的统计相关组件
利用图序列进行数据解读,主要包括节点序列分析、边序列分析以及结合节点和边序列的综合分析。节点序列分析涉及节点度分析(如入度、出度、度中心性)、节点属性分析(如品牌、价格等属性的分布与聚类)、节点标签分析(如不同标签的分布及标签间的关联)。边序列分析则关注边的权重分析(如关联强度)、边的类型分析(如管理、协作等关系)及路径分析(如最短路径计算)。结合节点和边序列的分析,如子图挖掘和图的动态分析,可以帮助深入理解图的结构和功能。例如,通过子图挖掘可以发现具有特定结构的子图,而图的动态分析则能揭示图随时间的变化趋势。这些分析方法结合使用,能够从多个角度全面解读图谱数据,为决策提供有力支持。
|
2月前
|
算法 索引
HashMap扩容时的rehash方法中(e.hash & oldCap) == 0算法推导
HashMap在扩容时,会创建一个新数组,并将旧数组中的数据迁移过去。通过(e.hash & oldCap)是否等于0,数据被巧妙地分为两类:一类保持原有索引位置,另一类索引位置增加旧数组长度。此过程确保了数据均匀分布,提高了查询效率。
45 2
|
2月前
|
搜索推荐 Shell
解析排序算法:十大排序方法的工作原理与性能比较
解析排序算法:十大排序方法的工作原理与性能比较
74 9
|
2月前
|
存储 算法 Java
数据结构与算法学习八:前缀(波兰)表达式、中缀表达式、后缀(逆波兰)表达式的学习,中缀转后缀的两个方法,逆波兰计算器的实现
前缀(波兰)表达式、中缀表达式和后缀(逆波兰)表达式的基本概念、计算机求值方法,以及如何将中缀表达式转换为后缀表达式,并提供了相应的Java代码实现和测试结果。
121 0
数据结构与算法学习八:前缀(波兰)表达式、中缀表达式、后缀(逆波兰)表达式的学习,中缀转后缀的两个方法,逆波兰计算器的实现
|
2月前
|
机器学习/深度学习 人工智能 开发框架
【AI系统】AI 学习方法与算法现状
在人工智能的历史长河中,我们见证了从规则驱动系统到现代机器学习模型的转变。AI的学习方法基于深度神经网络,通过前向传播、反向传播和梯度更新不断优化权重,实现从训练到推理的过程。当前,AI算法如CNN、RNN、GNN和GAN等在各自领域取得突破,推动技术进步的同时也带来了更大的挑战,要求算法工程师与系统设计师紧密合作,共同拓展AI技术的边界。
118 1