广告
返回顶部
首页 > 资讯 > 后端开发 > Python >LRU算法原理解析
  • 694
分享到

LRU算法原理解析

算法原理LRU 2023-01-31 00:01:56 694人浏览 独家记忆

Python 官方文档:入门教程 => 点击学习

摘要

LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。 现代操作系统提供了一种对主存的抽象概念虚拟内存,来对主存进行更好地管理。他将主存看成是一个存储在磁盘上的地址空间的高速

LRULeast Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。

现代操作系统提供了一种对主存的抽象概念虚拟内存,来对主存进行更好地管理。他将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在主存和磁盘之间来回传送数据。虚拟内存被组织为存放在磁盘上的N个连续的字节组成的数组,每个字节都有唯一的虚拟地址,作为到数组的索引虚拟内存被分割为大小固定的数据块虚拟页(Virtual Page,VP),这些数据块作为主存和磁盘之间的传输单元。类似地,物理内存被分割为物理页(Physical Page,PP)

虚拟内存使用页表来记录和判断一个虚拟页是否缓存在物理内存中:

如上图所示,当CPU访问虚拟页VP3时,发现VP3并未缓存在物理内存之中,这称之为缺页,现在需要将VP3从磁盘复制到物理内存中,但在此之前,为了保持原有空间的大小,需要在物理内存中选择一个牺牲页,将其复制到磁盘中,这称之为交换或者页面调度,图中的牺牲页VP4。把哪个页面调出去可以达到调动尽量少的目的?最好是每次调换出的页面是所有内存页面中最迟将被使用的——这可以最大限度的推迟页面调换,这种算法,被称为理想页面置换算法,但这种算法很难完美达到。

为了尽量减少与理想算法的差距,产生了各种精妙的算法,LRU算法便是其中一个。

LRU原理

LRU 算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

根据LRU原理和Redis实现所示,假定系统为某进程分配了3个物理块,进程运行时的页面走向为 7 0 1 2 0 3 0 4,开始时3个物理块均为空,那么LRU算法是如下工作的:

基于哈希表和双向链表的LRU算法实现

如果要自己实现一个LRU算法,可以用哈希表加双向链表实现:

设计思路是,使用哈希表存储 key,值为链表中的节点,节点中存储值,双向链表来记录节点的顺序,头部为最近访问节点。

LRU算法中有两种基本操作:

  • get(key):查询key对应的节点,如果key存在,将节点移动至链表头部。
  • set(key, value): 设置key对应的节点的值。如果key不存在,则新建节点,置于链表开头。如果链表长度超标,则将处于尾部的最后一个节点去掉。如果节点存在,更新节点的值,同时将节点置于链表头部。

LRU缓存机制

LeetCode上有一道关于LRU缓存机制的题目:

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。 写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

进阶:

你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

LRUCache cache = new LRUCache( 2  );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 该操作会使得密钥 2 作废
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得密钥 1 作废
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4

我们可以自己实现双向链表,也可以使用现成的数据结构,python中的数据结构OrderedDict是一个有序哈希表,可以记住加入哈希表的键的顺序,相当于同时实现了哈希表与双向链表。OrderedDict是将最新数据放置于末尾的:

In [35]: from collections import OrderedDict

In [36]: lru = OrderedDict()

In [37]: lru[1] = 1

In [38]: lru[2] = 2

In [39]: lru
Out[39]: OrderedDict([(1, 1), (2, 2)])

In [40]: lru.popitem()
Out[40]: (2, 2)

OrderedDict有两个重要方法:

  • popitem(last=True): 返回一个键值对,当last=True时,按照LIFO的顺序,否则按照FIFO的顺序。
  • move_to_end(key, last=True): 将现有 key 移动到有序字典的任一端。 如果 last 为True(默认)则将元素移至末尾;如果 last 为False则将元素移至开头。

删除数据时,可以使用popitem(last=False)将开头最近未访问的键值对删除。访问或者设置数据时,使用move_to_end(key, last=True)将键值对移动至末尾。

代码实现:

from collections import OrderedDict


class LRUCache:
    def __init__(self, capacity: int):
        self.lru = OrderedDict()
        self.capacity = capacity
        
    def get(self, key: int) -> int:
        self._update(key)
        return self.lru.get(key, -1)
        
    def put(self, key: int, value: int) -> None:
        self._update(key)
        self.lru[key] = value
        if len(self.lru) > self.capacity:
            self.lru.popitem(False)
         
    def _update(self, key: int):
        if key in self.lru:
            self.lru.move_to_end(key)

OrderedDict源码分析

 OrderedDict其实也是用哈希表与双向链表实现的:

class OrderedDict(dict):
    'Dictionary that remembers insertion order'
    # An inherited dict maps keys to values.
    # The inherited dict provides __getitem__, __len__, __contains__, and get.
    # The remaining methods are order-aware.
    # Big-O running times for all methods are the same as regular dictionaries.

    # The internal self.__map dict maps keys to links in a doubly linked list.
    # The circular doubly linked list starts and ends with a sentinel element.
    # The sentinel element never gets deleted (this simplifies the alGorithm).
    # The sentinel is in self.__hardroot with a weakref proxy in self.__root.
    # The prev links are weakref proxies (to prevent circular references).
    # Individual links are kept alive by the hard reference in self.__map.
    # Those hard references disappear when a key is deleted from an OrderedDict.

    def __init__(*args, **kwds):
        '''Initialize an ordered dictionary.  The signature is the same as
        regular dictionaries.  KeyWord argument order is preserved.
        '''
        if not args:
            raise TypeError("descriptor '__init__' of 'OrderedDict' object "
                            "needs an argument")
        self, *args = args
        if len(args) > 1:
            raise TypeError('expected at most 1 arguments, got %d' % len(args))
        try:
            self.__root
        except AttributeError:
            self.__hardroot = _Link()
            self.__root = root = _proxy(self.__hardroot)
            root.prev = root.next = root
            self.__map = {}
        self.__update(*args, **kwds)

    def __setitem__(self, key, value,
                    dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link):
        'od.__setitem__(i, y) <==> od[i]=y'
        # Setting a new item creates a new link at the end of the linked list,
        # and the inherited dictionary is updated with the new key/value pair.
        if key not in self:
            self.__map[key] = link = Link()
            root = self.__root
            last = root.prev
            link.prev, link.next, link.key = last, root, key
            last.next = link
            root.prev = proxy(link)
        dict_setitem(self, key, value)

 由源码看出,OrderedDict使用self.__map = {}作为哈希表,其中保存了key与链表中的节点Link()的键值对,self.__map[key] = link = Link():

class _Link(object):
    __slots__ = 'prev', 'next', 'key', '__weakref__'

  节点Link()中保存了指向前一个节点的指针prev,指向后一个节点的指针next以及key值。

  而且,这里的链表是一个环形双向链表,OrderedDict使用一个哨兵元素root作为链表的headtail

   self.__hardroot = _Link()
   self.__root = root = _proxy(self.__hardroot)
    root.prev = root.next = root

  由__setitem__可知,向OrderedDict中添加新值时,链表变为如下的环形结构:

         next             next             next
   root <----> new node1 <----> new node2 <----> root
         prev             prev             prev

 root.next为链表的第一个节点,root.prev为链表的最后一个节点。

  由于OrderedDict继承自dict,键值对是保存在OrderedDict自身中的,链表节点中只保存了key,并未保存value

  如果我们要自己实现的话,无需如此复杂,可以将value置于节点之中,链表只需要实现插入最前端与移除最后端节点的功能即可:

from _weakref import proxy as _proxy


class Node:
    __slots__ = ('prev', 'next', 'key', 'value', '__weakref__')


class LRUCache:

    def __init__(self, capacity: int):
        self.__hardroot = Node()
        self.__root = root = _proxy(self.__hardroot)
        root.prev = root.next = root
        self.__map = {}
        self.capacity = capacity
        
    def get(self, key: int) -> int:
        if key in self.__map:
            self.move_to_head(key)
            return self.__map[key].value
        else:
            return -1
         
    def put(self, key: int, value: int) -> None:
        if key in self.__map:
            node = self.__map[key]
            node.value = value
            self.move_to_head(key)
        else:
            node = Node()
            node.key = key
            node.value = value
            self.__map[key] = node
            self.add_head(node)
            if len(self.__map) > self.capacity:
                self.rm_tail()
        
    def move_to_head(self, key: int) -> None:
        if key in self.__map:
            node = self.__map[key]
            node.prev.next = node.next
            node.next.prev = node.prev
            head = self.__root.next
            self.__root.next = node
            node.prev = self.__root
            node.next = head
            head.prev = node
    
    def add_head(self, node: Node) -> None:
        head = self.__root.next
        self.__root.next = node
        node.prev = self.__root
        node.next = head
        head.prev = node
    
    def rm_tail(self) -> None:
        tail = self.__root.prev
        del self.__map[tail.key]
        tail.prev.next = self.__root
        self.__root.prev = tail.prev

node-lru-cache

在实际应用中,要实现LRU缓存算法,还要实现很多额外的功能。

有一个用javascript实现的很好的node-lru-cache包:

var LRU = require("lru-cache")
  , options = { max: 500
              , length: function (n, key) { return n * 2 + key.length }
              , dispose: function (key, n) { n.close() }
              , maxAge: 1000 * 60 * 60 }
  , cache = new LRU(options)
  , otherCache = new LRU(50) // sets just the max size

cache.set("key", "value")
cache.get("key") // "value"

这个包不是用缓存key的数量来判断是否要启动LRU淘汰算法,而是使用保存的键值对的实际大小来判断。选项options中可以设置缓存所占空间的上限max,判断键值对所占空间的函数length,还可以设置键值对的过期时间maxAge等,有兴趣的可以看下。

参考链接

  • LRU原理和Redis实现——一个今日头条的面试题

--结束END--

本文标题: LRU算法原理解析

本文链接: https://www.lsjlt.com/news/182435.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • LRU算法原理解析
    LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。 现代操作系统提供了一种对主存的抽象概念虚拟内存,来对主存进行更好地管理。他将主存看成是一个存储在磁盘上的地址空间的高速...
    99+
    2023-01-31
    算法 原理 LRU
  • PHP实现LRU算法的原理详解
    1.概念 LRU : 最近最少使用算法 2.代码 <php class Node { public $preKey = null; //链表前一个节点 publ...
    99+
    2022-11-13
  • LRU算法怎么理解
    本篇内容介绍了“LRU算法怎么理解”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!01、前言我们常用缓存提升数据查询速度,由于缓存容量有限,当...
    99+
    2023-06-16
  • LRU算法及Apache LRUMap源码实例解析
    目录1. 什么是LRU1.1 自定义实现LRU的要求1.2 Apache LRUMap示例1.2.1 pom依赖1.2.2 demo2. 源码解析2.1 设计2.2 数据结构2.3 ...
    99+
    2022-11-12
  • 如何理解Oracle数据库LRU算法中的LRU链、脏块与脏LRU链
    如何理解Oracle数据库LRU算法中的LRU链、脏块与脏LRU链,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。概述所谓的LRU(Least ...
    99+
    2022-10-19
  • Java之理解Redis回收算法LRU案例讲解
    如何通俗易懂的理解LRU算法? 1.LRU是什么? LRU全称Least Recently Used,也就是最近最少使用的意思,是一种内存管理算法,最早应用于Linux操作系统。 L...
    99+
    2022-11-12
  • vue.js diff算法原理详细解析
    目录diff算法的概念虚拟Domh函数diff对比规则patchpatchVnodeupdateChildren总结diff算法的概念 diff算法可以看作是一种对比算法,...
    99+
    2022-11-13
  • Label Propagation算法原理示例解析
    目录1. 概述2. Label Propagation算法2.1. Label Propagation算法概述2.2. Label Propagation算法原理3.3. Label...
    99+
    2023-02-01
    Label Propagation算法 Label Propagation
  • 级联分类器算法原理解析
    目录一、人脸检测算法分类二、Haar分类器算法2.1 人脸检测的大概流程2.2 Haar-like特征2.3 Adaboost算法2.4 弱分类器的构建2.5 强分类器的构造一、人脸...
    99+
    2022-11-13
  • Java中Redis回收算法LRU的示例分析
    这篇文章给大家分享的是有关Java中Redis回收算法LRU的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。如何通俗易懂的理解LRU算法?1.LRU是什么?LRU全称Least Recently Used...
    99+
    2023-06-20
  • java LRU算法及Apache LRUMap源码实例分析
    本篇内容主要讲解“java LRU算法及Apache LRUMap源码实例分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“java LRU算法及Apache LRUMap源...
    99+
    2023-06-21
  • LRU LFU TinyLFU缓存算法实例详解
    目录简介一、LRU和LFU算法LRU算法LFU算法小结:二、TinyLFU三、Window-TinyLFU简介 前置知识 知道什么是缓存 听完本节公开课,你可以收获 掌握朴素LRU、...
    99+
    2022-11-11
  • JavaScript实现LRU算法的示例详解
    目录LRU简介如何实现实现思路缺陷双向链表+哈希表双向链表实现思路不知道屏幕前的朋友们,有没有和我一样,觉得LRU算法原理很容易理解,实现起来却很复杂。 明明一个map就能解决,标准...
    99+
    2023-05-17
    JavaScript实现LRU算法 JavaScript LRU算法 JavaScript LRU
  • react diff 算法实现思路及原理解析
    目录事例分析diff 特点diff 思路实现 diff 算法修改入口文件实现 React.Fragment我们需要修改 children 对比前面几节我们学习了解了 react 的渲...
    99+
    2022-11-13
  • 使用Redis实现令牌桶算法原理解析
    在限流算法中有一种令牌桶算法,该算法可以应对短暂的突发流量,这对于现实环境中流量不怎么均匀的情况特别有用,不会频繁的触发限流,对调用方比较友好。 例如,当前限制10qps,大多数情况...
    99+
    2022-11-12
  • python实现CSF地面点滤波算法原理解析
    目录一、算法原理二、读取las点云三、算法源码四、结果展示五、CloudCompare实现一、算法原理 布料模拟滤波处理流程: 1)利用点云滤波算法或者点云处理软件滤除异常点; 2)...
    99+
    2022-11-12
  • JavaHashMap算法原理详细讲解
    目录前言一、HashMap概述二、HashMap的数据结构三、HashMap源码分析3.1 关键属性3.2 构造方法3.3 存储数据3.4 调整大小3.5 数据读取3.6 HashM...
    99+
    2023-02-08
    Java HashMap原理 Java HashMap
  • Java三种移位运算符原理解析
    Java中有三种移位运算符:左移运算符()和无符号右移运算符(>>>)。1. 左移运算符():将一个数的所有位向右移动指定的位数,高...
    99+
    2023-08-17
    Java
  • Spring注解@Scope原理及用法解析
    Spring注解@Scope用于指定bean的作用域,即bean的生命周期。@Scope注解有以下几个常用的取值:1. single...
    99+
    2023-08-17
    Spring
  • 如何理解TCP协议、算法和原理
    如何理解TCP协议、算法和原理,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。首先,我们需要知道,我们程序的数据首先会打到TCP的Segment中,然后TCP的S...
    99+
    2023-06-03
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作