LRU的JAVA实现

什么是LRU算法?

LRU是Least Recently Used的缩写,即最近最久未使用,是一种操作系统中常用的页面置换算法。

知道了什么是LRU后,我们再来聊下它的使用场景

在工作中,对于Redis我们一定是比较熟悉的,它是一个内存数据库;因为它是内存数据库,并且内存的空间是有限的,如果Redis中数据量很大的话,内存就可能被占满,但是此时如果还有数据存入Redis的话,那该怎么办呢?这就是由Redis的的内存淘汰策略所决定的。

LRU最近最久未使用算法就是Redis的内存淘汰策略之一。

设计

1、要求

①、支持数据get查询和数据put插入; ②、此数据结构的操作需要满足时间复杂度为O(1);

2、思路

根据要求的时间复杂度可以立马想到Hash表,Hash表的时间复杂度为O(1),所以可以使用Map这种key-value数据结构来满足时间复杂度;

但是根据LRU最近最久未使用的原理,需要将最新访问的数据放到最前面,并且当缓存容量满了的时候,还需要将最近最久未使用的数据淘汰掉,所以还需要一种数据结构来标识最新访问数据(首部),最久未使用数据(尾部),这里可以使用双向链表实现;

所以设计的LRU的数据结构是:HashMap + 双向链表

注意:也可以使用 LinkedHashMap 尝试实现 LRU缓存的。

3、链表节点

因为LRU缓存设计中使用了双向链表,所以需要设计下链表中的节点类,如下:


public class DoubleLinkedListNode {
  String key;
  Object value;
  // 头指针
  DoubleLinkedListNode pre;
  // 尾指针
  DoubleLinkedListNode next; 
  
  public DoubleLinkedListNode(String key, Object value) {
        this.key = key;
        this.value = value;
  }
}

完整代码

public class LRUCache {
 
  private HashMap<String, DoubleLinkedListNode> map = new HashMap<String, DoubleLinkedListNode>();
  // 头结点
  private DoubleLinkedListNode head;
  // 尾节点
  private DoubleLinkedListNode tail;
  // 双向链表的容量
  private int capacity;
  // 双向链表中节点的数量
  private int size;
 
  public LRUCache(int capacity) {
    this.capacity = capacity;
    size = 0;
  }
 
  /**
   * @Description: 将节点设置为头结点
   * @param node
   */
  public void setHead(DoubleLinkedListNode node) {
    // 节点的尾指针执行头结点
    node.next = head;
    // 节点的头指针置为空
    node.pre = null;
    if (head != null) {
      // 将头结点的头指针执行节点
      head.pre = node;
    }
    head = node;
    if (tail == null) {
      // 如果双向链表中还没有节点时,头结点和尾节点都是当前节点
      tail = node;
    }
  }
 
  /**
   * @Description:将双向链表中的节点移除
   * @param node
   */
  public void removeNode(DoubleLinkedListNode node) {
    DoubleLinkedListNode cur = node;
    DoubleLinkedListNode pre = cur.pre;
    DoubleLinkedListNode post = cur.next;
    // 如果当前节点没有头指针的话,说明它是链表的头结点
    if (pre != null) {
      pre.next = post;
    } else {
      head = post;
    }
    // 如果当前节点没有尾指针的话,说明当前节点是尾节点
    if (post != null) {
      post.pre = pre;
    } else {
      tail = pre;
    }
  }
 
  /**
   * @Description:从缓存Cache中get
   * @param key
   * @return
   */
  public Object get(String key) {
    // 使用hashmap进行查询,时间复杂度为O(1),如果进行链表查询,需要遍历链表,时间复杂度为O(n)
    if (map.containsKey(key)) {
      DoubleLinkedListNode node = map.get(key);
      // 将查询出的节点从链表中移除
      removeNode(node);
      // 将查询出的节点设置为头结点
      setHead(node);
      return node.value;
    }
    // 缓存中没有要查询的内容
    return null;
  }
 
  /**
   * @Description:将key-value存储set到缓存Cache中
   * @param key
   * @param value
   */
  public void set(String key, Object value) {
    if (map.containsKey(key)) {
      DoubleLinkedListNode node = map.get(key);
      node.value = value;
      removeNode(node);
      setHead(node);
    } else {
      // 如果缓存中没有词key-value
      // 创建一个新的节点
      DoubleLinkedListNode newNode = new DoubleLinkedListNode(key, value);
      // 如果链表中的节点数小于链表的初始容量(还不需要进行数据置换)则直接将新节点设置为头结点
      if (size < capacity) {
        setHead(newNode);
        // 将新节点放入hashmap中,用于提高查找速度
        map.put(key, newNode);
        size++;
      } else {
        // 缓存(双向链表)满了需要将"最近醉酒未使用"的节点(尾节点)删除,腾出新空间存放新节点
        // 首先将map中的尾节点删除
        map.remove(tail.key);
        // 移除尾节点并重新置顶尾节点的头指针指向的节点为新尾节点
        removeNode(tail);
        // 将新节点设置为头节点
        setHead(newNode);
        // 将新节点放入到map中
        map.put(key, newNode);
      }
    }
  }
 
  /**
   * @Description: 遍历双向链表
   * @param head
   *            双向链表的 头结点
   */
  public void traverse(DoubleLinkedListNode head) {
    DoubleLinkedListNode node = head;
    while (node != null) {
      System.out.print(node.key + "  ");
      node = node.next;
    }
    System.out.println();
  }
 
  // test
  public static void main(String[] args) {
    System.out.println("双向链表容量为6");
    LRUCache lc = new LRUCache(6);
 
    // 向缓存中插入set数据
    for (int i = 0; i < 6; i++) {
      lc.set("test" + i, "test" + i);
    }
 
    // 遍历缓存中的数据,从左到右,数据越不经常使用
    System.out.println("第一次遍历双向链表:(从头结点遍历到尾节点)");
    lc.traverse(lc.head);
 
    // 使用get查询缓存中数据
    lc.get("test2");
 
    // 再次遍历缓存中的数据,从左到右,数据越不经常使用,并且此次发现刚刚操作的数据节点位于链表的头结点了。
    System.out.println();
    System.out.println("get查询 test2节点后 ,第二次遍历双向链表:");
    lc.traverse(lc.head);
 
    // 再次向缓存中插入数据,发现缓存链表已经满了,需要将尾节点移除
    lc.set("sucess", "sucess");
 
    /**
     * 再次遍历缓存中的数据,从左到右,数据越不经常使用,并且此次发现刚刚set操作时由于链表满了, 就将尾节点test0
     * 移除了,并且将新节点置为链表的头结点。
     */
    System.out.println();
    System.out.println("put插入sucess节点后,第三次遍历双向链表:");
    lc.traverse(lc.head);
  }
}


LRU

1856 字

2018-04-25 20:55 +0800