LRU缓存算法实现

LRU缓存算法的含义、应用背景、理解以及Java代码实现

  1. LRU:全称least recently used,中文名为最近最少使用,它是最常用的缓存淘汰策略。
  2. LRU算法的应用背景:
    1. 让我们先思考一下互联网应用的整体流程,没错,大致流程就是下面这张图。
    2. 正如图中所示,当大量用户访问服务器时,服务器会频繁请求数据库调取数据,此时数据库的访问就很有可能出现瓶颈,那么我们可以在服务器本地缓存用户信息,缓解频繁访问数据库的压力,但是缓存数据总得有一个上限,我们采用什么算法解决达到上限之后的数据淘汰问题呢?今天要谈的LRU缓存算法正是为了解决此问题。
  3. LRU算法的理解:我们假设,每个缓存都具有优先级,每次使用缓存都应该将缓存的优先级提高,很久没有使用的缓存优先级最低,最应该被淘汰。回到实际的应用程序中,我想大家应该觉得这是理所当然的,很久没有使用的用户信息确实应该从缓存中淘汰掉,应该去保留那些频繁使用的用户信息,这有利于产生更高的利益。
  4. Java代码实现
package yuzhuoran.算法练习;

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

/**
 * 节点和LRU缓存采用泛型设计
 * 需要用到的数据结构是:双向链表、hash表
 * 双向链表用于数据优先级的设置:规定链表头优先级最高
 * hash表用于缓存数据
 */
public class LRUCache<T, V> {

    // 存储元素的个数
    int size = 0;
    // 容器大小
    int capacity = 0;
    // 缓存存储内容
    Map<T, Node<T, V>> cache;
    // 双向链表需要两个指针指向开头和结尾
    Node<T, V> first;
    Node<T, V> last;

    // 初始化容器大小和Map的实现类
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();
    }

    // 获取缓存元素
    public V get(T key) {
        //Map包含这个Key时,开始获取
        if(cache.containsKey(key)) {
            //获取Key对应的node
            Node<T, V> node = cache.get(key);
            //提升优先级
            this.getHigh(node);
            //返回此节点的内容
            return node.content;
        }
        //否则返回null
        return null;
    }

    //提升节点在双向链表的优先级(规定链表头优先级最高,所以要提升到链表头)
    public void getHigh(Node<T, V> node) {
        //先获取头部节点,便于下面更改头部节点
        Node<T, V> firstNode = this.first;

        //根据节点在链表的位置以不同方式提升优先级
        //节点在头部
        if(node.getPrev() == null) {
            return;
        }
        //节点在尾部
        else if(node.getNext() == null) {
            node.getPrev().setNext(null);
            this.last = node.getPrev();
        }
        //节点在中间
        else {
            node.getPrev().setNext(node.getNext());
            node.getNext().setPrev(node.getPrev());
        }
        //将节点设置为头部节点,注意断开新的头部节点的前置节点并建立新的头部节点与旧的头部节点的关系
        this.first = node;
        this.first.setNext(firstNode);
        this.first.setPrev(null);
        firstNode.setPrev(this.first);
    }

    // 添加新元素
    public void put(T key, V value) {
        //先根据Key从缓存中获取节点,看是否有节点存在
        Node<T, V> node = this.cache.get(key);
        //根据存在情况进行讨论
        if(node != null) {
            node.content = value;
            this.getHigh(node);
            return;
        } else {
            node = new Node<>(key,value);
        }
        //先获取头部、尾部指针的节点,便于操作
        Node<T, V> firstNode = this.first;
        Node<T, V> lastNode= this.last;

        /**
         * 分三种情况讨论:
         * 1.链表头部节点为null(即第一次添加数据),则设置头部节点、尾部节点,并将数据添加至缓存中。
         * 2.第二次及以后添加数据并且容器未满时,直接提升要添加的节点的优先级,并将数据添加至缓存中。
         * 3.容器满之后,需要一边从缓存中删除一边添加,注意更改链表首尾节点。
         */
        if(firstNode == null) {
            this.first = node;
            this.last = node;
            cache.put(key,this.first);
        } else if(this.size == this.capacity) {
            /**
             * 注意这段代码!!!
             * 在测试中发现当capacity为1的时候,104行(即try代码块的第一行)会出现NullPointerException,所以需要捕获这个异常并进行其他逻辑处理
             */
            this.last = this.last.getPrev();
            try {
                this.last.setNext(null);
                this.first = node;
                this.first.setNext(firstNode);
                firstNode.setPrev(this.first);
                cache.remove(lastNode.key);
                cache.put(key,this.first);
                return;
            } catch (NullPointerException e) {
                this.first = node;
                this.last = node;
                cache.remove(lastNode.key);
                cache.put(key,this.first);
                return;
            }
        } else {
            this.first = node;
            this.first.setNext(firstNode);
            firstNode.setPrev(this.first);
            cache.put(key,this.first);
        }
        //容器未满之前添加元素都将使元素数量+1
        this.size++;
    }

    //将缓存数据的Key和节点的content打印为字符串,便于测试
    public String toString() {
        StringBuilder sb = new StringBuilder();
        Node<T, V> node = this.first;
        while (node != null) {
            sb.append(node.key + ":" + node.content + " , ");
            node = node.getNext();
        }
        return sb.toString();
    }

    //内部的节点类,用于建立双向链表
    class Node<T, V> {
        //双向链表的前后指针
        Node<T, V> next;
        Node<T, V> prev;

        T key;  //对应缓存中的Key
        V content;  //对应缓存中的Value

        public Node() {
        }

        public Node(T key, V content) {
            this.key = key;
            this.content = content;
        }

        public Node<T, V> getNext() {
            return next;
        }

        public void setNext(Node<T, V> next) {
            this.next = next;
        }

        public Node<T, V> getPrev() {
            return prev;
        }

        public void setPrev(Node<T, V> prev) {
            this.prev = prev;
        }
    }

    public static void main(String[] args) {
        //在本地测试时,多组测试已正确。下面给出一组测试和结果
        LRUCache<Integer,String> cache = new LRUCache<>(3);
        cache.put(1,"1");
        cache.put(2,"2");
        cache.put(3,"3");
        cache.get(1);
        System.out.println(cache.toString());
    }
}
// 测试结果:1:1 , 3:3 , 2:2 , 
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现LRU缓存淘汰算法的一种常用方法是使用哈希表和双向链表结合实现。具体实现步骤如下: 1. 使用一个哈希表来存储缓存中的键值对,其中键为缓存的键,值为该键对应的节点在双向链表中的指针。 2. 使用一个双向链表来存储缓存中的键值对,每个节点包含该节点对应的键、值以及前驱和后继指针。 3. 当有新的键值对被访问时,首先在哈希表中查找该键是否存在,如果存在,则将该键所对应的节点移到链表头部,表示最近被访问过;如果不存在,则在哈希表和链表中分别添加该键值对以及节点,并将该节点插入到链表头部。 4. 当缓存空间不足时,淘汰链表尾部的节点,并在哈希表中删除对应的键值对。 下面是一个Python实现LRU缓存淘汰算法的代码示例: ```python class LRUCache: def __init__(self, capacity: int): self.capacity = capacity self.cache = {} self.head = Node(0, 0) self.tail = Node(0, 0) self.head.next = self.tail self.tail.prev = self.head def get(self, key: int) -> int: if key in self.cache: node = self.cache[key] self._remove(node) self._add(node) return node.val else: return -1 def put(self, key: int, value: int) -> None: if key in self.cache: self._remove(self.cache[key]) node = Node(key, value) self.cache[key] = node self._add(node) if len(self.cache) > self.capacity: node = self.head.next self._remove(node) del self.cache[node.key] def _add(self, node): p = self.tail.prev p.next = node node.prev = p node.next = self.tail self.tail.prev = node def _remove(self, node): p = node.prev n = node.next p.next = n n.prev = p class Node: def __init__(self, key, val): self.key = key self.val = val self.prev = None self.next = None ``` 在这个实现中,我们使用了一个双向链表来维护缓存中节点的顺序,其中head和tail分别是链表的头节点和尾节点。同时,我们使用了一个哈希表来查询节点是否存在以及快速删除节点。当有新的节点被访问时,我们将其移到链表头部,并且当缓存空间不足时,我们淘汰链表尾部的节点。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值