手撕面试题算法(1)——LRU算法

前言

LRU(Least Recently Used),即最近最少使用之意,是一种缓存淘汰策略,当需要淘汰一个数据时(如缓存空间已满),其将一个未使用时间最长的数据淘汰(删除)

本文只介绍最常用的LRU手写实现方法,即HashMap+链表的实现方法

适用于 Leetcode 146

源码

点击这里获取源码,别忘了点个Star哦~

一、设计思想

利用链表存储被存入容器的key,每当有一个数据被放入容器,则将此数据的键头插到链表中;
当有一个数据被读取/更新时,视为该数据被使用,将该数据的键在链表中的位置置于链表头
当容器已满,又有新数据被放入,则淘汰最久未被使用的数据,即链表尾的数据。

二、实现后的效果

LRU<Integer,String> lru = new LRU<>(5);
lru.put(1,"a");
lru.put(2,"b");
lru.put(3,"c");
lru.put(4,"d");
lru.put(5,"e");
System.out.println(lru.get(1)); // a , key: 1 被使用
lru.put(6,"f"); // 此时, key: 2 是最久未被使用的, < 2,"b"> 被淘汰
System.out.println(lru.get(3)); // c
lru.put(7,"g"); // 此时, key: 4 是最久未被使用的, <4,"d"> 被淘汰

三、实现步骤

1. 类定义

//手写一个LRU容器
public class LRU<K,V> {
    
    // 容器大小
    private int size;   
    
    // 链表存储key
    private LinkedList<K> linkedList;
    
    // HashMap存储键值对
    private HashMap<K,V> hashMap;
    
    public LRU(int size){
        this.size=size;
        this.linkedList=new LinkedList<>();
        this.hashMap=new HashMap<>();
    }
    
}

需要用到java.util下的LinkedList和HashMap,有条件的同学可以手写一个双向链表,手写LinkedList可以康康我的从零开始手撕一个数据结构(1)——双向链表

2. 方法定义

1)置入数据操作put方法

    public void put(K key,V val){
        //检测链表中是否已有该键
        if (linkedList.contains(key)){ // 链表中存在该键时
            //将该键的位置置于链表头
            linkedList.remove(key);
            linkedList.addFirst(key);
            //更新哈希表
            hashMap.put(key,val);
        }else { // 链表中不存在该键时

            //当容器满,进行淘汰策略
            if (linkedList.size()==size){
                K knockOutKey = linkedList.getLast();
                //在哈希表中删除最久未使用的数据
                hashMap.remove(knockOutKey);
                //链表尾删
                linkedList.removeLast();
            }

            //数据置入哈希表
            hashMap.put(key, val);
            //链表头插
            linkedList.addFirst(key);
        }
    }

(有几行重复代码,为了可读性也就不删了。。)
若put方法的key在链表中已存在,执行put方法视为访问了该数据一次

2)获取数据的get方法

    public V get(K key){
        //检测链表中是否存在该key
        if (linkedList.contains(key)){ // 当链表中存在该key时
            // 将key置于链头
            linkedList.remove(key);
            linkedList.addFirst(key);
            return hashMap.get(key);
        }else { // 链表中不存在该key,返回null
            return null;
        }
    }

get方法应该简单得多,命中缓存时将被命中的数据移到链表头并返回数据,未命中时返回null

3)打印缓存中key的方法

    public String keySetString(){
    	// 当容器为空时,返回null
        if (linkedList.size()==0){
            return null;
        }else { //当容器非空,打印缓存中所有的key
            StringBuilder sb = new StringBuilder();
            Iterator iterator = linkedList.iterator();
            while(iterator.hasNext())
                sb.append(iterator.next()).append(",");
            return sb.toString();
        }
    }

这个方法用于方便测试,观察缓存中的数据分布,可有可无

4)返回缓存中的数据个数

	public int size(){
		return linkedList.size();
	}

缓存中的数据个数即链表的长度

四、总结

自此,一个简单的LRU算法就实现了,来测试一下

        LRU<Integer,String> lru = new LRU<>(3);
        lru.put(1,"a");
        lru.put(2,"b");
        lru.put(3,"c");
        System.out.println(lru.keySetString()); // 3,2,1,
        lru.put(4,"d");
        //(当要存入的数据多于缓存限定的个数时,淘汰最不常用的数据,即 < 1, "a">
        System.out.println(lru.keySetString()); // 4,3,2, 
        lru.put(2,"z"); // 将 < 2,"b">更新为 < 2, "z">
        // key: 2被访问,置于链表头
        System.out.println(lru.keySetString()); // 2,4,3, 
        System.out.println(lru.get(2)); // z
        System.out.println(lru.get(3)); // c
        lru.put(5,"e");
        System.out.println(lru.keySetString()); // 5,3,2,

猜你喜欢

转载自blog.csdn.net/Yuc0114/article/details/108430116
今日推荐