LRU算法

本文详细介绍了LRU(最近最少使用)算法,这是一种常见的缓存淘汰策略。LRU利用Hash表+双向链表的数据结构,实现快速查找、删除和移动操作。文章通过代码实例解析了LRU节点设计、链表结构以及外部接口,展示了如何在内存满时通过删除最近最少使用的元素来保持缓存效率。
摘要由CSDN通过智能技术生成

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)如果是新结点,先查一下缓存是否已经满了,如果满了就先移除尾巴结点,然后在把新结点插到头部。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

肉丸饭团

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值