设计并实现一个LRU Cache

LRU Cache的设计与实现
本文介绍了Cache的基本概念、工作原理以及替换策略,重点讲解了最常使用(LRU)策略。LRU Cache的实现通常使用双链表和哈希表,保证插入、替换和查找操作的时间复杂度为O(1)。文中还提供了C++实现的思路分析。

一、什么是Cache

1 概念

Cache,即高速缓存,是介于CPU和内存之间的高速小容量存储器。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近CPU的频率。

当CPU发出内存访问请求时,会先查看 Cache 内是否有请求数据。

  • 如果存在(命中),则直接返回该数据;
  • 如果不存在(失效),再去访问内存 —— 先把内存中的相应数据载入缓存,再将其返回处理器。

提供“高速缓存”的目的是让数据访问的速度适应CPU的处理速度,通过减少访问内存的次数来提高数据存取的速度。

2 原理

Cache 技术所依赖的原理是”程序执行与数据访问的局部性原理“,这种局部性表现在两个方面:

  1. 时间局部性:如果程序中的某条指令一旦执行,不久以后该指令可能再次执行,如果某数据被访问过,不久以后该数据可能再次被访问。
  2. 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令或数据通常是顺序存放的。

时间局部性是通过将近来使用的指令和数据保存到Cache中实现。空间局部性通常是使用较大的高速缓存,并将 预取机制 集成到高速缓存控制逻辑中来实现。

3 替换策略

Cache的容量是有限的,当Cache的空间都被占满后,如果再次发生缓存失效,就必须选择一个缓存块来替换掉。常用的替换策略有以下几种:

  1. 随机算法(Rand):随机法是随机地确定替换的存储块。设置一个随机数产生器,依据所产生的随机数,确定替换块。这种方法简单、易于实现,但命中率比较低。

  2. 先进先出算法(FIFO, First In First Out):先进先出法是选择那个最先调入的那个块进行替换。当最先调入并被多次命中的块,很可能被优先替换,因而不符合局部性规律。这种方法的命中率比随机法好些,但还不满足要求。

  3. 最久未使用算法(LRU, Least Recently Used):LRU法是依据各块使用的情况, 总是选择那个最长时间未被使用的块替换。这种方法比较好地反映了程序局部性规律。

  4. 最不经常使用算法(LFU, Least Frequently Used):将最近一段时期内,访问次数最少的块替换出Cache。

4 概念的扩充

如今高速缓存的概念已被扩充,不仅在CPU和主内存之间有Cache,而且在内存和硬盘之间也有Cache(磁盘缓存),乃至在硬盘与网络之间也有某种意义上的Cache──称为Internet临时文件夹或网络内容缓存等。凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为Cache。


二、LRU Cache的实现

Google的一道面试题:

Design an LRU cache with all the operations to be done in O(1)

### LRU 缓存机制的设计实现 LRU(Least Recently Used,最近最少使用)缓存是一种常用的数据结构,用于存储有限数量的键值对。其核心机制是:当缓存容量达到上限时,优先移除最近最少使用的数据。为了高效实现 `get` 和 `put` 操作,通常采用 **哈希表 + 双向链表** 的组合结构。 #### 数据结构选择 - **双向链表**:用于维护缓存中键值对的访问顺序,最近使用的节点放在链表头部,最久未使用的节点放在尾部。 - **哈希表(HashMap)**:用于实现 O(1) 时间复杂度的键值查找,存储键与链表节点的映射关系。 #### 核心操作 1. **get(key)**: - 如果 `key` 不存在于哈希表中,返回 `-1`。 - 如果 `key` 存在,将对应的节点移动到链表头部,返回其值。 2. **put(key, value)**: - 如果 `key` 已存在,更新其值将该节点移动到链表头部。 - 如果 `key` 不存在: - 创建新节点插入链表头部。 - 如果当前缓存已满,删除链表尾部节点(即最久未使用的节点)。 #### Java 实现代码 以下是一个完整的 Java 实现: ```java import java.util.HashMap; class LRUCache { class DLinkedNode { int key; int value; DLinkedNode prev; DLinkedNode next; } private HashMap<Integer, DLinkedNode> cache = new HashMap<>(); private int size; private int capacity; private DLinkedNode head, tail; private void addNode(DLinkedNode node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } private void removeNode(DLinkedNode node) { DLinkedNode prev = node.prev; DLinkedNode next = node.next; prev.next = next; next.prev = prev; } private void moveToHead(DLinkedNode node) { removeNode(node); addNode(node); } private DLinkedNode popTail() { DLinkedNode res = tail.prev; removeNode(res); return res; } public LRUCache(int capacity) { this.size = 0; this.capacity = capacity; head = new DLinkedNode(); tail = new DLinkedNode(); head.next = tail; tail.prev = head; } public int get(int key) { DLinkedNode node = cache.get(key); if (node == null) return -1; moveToHead(node); return node.value; } public void put(int key, int value) { DLinkedNode node = cache.get(key); if (node == null) { DLinkedNode newNode = new DLinkedNode(); newNode.key = key; newNode.value = value; cache.put(key, newNode); addNode(newNode); size++; if (size > capacity) { DLinkedNode tail = popTail(); cache.remove(tail.key); size--; } } else { node.value = value; moveToHead(node); } } } ``` #### 时间复杂度分析 - **get 操作**:O(1),通过哈希表直接查找,再通过双向链表调整位置。 - **put 操作**:O(1),插入或更新节点时,通过哈希表和链表操作实现。 #### 空间复杂度 - **O(capacity)**,缓存最多存储 `capacity` 个键值对。 #### 应用场景 LRU 缓存机制广泛应用于以下场景: - 操作系统中的页面置换算法。 - 数据库查询缓存。 - Web 应用中的热点数据缓存。 - 浏览器历史记录管理。 #### 性能优化 - **发访问控制**:在多线程环境下,可使用 `ConcurrentHashMap` 和同步机制确保线程安全。 - **内存优化**:对于大规模数据缓存,可考虑使用更高效的内存管理方式,如堆外内存或分片缓存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值