LRU算法实现

什么是LRU算法?

LRU是一种缓存淘汰算法,比方说手机只能开三个应用,你开第四个应用的时候,最先打开的那个就会被关闭,而中途你用过哪个应用,哪个就会被提到最前面,剩下的顺序不变。

那么你要接受一个capacity参数作为缓存的最大容量,然后实现两个API,一个是put(key,val),另外一个是get(key),举个具体例子看看LRU如何工作:

//缓存容量为2
LRUCache cache = new LRUCache(2);
//cache就像一个队列
//最近使用的放在队头,久未使用的放在队尾

cache.put(1,1);

cache.put(2,2);
//cache = [(2,2),(1,1)];

cache.get(1);
//cache = [(1,1),(2,2)];

cache.put(3,3);
//cache = [(3,3),(1,1)];

cache.get(2);   //返回-1,未找到

cache.put(1,4);
//cache = [(1,4).(3,3)];

既然要用到键值对,那么肯定有map结构,而又需要方便地删除队尾元素,访问哪个元素以后还要放在队头,那么使用双向链表比较合适,这个数据结构长这样:
在这里插入图片描述

首先是双链表的节点类:

class Node {
    public int key,val;
    public Node next,prev;
    public Node(int k, int v){
        this.key = k;
        this.val = v;
    }
}

然后依靠此节点类构建一个双链表,实现几个需要的API:

class DoubleList{
    //链表头部添加节点
    public void addFirst(Node x);
    
    //删除链表中的x节点
    public void remove(Node x);
    
    //删除链表中的最后一个节点,并返回
    public Node removeLast();
    
    //返回链表长度
    public int size();
}

这里也可以解释为什么要用双向链表,因为我们有删除操作,删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针。

先把逻辑理清楚:

HashMap<Integer,Node> map;

DoubleList cache;

int get(int key){
    if(key不存在){
        return -1;
    }
    else{
        将数据(key,val)移到开头;
        return val; 
    }
}

void put(int key,int val){
    Node x = new Node(key,val);
    if(key已经存在){
        把旧数据删除;
        将新数据x插入到开头
    }
    else{
        if(cache已满){
            删除最后一个节点腾位置
            删除map中映射到该数据的键
        }
        将新节点x插入到开头
        map中新建key对新节点x的映射
    }
}

实际代码实现:

class LRUCache {
    private HashMap<Integer,Node> map;
    
    private DoubleList cache;
    
    private int capacity;
    
    public LRUCache(int cap){
        this.capacity = cap;
        map  = new HashMap<>();
        cache = new DoubleList();
    }
    
    public int get(int key){
        if(!map.containsKey(key)){
            return -1;
        }
        int val = map.get(key).val;
        //利用put方法把数据提前
        put(key,val);
        return val;
    }
    
    public void put(int key,int val){
        //先把新节点x做出来
        Node x = new Node(key,val);
        if(map.containsKey(key)){
            cache.remove(map.get(key));
            cache.addFirst(x);
        }
        else{
            if(cache.size == capacity){
                Node last = cache.removeLast();
                //这里要把map中的键也删除
                map.remove(last.key);
            }
            cache.addFirst(x);
            //这里要在map中添加索引
            map.put(key,x);
        }
    }
}

到这里,你已经掌握了LRU算法的思想和实现了,就是在处理链表节点的同时不要忘了更新哈希表中对节点的映射


再补上一个完全实现代码:

import java.util.HashMap;
import java.util.Map;

public class LRUCache {

    private Map<Integer, ListNode> map;

    /**
     * 双链表结点类
     */
    private class ListNode {

        private Integer key;
        private Integer value;
        /**
         * 前驱结点 precursor
         */
        private ListNode pre;
        /**
         * 后继结点 successor(写成 next 是照顾单链表的表示习惯)
         */
        private ListNode next;

        public ListNode() {
        }

        public ListNode(Integer key, Integer value) {
            this.key = key;
            this.value = value;
        }
    }

    private int capacity;

    /**
     * 虚拟头结点没有前驱
     */
    private ListNode dummyHead;
    /**
     * 虚拟尾结点没有后继
     */
    private ListNode dummyTail;

    public LRUCache(int capacity) {
        map = new HashMap<>(capacity);
        this.capacity = capacity;
        dummyHead = new ListNode(-1, -1);
        dummyTail = new ListNode(-1, -1);
        // 初始化链表为 head <-> tail

        dummyHead.next = dummyTail;
        dummyTail.pre = dummyHead;
    }

    /**
     * 如果存在,把当前结点移动到双向链表的头部
     *
     * @param key
     * @return
     */
    public int get(int key) {
        if (map.containsKey(key)) {
            ListNode node = map.get(key);
            int val = node.value;

            // 把当前 node 移动到双向链表的头部
            moveNode2Head(key);
            return val;
        } else {
            return -1;
        }
    }

    /**
     * 如果哈希表的容量满了,就要删除一个链表末尾元素,然后在链表头部插入新元素
     *
     * @param key
     * @param value
     */
    public void put(int key, int value) {
        if (map.containsKey(key)) {
            // 1、更新 value
            map.get(key).value = value;
            // 2、把当前 node 移动到双向链表的头部
            moveNode2Head(key);
            return;
        }

        // 放元素的操作是一样的

        if (map.size() == capacity) {
            // 如果满了
            ListNode oldTail = removeTail();

            // 设计 key 就是为了在这里删除
            map.remove(oldTail.key);
        }

        // 然后添加元素
        ListNode newNode = new ListNode(key, value);
        map.put(key, newNode);
        addNode2Head(newNode);
    }

    // 为了突出主干逻辑,下面是 3 个公用的方法

    /**
     * 删除双链表尾部结点
     */
    private ListNode removeTail() {
        ListNode oldTail = dummyTail.pre;
        ListNode newTail = oldTail.pre;

        // 两侧结点建立连接
        newTail.next = dummyTail;
        dummyTail.pre = newTail;

        // 释放引用
        oldTail.pre = null;
        oldTail.next = null;

        return oldTail;
    }

    /**
     * 把当前 key 指向的结点移到双向链表的头部
     *
     * @param key
     */
    private void moveNode2Head(int key) {
        // 1、先把 node 拿出来
        ListNode node = map.get(key);

        // 2、原来 node 的前驱和后继接上
        node.pre.next = node.next;
        node.next.pre = node.pre;

        // 3、再把 node 放在末尾
        addNode2Head(node);
    }

    /**
     * 在双链表的头部新增一个结点
     *
     * @param newNode
     */
    private void addNode2Head(ListNode newNode) {
        // 1、当前头结点
        ListNode oldHead = dummyHead.next;

        // 2、末尾结点的后继指向新结点
        oldHead.pre = newNode;

        // 3、设置新结点的前驱和后继
        newNode.pre = dummyHead;
        newNode.next = oldHead;

        // 4、更改虚拟头结点的后继结点
        dummyHead.next = newNode;
    }
}
/**
 * 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);
 */
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值