java 代码实现LRU算法

RUL算法实现

  • LRU简单介绍
    • 实现思虑
    • java代码实现
    • 代码简单分析
      • 内部类、链表节点MyNode.java
      • 构造方法分析
      • get方法分析
      • put方法
    • 结束语

LRU简单介绍

LRU(Least Recently Used)是一种常见的页面置换算法,在计算中,所有的文件操作都要放在内存中进行,然而计算机内存大小是固定的,所以我们不可能把所有的文件都加载到内存,因此我们需要制定一种策略对加入到内存中的文件进项选择。

实现思虑

1、数据需要保存,可以使用ConcurrentHashMap
2、需要记录数据的先后关系,常见的保存方式可以是数组、链表,但是使用数组则不方便修改数据保存的位置。大部分同学都应该会刷面试题,arrayList 和 LinkedList 的区别,应该都清楚,LinkedList在数据的增删方面具有极大的优势。但是此处我们更适合双向链表,默认拥有头、尾部节点。
3、rul算法需要一个限定储存数据的长度

java代码实现

LRUCache.java

public class LRUCache<V> {
    private int capacity = 1024;
    private Map<String,MyNode<String,V>> map = new ConcurrentHashMap<>();
    private MyNode head;
    private MyNode tail;

    public LRUCache(){
        this.head = new MyNode();
        this.tail = new MyNode();
        this.head.nextNode = tail;
        this.tail.preNode = head;
    }
    //为方便测试以及动态控制容量
    public LRUCache(int capacity){
        this.head = new MyNode();
        this.tail = new MyNode();
        this.capacity = capacity;
        this.head.nextNode = tail;
        this.tail.preNode = head;
    }

    //
    public V get(String key){
        MyNode<String,V> curNode = map.get(key);
        if(curNode==null){
            return null;
        }else if(curNode==head.nextNode){
            //获取数据就是头部最新 数据,无需处理
            return curNode.val;
        }
        //1 headNext 指向curNode.next
        MyNode<String,V> headNext = head.nextNode;
        headNext.nextNode = curNode.nextNode;
        curNode.nextNode.preNode = headNext.nextNode;

        //2 head.next 指向curNode
        curNode.nextNode = headNext;
        curNode.preNode = head;
        head.nextNode = curNode;

        return curNode.val;
    }

    public void put(String key,V v){
        V gv = get(key);
        if(gv==null){
            //不存在的数据
            //1、判断是否满容量 是则移除队尾数据 添加到队首  否则,只添加到队首
            if(capacity>map.size()){
                //1、判断是否满容量 ,未满,只添加到队首
                MyNode<String,V> newNode = new MyNode();
                newNode.key=key;
                newNode.val =v;

                intoHeadNext(newNode,key,v);
                map.put(key,newNode);
            }else{
                //2、判断是否满容量 ,已满,添加到队首、同时,移除队尾
                MyNode<String,V> newNode = new MyNode();
                newNode.key=key;
                newNode.val =v;
                intoHeadNext(newNode,key,v);
                //移除map中的尾部数据
                MyNode<String,V> tailPre = tail.preNode;
                map.remove(tailPre.key);
                //移除队尾
                expireTail();
                map.put(key,newNode);
            }

        }else{
            //存在数据
            if(gv.equals(v)){
                //一样的数据添加进去无需处理
            }else{
                //新数据代替老数据
                MyNode<String,V> newNode = new MyNode();
                newNode.key=key;
                newNode.val =v;
                intoHeadNext(newNode,key,v);

                //移除oldValue
                MyNode<String,V> oldValue = map.get(key);
                oldValue.preNode.nextNode = oldValue.nextNode;
                oldValue.nextNode.preNode = oldValue.preNode;
                oldValue = null;

            }

        }
    }

    /**
     * 新增数据入队首
     * @param key
     * @param v
     */
    private void intoHeadNext(MyNode<String,V> newNode,String key,V v){
        newNode.nextNode = head.nextNode;
        newNode.preNode = head;

        head.nextNode.preNode=newNode;
        head.nextNode = newNode;
    }

    private void expireTail(){
        MyNode tailPre = tail.preNode;

        tailPre.preNode.nextNode = tail;
        tail.preNode = tailPre.preNode;
        tailPre =null;
        //help gc
    }


    private class MyNode<K,V>{
        V val;
        K key;
        MyNode preNode;
        MyNode nextNode;
    }

代码简单分析

内部类、链表节点MyNode.java

preNode和nextNode构造成双向链表,如果还是不太清楚的同学,建议从简单链表重新加强学习,一起加油!

private class MyNode<K,V>{
        V val;
        K key;
        MyNode preNode;
        MyNode nextNode;
    }

构造方法分析

实例话的时候就应该构造好一个最简单的队列

 public LRUCache(){
 		//给双向队列初始化一个默认的头节点 这个节点不能覆盖修改,方便后续操作
        this.head = new MyNode();
        //给双向列表一个默认的尾节点 ,不能修改、覆盖,同头节点
        this.tail = new MyNode();
		//构造双向链表
        this.head.nextNode = tail;
        this.tail.preNode = head;
    }

get方法分析

思路:

  1. 集合中不存在数据,这个情况最简单,直接返回null就可以
  2. 集合中存在数据,需要考虑几个简单的场景
    2.1. 特殊情况,该数据就是链表逻辑首未(不考虑head tail 默认节点),不需要做移动如代码
    2.2. 普通情况,该数据就是链表中的其他位置数据,需要把这个节点数据转移到链表首位,同时断开它之前的位置关系,重新跟历史头部节点建立上下节点关系。本人绘图功底很差,又比较懒,就不绘制图表了,大多同学应该都能理解,不能理解的多思考一下,欢迎交流。
MyNode<String,V> curNode = map.get(key);
 public V get(String key){
        MyNode<String,V> curNode = map.get(key);
        if(curNode==null){
            return null;
        }else if(curNode==head.nextNode){
            //获取数据就是头部最新 数据,无需处理
            return curNode.val;
        }   
       //1 headNext 指向curNode.next
        MyNode<String,V> headNext = head.nextNode;
        headNext.nextNode = curNode.nextNode;
        curNode.nextNode.preNode = headNext.nextNode;

        //2 head.next 指向curNode
        curNode.nextNode = headNext;
        curNode.preNode = head;
        head.nextNode = curNode;

        return curNode.val;
 }

put方法

思路:

  1. 一般情况,我们操作map 直接put是没有问题的,但是,由于需要考虑到需要更新链表,所以,需要先查数据,再有后续操作。
  2. 先执行查询一次,有两个情况,内存中已经存在数据,判断是否跟新添加的数据一样,如果不一样,则更新数据、把该数据放在链表首位,如果一样则只需要把该数据防止再链表首位
  3. 另外一个情况,就是内存中不存在数据,需要判断内存中的数据是否达到储存上限,如果未达到,则只需要添加数据到map,把这个数据放置到链表首位。如果达到上限需要再添加删除队尾节点数据逻辑同时移除map中队尾对应数据。
public void put(String key,V v){
        V gv = get(key);
        if(gv==null){
            //不存在的数据
            //1、判断是否满容量 是则移除队尾数据 添加到队首  否则,只添加到队首
            if(capacity>map.size()){
                //1、判断是否满容量 ,未满,只添加到队首
                MyNode<String,V> newNode = new MyNode();
                newNode.key=key;
                newNode.val =v;

                intoHeadNext(newNode,key,v);
                map.put(key,newNode);
            }else{
                //2、判断是否满容量 ,已满,添加到队首、同时,移除队尾
                MyNode<String,V> newNode = new MyNode();
                newNode.key=key;
                newNode.val =v;
                intoHeadNext(newNode,key,v);
                //移除map中的尾部数据
                MyNode<String,V> tailPre = tail.preNode;
                map.remove(tailPre.key);
                //移除队尾
                expireTail();
                map.put(key,newNode);
            }

        }else{
            //存在数据
            if(gv.equals(v)){
                //一样的数据添加进去无需处理
            }else{
                //新数据代替老数据
                MyNode<String,V> newNode = new MyNode();
                newNode.key=key;
                newNode.val =v;
                intoHeadNext(newNode,key,v);

                //移除oldValue
                MyNode<String,V> oldValue = map.get(key);
                oldValue.preNode.nextNode = oldValue.nextNode;
                oldValue.nextNode.preNode = oldValue.preNode;
                oldValue = null;

            }

        }
    }

   /**
     * 新增数据入队首
     * @param newNode
     * @param key
     * @param v
     */
    private void intoHeadNext(MyNode<String,V> newNode,String key,V v){
        newNode.nextNode = head.nextNode;
        newNode.preNode = head;

        head.nextNode.preNode=newNode;
        head.nextNode = newNode;
    }

    /**
     * 过期队尾数据
     */
    private void expireTail(){
        MyNode tailPre = tail.preNode;

        tailPre.preNode.nextNode = tail;
        tail.preNode = tailPre.preNode;
        tailPre =null;
        //help gc
    }

结束语

不知道大家看出代码的bug了吗,没有bug的代码,是不完美的代码
1、在put 方法中,先查询数据,此处用的get 方法,这里是不合适的,会做一次队列位置移动,应该使用 map.get(key)
2、在判断map.size()跟capacity的大小后,有不同的逻辑,此处不是原子操作,那问题来了,这个put get方法都有操作双向链表,这个操作其实都是线程不安全的。在单个线程中测试,是不能发现问题的。处理方案很简单,只需要在get put 方法添加上synchrocized关键字,但是性能就极大的降低了,大家可以再思考优化方案。

欢迎交流

你可能感兴趣的:(java)