1.什么是LRU算法?
LRU(最近最少使用),是一种缓存淘汰机制。如下图:
2.LRU的数据结构是啥样的?
Hash表+双向链表.
1)为啥使用Hash表和双向链表?
查找:由上图可知,通过key使用Hash表查找对应节点,只需要O(1)的时间复杂度。
删除和移动:如果是单向链表,删除和移动结点是需要访问前驱元素的,而访问前驱元素就要遍历,所以单向链表的删除和移动是需要的时间复杂度是o(n),但是如果使用双向链表,通过pre就可以访问前边的元素,操作都是指针移动,那么双向链表的删除和移动的时间复杂度也是o(1),因此使用双向链表。
2)一定要使用Hash表+双向链表做LRU吗?
Hash表的缺点是空间浪费,因为有hash冲突的风险,因此Hash表的使用率在70%左右最为合适。双向链表的缺点是相对于单项链表,数组,队列来讲,每个元素除了数据域外还有指针域。如果说我们针对的数据规模很小,小到要比Hash表和双向链表浪费的空间还要小,那么就没有必要使用这样的结构。
3.LRU代码实例讲解
(1)结点元素设计
@interface _DoubleLinkNode : NSObject
{
@package
__weak _DoubleLinkNode *_prev;
__weak _DoubleLinkNode *_next;
id _key;
id _value;
}
@end
// yymemcache ->
@implementation _DoubleLinkNode
@end
这个地方结点结构中为啥要使用_key? 比如 我们外部提供了一个新结点,那么我们可以根据新结点的_key很快的查找到Hash表中是否存在一模一样的结点,是不是很方便。
我们的hash表的结构是以_key为key,以_DoubleLinkNode为value
(3)双向链表结构设计
@interface _DoubleLink() {
@package
NSMutableDictionary *_dic;
_DoubleLinkNode * _head;
_DoubleLinkNode * _tail;
NSUInteger _count;
NSUInteger _capacity;
}
- (void)addNodeToHead:(_DoubleLinkNode *)node;
- (void)moveNodeToHead:(_DoubleLinkNode *)node;
- (void)removeNode:(_DoubleLinkNode *)node;
- (_DoubleLinkNode *)removeTailNode;
@end
1)addNodeToHead:和moveNodeToHead的区别在于前者是无中生有,一个新的结点放到链表的最前边,后者是先从链表中删除,再从头部插入。
2)其中_count和_capacity为啥需要两个属性,_capacity是外部传进来的,指定缓存大小,是个定值,而_count是内部变化的,用来记录链表大小,当插入新的结点时会使用_count和_capacity进行比较,_count<_capacity,方可插入。
3)头结点和尾结点是为了方便链表操作。
@implementation _DoubleLink
- (instancetype)initWithCapacity:(NSUInteger)numItems {
self = [super init];
if (self) {
_capacity = numItems;
// 初始化hash表
_dic = [NSMutableDictionary dictionaryWithCapacity:numItems];
_head = [_DoubleLinkNode new];
_tail = [_DoubleLinkNode new];
_head->_next = _tail;
_tail->_prev = _head;
}
return self;
}
- (void)addNodeToHead:(_DoubleLinkNode *)node {
_dic[node->_key] = node;
_count++;
// 指向head
node->_prev = _head;
node->_next = _head->_next;
_head->_next->_prev = node;
_head->_next = node;
}
- (void)moveNodeToHead:(_DoubleLinkNode *)node {
[self removeNode:node];
[self addNodeToHead:node];
}
// 最近最少使用 -》node 尾部
// 最近最多使用 -〉node 头部
- (void)removeNode:(_DoubleLinkNode *)node {
[_dic removeObjectForKey:node->_key];
_count--;
node->_prev->_next = node->_next;
node->_next->_prev = node->_prev;
}
- (_DoubleLinkNode *)removeTailNode {
//最后一个node
_DoubleLinkNode *node = _tail->_prev;
[self removeNode:node];
return node;
}
@end
这部分没啥好讲的,需要讲的 ,请评论留言啊 。
(4)LRU外部接口设计
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol TinyLRUCachePolicy <NSObject>
- (id)createValue;
@end
@interface LRUCache<__covariant KeyType, __covariant ObjectType> : NSObject
- (instancetype)initWithCapacity:(NSUInteger)numItems;
- (nullable ObjectType)objectForKey:(KeyType<TinyLRUCachePolicy>)aKey;
@end
NS_ASSUME_NONNULL_END
只需要两个方法
1)指定换从大小 initWithCapacity
2)查找缓存 objectForKey
实现方法:
@interface LRUCache() {
_DoubleLink *_lru;
NSUInteger _numItems;
}
@end
@implementation LRUCache
- (instancetype)initWithCapacity:(NSUInteger)numItems {
self = [super init];
if (self) {
_numItems = numItems;
_lru = [[_DoubleLink alloc] initWithCapacity:numItems];
}
return self;
}
- (nullable id)objectForKey:(id<TinyLRUCachePolicy>)aKey {
// O(1) -> YYMCache
_DoubleLinkNode *node = _lru->_dic[aKey];
id value = nil;
if ([aKey respondsToSelector:@selector(createValue)]) {
value = [aKey createValue];
}
if (node) {
// 更新value
node->_value = value;
[_lru moveNodeToHead:node];
} else {
if (_lru->_count == _numItems) {
[_lru removeTailNode];
}
node = [_DoubleLinkNode new];
node->_key = aKey;
node->_value = value;
[_lru addNodeToHead:node];
}
return nil;
}
- (NSString *)description {
if (_numItems == 0) {
return @"<empty cache>";
}
NSMutableString *all = [NSMutableString stringWithString:@"\n|------------LRUCache----------|\n"];
_DoubleLinkNode *node = _lru->_head->_next;
int index = 0;
while (node && node->_key) {
[all appendString:[NSString stringWithFormat:@"|-%d-|--key:--%@--value:--%@--|\n",index, node->_key, node->_value]];
node = node->_next;
index++;
}
return all;
}
@end
查找缓存的时候,查看hash表中是否有缓存。
1)如果存在该结点,就把该缓存结点的value值更新,并且把该结点移动到头部(实际实现中分两步1删除旧的位置2 把该结点插到头部位置)
2)如果是新结点,先查一下缓存是否已经满了,如果满了就先移除尾巴结点,然后在把新结点插到头部。