zoukankan      html  css  js  c++  java
  • LeetCode 146. LRU缓存机制

    我的LeetCode:https://leetcode-cn.com/u/ituring/

    我的LeetCode刷题源码[GitHub]:https://github.com/izhoujie/Algorithmcii

    LeetCode 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

    来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/lru-cache
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

    解题思路

    首先一定要明白LRU的缓存逻辑;
    一种思路是直接使用LinkedHashMap来实现LRU缓存,LinkedHashMap有一个关键参数是accessOrder,默认为false,也就是按照put的顺序排序,若accessOrder为true,那么将按照访问时间排序,当前的访问将位于尾部,历史访问将越靠近头部,恰好符合LRU的逻辑;
    另一种思路就是自己实现双链表结构;

    思路1-使用LinkedHashMap构造LRU

    核心是LinkedHashMap的accessOrder参数,afterNodeAccess方法和removeEldestEntry方法:

    • afterNodeAccess方法将根据accessOrder实现排序,默认false按照put顺序排序,设为true则按照访问时间排序,符合缓存的更新策略;
    • removeEldestEntry方法实现淘汰策略,默认false即不淘汰也就是达到最大缓存后将不再resize,设为true将淘汰头部数据,设定size()>capacity便可实现缓存满时淘汰旧数据;

    创建LinkedHashMap需要使用有参构造方法,同时顺带覆写其removeEldestEntry方法:

    LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<Integer, Integer>(capacity, 0.75F, true) {
    	/**
    	 * 
    	 */
    	private static final long serialVersionUID = 1L;
    
    	@Override
    	protected boolean removeEldestEntry(java.util.Map.Entry<Integer, Integer> eldest) {
    		return size() > capacity;
    	}
    };
    

    afterNodeAccess源码部分,可以看到是实现了元素移动到链表尾部:

        void afterNodeAccess(Node<K,V> e) { // move node to last
            LinkedHashMap.Entry<K,V> last;
            if (accessOrder && (last = tail) != e) {
                LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
                p.after = null;
                if (b == null)
                    head = a;
                else
                    b.after = a;
                if (a != null)
                    a.before = b;
                else
                    last = b;
                if (last == null)
                    head = p;
                else {
                    p.before = last;
                    last.after = p;
                }
                tail = p;
                ++modCount;
            }
        }
    

    算法复杂度:

    • 时间复杂度:O(1)
    • 空间复杂度:O(capacity)

    思路2-HashMap+自己实现双链表结构

    实际与LinkedHashMap类似,唯一的区别在于更新节点时,LinkedHashMap对于相同key的节点需要遍历key下的链表,而自己实现的双链表无需遍历;

    算法复杂度:

    • 时间复杂度:O(1)
    • 空间复杂度:O(capacity)

    算法源码示例

    package leetcode;
    
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    
    /**
     * @author ZhouJie
     * @date 2020年4月8日 下午9:55:11 
     * @Description: 146. LRU缓存机制
     *
     */
    public class LeetCode_0146 {
    
    }
    
    /**
     * @author ZhouJie
     * @date 2020年4月8日 下午10:11:52 
     * @Description: 1-利用LinkedHashMap的特性直接构造一个LRU,缺点:在移除key相同的元素时需要遍历链表
     *
     */
    class LRUCache_1 {
    	private LinkedHashMap<Integer, Integer> cache;
    
    	public LRUCache_1(int capacity) {
    		cache = new LinkedHashMap<Integer, Integer>(capacity, 0.75F, true) {
    			/**
    			 * 
    			 */
    			private static final long serialVersionUID = 1L;
    
    			@Override
    			protected boolean removeEldestEntry(java.util.Map.Entry<Integer, Integer> eldest) {
    				return size() > capacity;
    			}
    		};
    
    	}
    
    	public int get(int key) {
    		Integer value = cache.get(key);
    		if (value == null) {
    			return -1;
    		} else {
    			return value;
    		}
    	}
    
    	public void put(int key, int value) {
    		cache.put(key, value);
    	}
    }
    
    /**
     * @author ZhouJie
     * @date 2020年4月8日 下午10:44:26 
     * @Description: 2-只使用HashMap保存数据,自建Node双向链表来记录访问顺序;
     *
     */
    class LRUCache_2 {
    	/**
    	 * @author ZhouJie
    	 * @date 2020年4月8日 下午10:47:19 
    	 * @Description: 定义双链表的Node节点,_0146_2,后缀为题号_方法号
    	 *
    	 */
    	class Node_0146_2 {
    		Node_0146_2 pre;
    		Node_0146_2 next;
    		int k;
    		int v;
    
    		public Node_0146_2(int k, int v) {
    			this.k = k;
    			this.v = v;
    		}
    	}
    
    	// 双链表的头尾节点
    	Node_0146_2 head;
    	Node_0146_2 tail;
    	private int capacity;
    	private HashMap<Integer, Node_0146_2> cache;
    
    	public LRUCache_2(int capacity) {
    		this.capacity = capacity;
    		this.cache = new HashMap<Integer, Node_0146_2>();
    	}
    
    	public int get(int key) {
    		Node_0146_2 node = cache.get(key);
    		if (node != null) {
    			moveToTail(node);
    			return node.v;
    		} else {
    			return -1;
    		}
    	}
    
    	public void put(int key, int value) {
    		Node_0146_2 node = cache.get(key);
    		// 存在时需要更新map对应值并将节点移到链表尾部
    		if (node != null) {
    			node.v = value;
    			moveToTail(node);
    		} else {
    			// 不存在时需要先判断容量,满了需要先删除头部节点和对应map中的数据,然后再新建节点放到链表的尾部并放入map
    			if (cache.size() == capacity) {
    				Node_0146_2 oldHead = removeHead();
    				if (oldHead != null) {
    					cache.remove(oldHead.k);
    				}
    			}
    			Node_0146_2 newNode = new Node_0146_2(key, value);
    			addToTail(newNode);
    			cache.put(key, newNode);
    		}
    	}
    
    	/**
    	 * @author: ZhouJie
    	 * @date: 2020年4月8日 下午11:27:17 
    	 * @param: @return
    	 * @return: Node_0146_2
    	 * @Description: 缓存满时需要删除最旧的数据,对应到方法是丢弃头部节点,返回的节点供map删除对应数据
    	 *
    	 */
    	private Node_0146_2 removeHead() {
    		if (head == null) {
    			return null;
    		} else {
    			Node_0146_2 temp = head;
    			head = head.next;
    			temp.next = null;
    			head.pre = null;
    			return temp;
    		}
    	}
    
    	/**
    	 * @author: ZhouJie
    	 * @date: 2020年4月8日 下午11:28:53 
    	 * @param: @param node
    	 * @return: void
    	 * @Description: get命中时需要更新缓存,对应到方法是移动节点到尾部
    	 *
    	 */
    	private void moveToTail(Node_0146_2 node) {
    		// 若缓存最多只有一个数据或目标节点就是尾部节点时,什么都不做
    		if (head == tail || node == tail) {
    			return;
    		}
    		// 若目标节点是头结点,则需要先更新头结点,否则直接改动上下节点的关联关系
    		if (node == head) {
    			head = head.next;
    			head.pre = null;
    		} else {
    			node.pre.next = node.next;
    			node.next.pre = node.pre;
    		}
    		// 将目标节点移动到尾部
    		tail.next = node;
    		node.pre = tail;
    		node.next = null;
    		tail = node;
    	}
    
    	/**
    	 * @author: ZhouJie
    	 * @date: 2020年4月8日 下午11:31:34 
    	 * @param: @param node
    	 * @return: void
    	 * @Description: 新建缓存总是放到尾部
    	 *
    	 */
    	private void addToTail(Node_0146_2 node) {
    		// 首个数据
    		if (tail == null) {
    			head = tail = node;
    		}
    		tail.next = node;
    		node.pre = tail;
    		tail = node;
    	}
    
    }
    
    /**
     * 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);
     */
    
  • 相关阅读:
    01背包
    用动态规划求两个自然数的最大公约数
    编程实现文件的复制功能,要求源文件名及目标文件名在程序运行后根据提示输入
    this和super
    JAVA中static的使用
    结构化异常处理 笔记
    继承和多态 笔记
    javascript 客户端验证和页面特效制作 学习笔记
    定义封装的类类型 笔记
    C# 核心编程结构Ⅱ 笔记
  • 原文地址:https://www.cnblogs.com/izhoujie/p/12664045.html
Copyright © 2011-2022 走看看