基于单链表的 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;
    }
}

参考文献

  • 页面置换算法简介
  • LRU 算法原理与实践
  • 设计并实现一个 LRU Cache
  • 如何设计实现一个 LRU Cache

你可能感兴趣的:(基于单链表的 LRU 算法实现)