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);
        }
    }
}


目录
相关文章
|
23天前
|
算法 数据安全/隐私保护 计算机视觉
基于二维CS-SCHT变换和LABS方法的水印嵌入和提取算法matlab仿真
该内容包括一个算法的运行展示和详细步骤,使用了MATLAB2022a。算法涉及水印嵌入和提取,利用LAB色彩空间可能用于隐藏水印。水印通过二维CS-SCHT变换、低频系数处理和特定解码策略来提取。代码段展示了水印置乱、图像处理(如噪声、旋转、剪切等攻击)以及水印的逆置乱和提取过程。最后,计算并保存了比特率,用于评估水印的稳健性。
|
15天前
|
存储 算法
【软件设计师】常见的算法设计方法——递推法
【软件设计师】常见的算法设计方法——递推法
|
8天前
|
算法 数据安全/隐私保护 C++
基于二维CS-SCHT变换和扩频方法的彩色图像水印嵌入和提取算法matlab仿真
该内容是关于一个图像水印算法的描述。在MATLAB2022a中运行,算法包括水印的嵌入和提取。首先,RGB图像转换为YUV格式,然后水印通过特定规则嵌入到Y分量中,并经过Arnold置乱增强安全性。水印提取时,经过逆过程恢复,使用了二维CS-SCHT变换和噪声对比度(NC)计算来评估水印的鲁棒性。代码中展示了从RGB到YUV的转换、水印嵌入、JPEG压缩攻击模拟以及水印提取的步骤。
|
15天前
|
算法
【优选算法】——Leetcode——LCR 179. 查找总价格为目标值的两个商品
【优选算法】——Leetcode——LCR 179. 查找总价格为目标值的两个商品
|
15天前
|
算法
【优选算法】——Leetcode——611. 有效三角形的个数
【优选算法】——Leetcode——611. 有效三角形的个数
|
15天前
|
算法 容器
【优选算法】—Leetcode—11—— 盛最多水的容器
【优选算法】—Leetcode—11—— 盛最多水的容器
|
15天前
|
算法
【优选算法】——Leetcode——202—— 快乐数
【优选算法】——Leetcode——202—— 快乐数
【优选算法】——Leetcode——202—— 快乐数
|
15天前
|
算法
[优选算法]——双指针——Leetcode——1089. 复写零
[优选算法]——双指针——Leetcode——1089. 复写零
|
15天前
|
算法
【优选算法】——双指针——Leetcode——283.移动零
【优选算法】——双指针——Leetcode——283.移动零
|
15天前
|
算法
【软件设计师】常见的算法设计方法——穷举搜索法
【软件设计师】常见的算法设计方法——穷举搜索法