LRU:Least Recently Used,最少最近使用算法。
算法描述
leetcode 146 [LRU缓存机制] 有关于这个数据结构的设计的描述,下面我直接copy一下,实现LRUCache类:
LRUCache(int capacity)以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key)如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。void put(int key, int value)如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该逐出最久未使用的关键字
注意:put & get方法的都需要是O(1) 的时间复杂度
LRUCache lRUCache = new LRUCache(2);lRUCache.put(1, 1); // 缓存是 cache = [1=>1]lRUCache.put(2, 2); // 缓存是 cache = [2=>2, 1=>1]lRUCache.get(1); // 返回 1,访问了1就会将1提到队列前头 cache = [1=>1, 2=>2],lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 cache = [3=>3, 1=>1]lRUCache.get(2); // 返回 -1 (未找到)lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 cache = [4=>4, 3=>3]lRUCache.get(1); // 返回 -1 (未找到)lRUCache.get(3); // 返回 3 cache = [3=>3, 4=>4]lRUCache.get(4); // 返回 4 cache = [4=>4, 3=>3]
算法设计
上面我们知道有一个条件是put & get方法的时间复杂度要是O(1),那么构建的cache缓存需要满足以下条件
1、cache中的元素,应该是有时序的,来区分哪些是最近访问的/较久未访问的数据,容量满的时候还需要删掉末尾的元素腾出空间。
2、在cache需要快速找到某个key是否存在并且获得对应的value O(1),这个点我们很容易想到hashMap。
3、每次访问cache中的某个key,需要将这个元素变成队列最开头的位置,也就是说cache要支持在任意位置快速的删除/插入元素 O(1)
我们知道hashMap查找快,但是数据没有固定顺序;链表有顺序,插入删除快,但查找慢。结合一下基本就满足了我们上面的需求,一般也是采用这个结构实现的即哈希链表:LinkedHashMap
代码实现:双向链表+hashMap
双向链表部分
/** 双链表Node*/class DoubleLinkedListNode {public key: number;public value: number;public next: any = null;public prev: any = null;constructor(key: number, value: number) {this.key = key;this.value = value;}}class DoubleLinkedList {// 链表头节点, Number.POSITIVE_INFINITY -> 正无穷大public head = new DoubleLinkedListNode(Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY);// 链表尾节点, Number.NEGATIVE_INFINITY -> 负无穷大public tail = new DoubleLinkedListNode(Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY);private size: number = 0; //链表长度constructor() {this.head.next = this.tail;this.tail.prev = this.head;}public addFirstNode(node: DoubleLinkedListNode): any {// node链起来node.prev = this.head;node.next = this.head.next;// 处理nodethis.head.next.prev = node;this.head.next = node;// size ++this.size += 1;}public remove(node: any): void {node.prev.next = node.next;node.next.prev = node.prev;// size --this.size -= 1;}public removeLastNode() {// 空链表上,其实这里也可以通过size判断if (this.head.next === this.tail) return null;const last = this.tail.prev;this.remove(last);return last;}public getSize(): number {return this.size;}}
LRUCache实现部分
class LRUCache {private hashMap = new Map();private cache: DoubleLinkedList = new DoubleLinkedList();private capacity: number; //容量constructor(capacity: number) {this.capacity = capacity;}// key访问提升到最近使用private getRecently(key: number): void {const node: DoubleLinkedListNode = this.hashMap.get(key);// 移除nodethis.cache.remove(node);// 放到队头this.cache.addFirstNode(node);}// 移除最久未使用的元素private removeLastRecently(): void {const lastNode: any = this.cache.removeLastNode();// map中也要删掉改元素哦this.hashMap.delete(lastNode?.key);}// 添加最近使用的元素private addRecently(key: number, value: number): void {const node: DoubleLinkedListNode = new DoubleLinkedListNode(key, value);this.cache.addFirstNode(node);this.hashMap.set(key, node);}/** 判断hashMap是否有该元素* 没有: 返回 -1* 有: 将元素提升到链表头,返回对应的元素*/public get(key: number) {if (this.hashMap.has(key)) {this.getRecently(key);return this.hashMap.get(key).value;} else {return -1;}}/** 判断是否超过容器的极限了* 有: 将链表最后一个元素移除,后再将该元素放在链表头* 没有: 放到链表头*/public put(key: number, value: number) {if (this.cache.getSize() === this.capacity) {this.removeLastRecently();}this.addRecently(key, value);console.log("map", this.hashMap);}}
验证部分的代码
const lRUCache = new LRUCache(2);console.log("-----");lRUCache.put(1, 1); // 缓存是 {1=1}lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}console.log("-----");lRUCache.get(1); // 返回 1lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}console.log("-----");lRUCache.get(2); // 返回 -1 (未找到)lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}console.log("-----");lRUCache.get(1); // 返回 -1 (未找到)lRUCache.get(3); // 返回 3lRUCache.get(4); // 返回 4
代码实现:迭代器+hashMap
ES6中的Map本身是可迭代的,并且key有序,这样我们可以利用这个特性替代双向链表的功能
/** map的key是一个有序(迭代器)* 可以替代LinkedListHashMap的功能*/class LRUCacheMap<T, U> {private cache: Map<T, U> = new Map();private capacity: number;constructor(capacity: number) {this.capacity = capacity;}/** 判断cache是否有该元素* 没有: 返回 null* 有: 将元素提升到链表头,返回对应的元素*/public get(key: T): U | null {if (this.cache.get(key)) {// 先删除该元素,再提升到头部const value: U = this.cache.get(key) as U;this.cache.delete(key);this.cache.set(key, value);return value;} else {return null;}}// 获取map中最久没有访问一个元素(最先插入)private getFirstKey() {const keys = this.cache.keys();return keys.next().value;}/** 判断是否超过容器的极限了* 有: 将map最后一个元素移除,后再将该元素放在链表头* 没有: 放到链表头*/public put(key: T, value: U) {// 已经存在的删除: map的key如果是引用类型并不会被覆盖的if (this.cache.has(key)) {this.cache.delete(key);}if (this.capacity === this.cache.size) {const lastKey = this.getFirstKey();this.cache.delete(lastKey);}this.cache.set(key, value);}toString() {console.table(this.cache);}}
const lruCacheMap = new LRUCacheMap(4);lruCacheMap.put(2, 2); // 入 2,剩余容量3lruCacheMap.put(3, 3); // 入 3,剩余容量2lruCacheMap.put(4, 4); // 入 4,剩余容量1lruCacheMap.put(5, 5); // 入 5,已满 从头至尾 2-3-4-5lruCacheMap.toString();lruCacheMap.put(4, 4); // 入4,已存在 ——> 置队尾 2-3-5-4lruCacheMap.toString();lruCacheMap.put(1, 1); // 入1,不存在 ——> 删除队首 插入1 3-5-4-1lruCacheMap.toString();lruCacheMap.get(3); // 获取3,刷新3——> 置队尾 5-4-1-3lruCacheMap.toString();

