如何设计一个LRU Cache?

本文介绍LRUCache的设计思路及Java实现,采用双向链表和哈希表结合的方式,确保所有操作能在O(1)时间内完成。

 如何设计一个LRU Cache?

Google和百度的面试题都出现了设计一个Cache的题目,什么是Cache,如何设计简单的Cache,通过搜集资料,本文给出个总结。

 通常的问题描述可以是这样:

Question:

[1] Design a layer in front of a system which cache the last n requests and the responses to them from the system.

在一个系统之上设计一个Cache,缓存最近的n个请求以及系统的响应。
what data structure would you use to implement the cache in the later to support following operations.

用什么样的数据结构设计这个Cache才能满足下面的操作呢?
[a] When a request comes look it up in the cache and if it hits then return the response from here and do not pass the request to the system
[b] If the request is not found in the cache then pass it on to the system
[c] Since cache can only store the last n requests, Insert the n+1th request in the cache and delete one of the older requests from the cache

因为Cache只缓存最新的n个请求,向Cache插入第n+1个请求时,从Cache中删除最旧的请求。

[d]Design one cache such that all operations can be done in O(1) – lookup, delete and insert.

 Cache简介:

Cache(高速缓存), 一个在计算机中几乎随时接触的概念。CPU中Cache能极大提高存取数据和指令的时间,让整个存储器(Cache+内存)既有Cache的高速度,又能有内存的大容量;操作系统中的内存page中使用的Cache能使得频繁读取的内存磁盘文件较少的被置换出内存,从而提高访问速度;数据库中数据查询也用到Cache来提高效率;即便是Powerbuilder的DataWindow数据处理也用到了Cache的类似设计。Cache的算法设计常见的有FIFO(first in first out)和LRU(least recently used)。根据题目的要求,显然是要设计一个LRU的Cache。

 解题思路:

Cache中的存储空间往往是有限的,当Cache中的存储块被用完,而需要把新的数据Load进Cache的时候,我们就需要设计一种良好的算法来完成数据块的替换。LRU的思想是基于“最近用到的数据被重用的概率比较早用到的大的多”这个设计规则来实现的。

为了能够快速删除最久没有访问的数据项和插入最新的数据项,我们双向链表连接Cache中的数据项,并且保证链表维持数据项从最近访问到最旧访问的顺序。每次数据项被查询到时,都将此数据项移动到链表头部(O(1)的时间复杂度)。这样,在进行过多次查找操作后,最近被使用过的内容就向链表的头移动,而没有被使用的内容就向链表的后面移动。当需要替换时,链表最后的位置就是最近最少被使用的数据项,我们只需要将最新的数据项放在链表头部,当Cache满时,淘汰链表最后的位置就是了。

  注: 对于双向链表的使用,基于两个考虑。首先是Cache中块的命中可能是随机的,和Load进来的顺序无关。其次,双向链表插入、删除很快,可以灵活的调整相互间的次序,时间复杂度为O(1)。

    查找一个链表中元素的时间复杂度是O(n),每次命中的时候,我们就需要花费O(n)的时间来进行查找,如果不添加其他的数据结构,这个就是我们能实现的最高效率了。目前看来,整个算法的瓶颈就是在查找这里了,怎么样才能提高查找的效率呢?Hash表,对,就是它,数据结构中之所以有它,就是因为它的查找时间复杂度是O(1)。

梳理一下思路:对于Cache的每个数据块,我们设计一个数据结构来储存Cache块的内容,并实现一个双向链表,其中属性next和prev时双向链表的两个指针,key用于存储对象的键值,value用户存储要cache块对象本身。

 Cache的接口:

查询:

  • 根据键值查询hashmap,若命中,则返回节点,否则返回null。
  • 从双向链表中删除命中的节点,将其重新插入到表头。
  • 所有操作的复杂度均为O(1)。

插入:

  • 将新的节点关联到Hashmap
  • 如果Cache满了,删除双向链表的尾节点,同时删除Hashmap对应的记录
  • 将新的节点插入到双向链表中头部

更新:

  • 和查询相似

删除:

  • 从双向链表和Hashmap中同时删除对应的记录。

LRU Cache的Java 实现:

 public interface Cache<K extends Comparable, V> {

   V get(K obj);  //查询

   void put(K key, V obj); //插入和更新

   void put(K key, V obj, long validTime);

   void remove(K key); //删除

   Pair[] getAll();

   int size();

}

  public class Pair<K extends Comparable, V> implements Comparable<Pair> {

   public Pair(K key1, V value1) {

      this.key = key1;

      this.value = value1;

   }

   public K key;

   public V value;

   public boolean equals(Object obj) {

      if(obj instanceof Pair) {

         Pair p = (Pair)obj;

         return key.equals(p.key)&&value.equals(p.value);

      }

      return false;

   }

   @SuppressWarnings("unchecked")

   public int compareTo(Pair p) {

      int v = key.compareTo(p.key);

      if(v==0) {

         if(p.value instanceof Comparable) {

            return ((Comparable)value).compareTo(p.value);

         }

      }

      return v;

   }

   @Override

   public int hashCode() {

      return key.hashCode()^value.hashCode();

   }

   @Override

   public String toString() {

      return key+": "+value;

   }

}

 public class LRUCache<K extends Comparable, V> implements Cache<K, V>,

      Serializable {

   private static final long serialVersionUID = 3674312987828041877L;

   Map<K, Item> m_map = Collections.synchronizedMap(new HashMap<K, Item>());

   Item m_start = new Item();      //表头

   Item m_end = new Item();        //表尾

   int m_maxSize;

   Object m_listLock = new Object();        //用于并发的锁

   static class Item {

      public Item(Comparable k, Object v, long e) {

         key = k;

         value = v;

         expires = e;

      }

      public Item() {}

      public Comparable key;        //键值

      public Object value;          //对象

       public long expires;          //有效期

      public Item previous;

      public Item next;

   }

   void removeItem(Item item) {

      synchronized(m_listLock) {

         item.previous.next = item.next;

         item.next.previous = item.previous;

      }

   }

   void insertHead(Item item) {

      synchronized(m_listLock) {

         item.previous = m_start;

         item.next = m_start.next;

         m_start.next.previous = item;

         m_start.next = item;

      }

   }

   void moveToHead(Item item) {

      synchronized(m_listLock) {

         item.previous.next = item.next;

         item.next.previous = item.previous;

         item.previous = m_start;

         item.next = m_start.next;

         m_start.next.previous = item;

         m_start.next = item;

      }

   }

   public LRUCache(int maxObjects) {

      m_maxSize = maxObjects;

      m_start.next = m_end;

      m_end.previous = m_start;

   }

   @SuppressWarnings("unchecked")

   public Pair[] getAll() {

      Pair p[] = new Pair[m_maxSize];

      int count = 0;

      synchronized(m_listLock) {

         Item cur = m_start.next;

         while(cur!=m_end) {

            p[count] = new Pair(cur.key, cur.value);

            ++count;

            cur = cur.next;

         }

      }

      Pair np[] = new Pair[count];

      System.arraycopy(p, 0, np, 0, count);

      return np;

   }

   @SuppressWarnings("unchecked")

   public V get(K key) {

      Item cur = m_map.get(key);

      if(cur==null) {

         return null;

      }

     //过期则删除对象

      if(System.currentTimeMillis()>cur.expires) {

         m_map.remove(cur.key);

         removeItem(cur);

         return null;

      }

      if(cur!=m_start.next) {

         moveToHead(cur);

      }

      return (V)cur.value;

   }

   public void put(K key, V obj) {

      put(key, obj, -1);

   }

   public void put(K key, V value, long validTime) {

      Item cur = m_map.get(key);

      if(cur!=null) {

         cur.value = value;

         if(validTime>0) {

            cur.expires = System.currentTimeMillis()+validTime;

         }

         else {

            cur.expires = Long.MAX_VALUE;

         }

         moveToHead(cur);  //成为最新的对象,移动到头部

         return;

      }

      if(m_map.size()>=m_maxSize) {

         cur = m_end.previous;

         m_map.remove(cur.key);

         removeItem(cur);

      }

      long expires=0;

      if(validTime>0) {

         expires = System.currentTimeMillis()+validTime;

      }

      else {

         expires = Long.MAX_VALUE;

      }

      Item item = new Item(key, value, expires);

      insertHead(item);

      m_map.put(key, item);

   }

   public void remove(K key) {

      Item cur = m_map.get(key);

      if(cur==null) {

         return;

      }

      m_map.remove(key);

      removeItem(cur);

   }

   public int size() {

      return m_map.size();

   }

}

### 设计与实现LRU缓存类 #### 背景介绍 LRU(Least Recently Used)是一种常见的缓存淘汰策略,用于管理固定大小的缓存空间。当缓存满时,会优先移除最近最少使用的数据项[^3]。 #### 数据结构的选择 为了高效地实现LRU缓存,通常采用哈希表(Hash Map)和双向链表(Doubly Linked List)相结合的方式: - **哈希表**:提供O(1)时间复杂度的查找功能,存储键值对以及节点引用。 - **双向链表**:维护访问顺序,新访问的节点会被移动到头部,而尾部则是最久未访问的节点。 这种组合能够确保`get`和`put`操作的时间复杂度均为O(1)[^4]。 --- #### Python实现 以下是基于Python的LRU缓存类实现: ```python class DLinkedNode: def __init__(self, key=0, value=0): self.key = key self.value = value self.prev = None self.next = None class LRUCache: def __init__(self, capacity: int): self.cache = {} self.capacity = capacity self.size = 0 self.head = DLinkedNode() self.tail = DLinkedNode() # 初始化头尾指针 self.head.next = self.tail self.tail.prev = self.head def _add_node(self, node: DLinkedNode): """始终将新的node添加到head之后""" node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _remove_node(self, node: DLinkedNode): """删除指定节点""" prev = node.prev new_next = node.next prev.next = new_next new_next.prev = prev def _move_to_head(self, node: DLinkedNode): """将某个已存在的节点移到头部""" self._remove_node(node) self._add_node(node) def _pop_tail(self): """弹tail前的一个节点""" res = self.tail.prev self._remove_node(res) return res def get(self, key: int) -> int: node = self.cache.get(key, None) if not node: return -1 self._move_to_head(node) return node.value def put(self, key: int, value: int): node = self.cache.get(key, None) if not node: new_node = DLinkedNode(key, value) self.cache[key] = new_node self._add_node(new_node) self.size += 1 if self.size > self.capacity: tail = self._pop_tail() del self.cache[tail.key] self.size -= 1 else: node.value = value self._move_to_head(node) ``` 上述代码实现了LRU缓存的核心逻辑,包括`get`方法、`put`方法以及辅助函数如 `_add_node`, `_remove_node`, 和 `_move_to_head` 等[^4]。 --- #### Java实现 以下是基于Java的LRU缓存类实现: ```java import java.util.HashMap; import java.util.Map; class Node { public int key, val; public Node next, prev; public Node(int k, int v){ this.key = k; this.val = v; } } public class LRUCache { private Map<Integer, Node> map = new HashMap<>(); private final int capacity; private int size; private Node head, tail; public LRUCache(int capacity) { this.capacity = capacity; this.size = 0; head = new Node(-1, -1); tail = new Node(-1, -1); head.next = tail; tail.prev = head; } private void add(Node node){ node.next = head.next; node.prev = head; head.next.prev = node; head.next = node; } private void remove(Node node){ node.prev.next = node.next; node.next.prev = node.prev; } private void moveToHead(Node node){ remove(node); add(node); } private Node popTail(){ Node res = tail.prev; remove(res); return res; } public int get(int key){ Node node = map.get(key); if(node == null) return -1; moveToHead(node); return node.val; } public void put(int key, int value){ Node node = map.get(key); if(node == null){ Node newNode = new Node(key, value); map.put(key, newNode); add(newNode); size++; if(size > capacity){ Node tail = popTail(); map.remove(tail.key); size--; } }else{ node.val = value; moveToHead(node); } } } ``` 此版本同样利用了哈希表和双向链表来实现高效的缓存管理和更新操作。 --- #### C++实现 以下是基于C++的LRU缓存类实现: ```cpp #include <unordered_map> using namespace std; struct ListNode { int key, value; ListNode* prev; ListNode* next; ListNode() : key(0), value(0), prev(nullptr), next(nullptr) {}; ListNode(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}; }; class LRUCache { private: unordered_map<int, ListNode*> cache; int cap, size; ListNode *head, *tail; void addToHead(ListNode* node){ node->next = head->next; node->prev = head; head->next->prev = node; head->next = node; } void removeFromList(ListNode* node){ node->prev->next = node->next; node->next->prev = node->prev; } void moveToHead(ListNode* node){ removeFromList(node); addToHead(node); } ListNode* removeTail(){ ListNode* res = tail->prev; removeFromList(res); return res; } public: LRUCache(int capacity) { cap = capacity; size = 0; head = new ListNode(); tail = new ListNode(); head->next = tail; tail->prev = head; } int get(int key) { if(!cache.count(key)) return -1; ListNode* node = cache[key]; moveToHead(node); return node->value; } void put(int key, int value) { if(cache.count(key)){ cache[key]->value = value; moveToHead(cache[key]); } else{ ListNode* node = new ListNode(key, value); cache[key] = node; addToHead(node); ++size; if(size > cap){ ListNode* removed = removeTail(); cache.erase(removed->key); delete removed; --size; } } } }; ``` 这段代码展示了如何在C++中通过自定义链表节点和标准模板库中的`unordered_map`来构建LRU缓存。 --- #### 总结 以上分别提供了Python、Java和C++三种语言下的LRU缓存实现方案。每种实现均采用了相同的底层原理——即结合哈希表和双向链表完成快速定位与动态调整的功能需求。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值