算法学习:LeetCode学习笔记——146:LRU缓存机制

leetCode学习笔记(java):146:LRU缓存机制

题目描述:
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

进阶:

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

示例:

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

思路:题目要求算法时间复杂度为O(1),且数据的存储形式为(key,value)形式,很容易想到要使用到类似于HashMap的数据结构。
再看LRU的定义和工作原理举例:
LRU :是内存管理的一种页面置换算法,对于在内存中但又不用的数据块(内存块)叫做LRU,操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间来加载另外的数据。
什么是LRU算法? LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。
工作原理举例:
LRU(least recently used)最近最少使用。
假设 序列为 4 3 4 2 3 1 4 2
物理块有3个 则
首轮 4调入内存 4
次轮 3调入内存 3 4
之后 4调入内存 4 3
之后 2调入内存 2 4 3
之后 3调入内存 3 2 4
之后 1调入内存 1 3 2(因为最少使用的是4,所以丢弃4)
之后 4调入内存 4 1 3(原理同上)
最后 2调入内存 2 4 1

从这个例子我们也很容易看出,LRU的工作原理类似于队列,每次将刚刚使用过的数据块(认为这个数据块在之后会经常被使用)放在队尾,队列满的话就将队首的元素进行删除。
这是我最开始的思路,通过Queue和HashMap实现代码如下

class LRUCache {
    private Queue<Integer> queue;
    private  Map<Integer,Integer> hashMap; 
    private int capacity;
    public LRUCache(int capacity) {
        queue = new LinkedList();
        this.capacity = capacity;
        hashMap = new HashMap<Integer, Integer>();
    }

    public int get(int key) {
        if(queue.contains(key)){  //如果队列中已经存在key,则在获取value后,将key放到队尾
            int value = hashMap.get(key);
            queue.remove(key);
            queue.offer(key);  //将key重新加入到队尾
            return value;
        }else{
            return -1;
        }

    }
    public void put(int key, int value) {
        if(queue.size() == capacity && !hashMap.containsKey(key)){
            hashMap.remove(queue.poll());
        }else if(hashMap.containsKey(key)){  //如果key已经存在,则需要更新value值,
                                           //此时也视为操作该数据所以也要进行key重新入队的操作
            hashMap.put(key,value);    
            queue.remove(key);
            queue.offer(key);
        }
        if(!hashMap.containsKey(key)){    //如果key为新值,则直接将数据加入map,并将key入队
            hashMap.put(key,value);
            queue.offer(key);
        }
    }
}
/**
 * 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);
 */

在这里插入图片描述
虽然结果是通过了,但时间复杂度却高的吓人,接下来进行改进。

原因分析:时间复杂度之所以这么高的原因,应该是因为每次增删都使用了remove()方法,在删除队列中的特定晕元素,必须进行遍历判断,因此当数据量很大时,效率是很低的。

改进方案:参考其他人的代码,大部人都用了LinkedHashMapde 数据结构进行设计,LinkedHashMap是 双链表+HashMap,即可以使插入删除(key,value)的时间复杂的达到O(1),也可以通过双向链表实现节点的快速插入删除,时间复杂度会大大降低。
即用HashMap能保证查找的时间复杂度是O(1),双向链表保证的是增删的时间复杂度是O(1),当然用单向链表也可以,但是不太方便;
也就是用双向链表的插入到头结点删除结点来代替方法1中的 删除队列key元素remove和**key的重新入队和出队queue.poll(),queue.offer()**的操作来提升增删效率。
双向链表的结构如下:
在这里插入图片描述

LinkedHashMap的结构如下:(转自:https://blog.csdn.net/justloveyou_/article/details/71713781)
在这里插入图片描述

则以LinkedHashMap为数据结构进行LRU的实现:
先将(key,value)存入dulNode(双向链表的结点),再将(key,dulNode)存入HashMap中,Map进行快速查找(O(1)),链表进行快速增删(O(1)。

public class LRUCache2 {
    class dulNode{   //双向链表的结点
        int key;
        int value;
        dulNode pre;
        dulNode next;
        dulNode(int key,int value){
            this.key = key;
            this.value = value;
        }
    }
    private  dulNode head;
    private  dulNode tail;
    private int size = 0;  //记录链表有效数据的长度
    private int capacity;
    Map<Integer,dulNode> hashMap;

    public void removeNode(dulNode node){  //双向链表中删除结点
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }
    public void addToHead(dulNode node){   //将结点插入到头结点后,对应队列的入队
        node.next = head.next;   //初始时head.next 即为tail,相当与 node.next = tail;
        head.next.pre = node;
        node.pre = head;
        head.next = node;
    }
    public LRUCache2(int capacity) {
        hashMap = new HashMap<Integer,dulNode>();
        this.capacity = capacity;  //初始化最大容量
        head = new dulNode(0,0);  //初始化头结点
        tail = new dulNode(0,0); //初始化尾结点
        head.pre = null;
        head.next = tail;
        tail.next = null;
        tail.pre = head;
    }
    public int get(int key) {
        if(hashMap.get(key) != null){
            int value = hashMap.get(key).value;
            dulNode latest = hashMap.get(key);
            removeNode(latest);   //每次对结点进行get操作,则视这个结点为最新的,将其删除,
            addToHead(latest);   //再插入到头结点之后
            return value;
        }else{
            return -1;
        }
    }
    public void put(int key, int value) {
        if(hashMap.get(key) !=null ){   //若key已经存在,则需更新key,value,则需先删除原始key结点
            removeNode(hashMap.get(key));
            hashMap.remove(key);
            size--;
        }
        dulNode newNode = new dulNode(key,value);
        hashMap.put(key,newNode);
        if(size <capacity){     //链表长度小于阈值则直接加到头结点后面
            addToHead(newNode);
            size++;
        }else{   //如果超过阈值则需先删除尾部前的结点,在将新结点插入到头结点后
            dulNode reNode = tail.pre;
            removeNode(reNode);
            hashMap.remove(reNode.key);
            addToHead(newNode);
        }
    }
}

执行效率成功提高到了106ms,果然对于需要频繁进行增删的数据,链表的效率的确比其他数据结构要高的多。
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值