基于单链表的 LRU 算法实现

本文首发于 LOGI'S BLOG,由作者转载。

在使用页进行内存管理的操作系统中,当新页进入内存且内存已满时,需要 页面置换算法 决定哪个页应该被替换。

缺页中断

当正在运行的程序访问一个被映射于虚拟内存却未被加载到物理内存中的内存页时,缺页中断便发生了。由于真实物理内存远小于虚拟内存,缺页中断无法避免。缺页中断发生时,操作系统不得不将某个已加载的内存页替换为新的正被需要的页面。不同页面置换算法决定被替换页的方式各不相同,但所有算法都致力于减少缺页中断次数。

常见页面置换算法

最佳置换 Optimal Page Replacement(OPT)

在该算法中,那些将来最长时间不会被使用的页面会被替换。最佳置换算法最大程度地减少了缺页中断,但它不可实现,因为操作系统无法得知将来的请求,它的最大意义在于为评估其他页面置换算法的性能提供指标。

先进先出 First In First Out(FIFO)

这是最简单的置换算法。在该算法中,操作系统以队列形式持续跟踪位于内存中的页面,年龄最长的页面位于队列最前面。发生缺页中断时,队列最前面的页面将被替换。

Belady’s Anomaly 毕雷迪异常:采用 FIFO 算法时,如果对一个进程未分配它所要求的全部页面,有时就会出现分配的页面数增多,缺页率反而提高(时高时低)的异常现象。

原因:FIFO 算法的置换特征与进程访问内存的动态特征是矛盾的,即被置换的页面并不是进程不会访问的。

最少使用 Least Frequently Used(LFU)

该算法为每个页面设置一个访问计数器,页面被访问时,计数加 1,缺页时,置换计数最小的页面。其缺陷是开始时频繁使用,但以后不再使用的页面很难被置换。此外新加入的页面因访问次数较少,可能很快又被置换出去。

最近最少使用 Least Recently Used (LRU)

该算法选择最长时间没有被引用的页面进行置换。具体做法是,记录每个逻辑页面的上一次访问时间,发生缺页中断时,淘汰从上一次使用到此刻间隔最长的页面。根据 程序执行与数据访问的局部性原理,如果某些页面长时间未被访问,则它们在将来有很大可能仍然长时间不被访问,所以 LRU 的性能最接近于 OPT。

基于单链表实现 LRU

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

获取数据 get(key):如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。

写入数据 put(key, value):如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

// 示例:
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

思路:维护一个有序单链表,规定越靠近表尾的结点是越早访问的。当访问新页面 get(key) 时,从表头遍历链表,如果该页面已存在,则将其从原来位置删除,然后插入到表头。加载页面 put(key,value) 时,若该页面不存在且链表未满,则将页面插入表头。否则,删除表尾结点,再将新页面插入表头。

存储密度:2/3

空间复杂度:O(1)

时间复杂度:遍历链表 O(n),插入删除 O(1),因此总的时间复杂度 O(n)

package com.logi.algorithm;

/**
 * @author LOGI
 * @version 1.0
 * @date 2019/7/9 18:02
 */
public class LRUWithSinglyLinkedList {
    LRUNode head;
    int capacity;
    int size;

    public LRUWithSinglyLinkedList(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.head = new LRUNode();
    }

    public static void main(String[] args) {
        LRUWithSinglyLinkedList lru = new LRUWithSinglyLinkedList(2);
        lru.put(1, 1);
        System.out.println(lru + ", after put(1,1)");
        lru.put(2, 2);
        System.out.println(lru + ", after put(2,2)");
        lru.get(1);
        System.out.println(lru + ", after get(1)");
        lru.put(3, 3);
        System.out.println(lru + ", after put(3,3)");
        lru.get(2);
        System.out.println(lru + ", after get(2)");
        lru.put(4, 4);
        System.out.println(lru + ", after put(4,4)");
        lru.get(1);
        System.out.println(lru + ", after get(1)");
        lru.get(3);
        System.out.println(lru + ", after get(3)");
        lru.get(4);
        System.out.println(lru + ", after get(4)");
    }

    @Override
    public String toString() {
        LRUNode current = this.head.next;
        StringBuilder list = new StringBuilder();
        while (current != null) {
            list.append(current.value);
            if (current.next != null) {
                list.append("->");
            }
            current = current.next;
        }
        return list.toString();
    }

    /**
     * 根据 key 查找 value,如果已存在将其移至表头并返回,否则返回 -1
     *
     * @param key
     * @return
     */
    public int get(int key) {
        LRUNode prev = this.getPrev(key);
        if (prev != null) {
            LRUNode current = prev.next;
            this.delete(prev);
            this.insert(current);
            return current.value;
        } else {
            return -1;
        }
    }

    /**
     * 加载页面,如果缓存已满,删掉表尾
     *
     * @param key
     * @param value
     */
    public void put(int key, int value) {
        LRUNode current = new LRUNode(key, value);
        LRUNode prev = this.getPrev(key);
        if (prev == null) {
            if (this.size == this.capacity) {
                this.delete(this.getTailPrev());
            }
            this.insert(current);
        }
    }

    /**
     * 获取 tail 前驱
     *
     * @return
     */
    public LRUNode getTailPrev() {
        LRUNode current = this.head;
        if (current.next == null) {
            return null;
        }
        while (current.next.next != null) {
            current = current.next;
        }
        return current;
    }

    /**
     * 根据 key 获得前驱
     *
     * @param key
     * @return
     */
    public LRUNode getPrev(int key) {
        LRUNode prev = this.head;
        while (prev != null) {
            if (prev.next != null && prev.next.key == key) {
                break;
            }
            prev = prev.next;
        }
        return prev;
    }

    /**
     * 根据前驱删除
     *
     * @param prev
     */
    public void delete(LRUNode prev) {
        prev.next = prev.next.next;
        this.size--;
    }

    /**
     * 插入到表头
     *
     * @param current
     */
    public void insert(LRUNode current) {
        current.next = this.head.next;
        this.head.next = current;
        this.size++;
    }
}

class LRUNode {
    LRUNode next;
    int key;
    int value;

    public LRUNode() {
        this.key = this.value = 0;
        this.next = null;
    }

    public LRUNode(int key, int value) {
        this.key = key;
        this.value = value;
        this.next = null;
    }
}

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容

  • 8.1虚拟存储的需求背景 虚拟内存是非连续内存分配的一个延续,非连续内存分配在存储空间内可以连续也可以不连续。虚拟...
    龟龟51阅读 5,852评论 2 6
  • 一、实验目的及基本要求 设计和实现最佳置换算法、先进先出置换算法、最近最久未使用置换算法、页面缓冲置换算法,改进C...
    二磊Jerly阅读 1,167评论 0 0
  • 1. 虚拟存储器的基本概念 分析常规存储器管理不足的原因: 1)常规存储器管理方式的特征 一次性:作业在运行前一...
    Whocare_2f87阅读 1,087评论 0 0
  • 1. 虚拟存储器的基本概念 分析常规存储器管理不足的原因: 1)常规存储器管理方式的特征 一次性:作业在运行前一...
    盆栽木只阅读 1,317评论 0 0
  • 一、虚拟存储技术 所谓虚拟存储技术是指:当进程运行时,先将其一部分装入内存,另一部分暂留在磁盘,当要执行的指令或访...
    yjaal阅读 3,521评论 0 6