LRU算法(java实现)

2019/12/22修正

       期末要做课设,回顾之前敲过的代码,用随机序列跑程序的时候出现了问题,在此说明。
       源程序的dataExist()方法,因为要建立临时栈,调用临时栈的初始方法,如果栈容量<1,那么会报错。为何会出现这种情况,原因在于判断的时候没有考虑到栈顶元素与要调用元素是否相等的问题,如果相等,那么不需要做任何变化,否则,需要建立临时栈,调顺序。
       代码嵌入:

	private static void dataExist(Stack s, int a) {
		//如果这个元素就是栈顶元素,那么不需要做任何变化。
		if (a == s.peek()) {
            return;
        }
        ...//下面照旧
	}

算法导入

       哈喽小伙伴们,英俊的我又回来了哈哈哈。今天给大家带来的是操作系统中的页面置换算法LRU算法实现。

       页面置换算法跟虚拟存储器有关,大家都知道,虚拟存储器的功能是内存扩充,但只是逻辑上扩充内容容量,而不是物理上扩充内存,例如台式机装机或笔记本扩容时大家买的内存条,这才是真正意义的物理上的内存扩充。

       然而现在一般的大型游戏大小都上十G,所要求的内存若按传统存储器管理方式一次性全部装入内存方开始运行是不现实的,以及程序运行时存在的局部性现象,应用程序在运行之前没有必要全部装入内存中,而仅须将那些当前要运行的少数页面或段先装入内存便可运行。

       在讲算法之前,先给大家讲讲虚拟存储器的工作原理及相关的页面置换算法,不敢兴趣的可以跳过。

一、虚拟存储器的基本工作情况

       程序运行时,如果它所要访问的页(段)已调入内存,便可以继续执行下去;但如果程序所要访问的页(段)尚未调入内存(称为缺页或缺段),便发出缺页(段)中断请求,此时OS将利用请求调页(段)功能将它们调入内存,以便进程能够继续执行下去。如果此时内存已满,无法装入新的页(段),OS还需要利用页(段)的置换功能,将内存中暂时不用的页(段)调出内存,腾出空间调入要访问的页(段),使程序继续执行下去。那么,页面置换算法的作用就尤为重要了,因为内存就那么多,总有内存满的时候吧。

二、页面置换算法

       下面介绍几种常用的置换算法吧,,因为本节只讲LRU算法的实现,所以其他的带过,了解即可。

1.最佳置换算法(Optimal): 理想化算法,有最好的性能,但实际上却无法实现。选择的被淘汰页面默认是以后永不使用的,或者是在未来时间内不在访问的页面。好像伊甸园啊,想想就好了。。。你总不可能预测未来一段时间内那些页面不被访问吧。可以枪毙了!

2.FIFO算法(先进先出): 最早出现的算法,可能是性能最差的算法。该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰。但它现在不是主角,所以也枪毙了。

3.LRU (Least Recently Used) 算法: 也叫最近最久未使用算法。该算法根据页面调入内存后的使用情况来判断,选择最近最久未使用的页面予以淘汰,也称“向前看”。本次算法的主角,将用java中的栈来实现LRU算法来实现。为什么要用栈呢?待我慢慢分析。

4.LFU(Least Frequently Used)算法: 也叫最少使用算法,选择最近时期使用最少的页面作为淘汰页。一般采用移位寄存器方式。不是主角咱也不详说。

5.CLOCK算法: 改进的LRU算法,这个有兴趣可以去了解下,我也不想讲。

算法分析

       讲个笑话,LRU算法挺贴近生活的。

       比如,抱着一叠书去图书馆学习,如果我正在做备考,我要考研,高数很难,我要刷会题,那么我会把高数上、下,放在书的最上面。如果某个公式我忘了,我随手就可以书来找一找(当然最好不要忘哈)。其他书籍就堆在下面了。过一会,我要复习数据结构,于是我把数据结构书从下面拿出来放在了上面,注意,我肯定是拿我需要的书,其他的原来是咋样就是咋样。再比如,家里的橱柜,经常穿的衣服肯定放在外面,而有些不经常穿的衣服就放在下面的箱子了。

       怎么样,有点概念了吗,最近使用的永远放在最上面,而最久未使用的肯定堆在下面不想理。

       这不就是栈吗?先进来的放在栈底,后进来的放在栈顶。所以为什么要用栈,因为它最像我们设想的那样。

算法设计

       好了,明确了使用哪种数据结构后,就好开始设计相关算法了。有没有一个模板呢,大家可以看一下我的第一篇博客:

Java实现动态优先级算法

       理一理,想清楚了那就gkd:

一、实现栈:

       确定了数据结构是栈了,因为最适合。那好,先用Java写一个用来表示栈的类吧。

       用一个一维数组Storage[]存放栈中元素,两个整形变量表示栈的容量和其中元素的个数,然后还有栈的初始化构造方法,以及带参数的构造方法。同时基本的栈通用方法pop()和push方法这也要有吧。好,目前看起来栈需要的工作就是这些了。把这个栈取个名叫Stack.java。

Stack.java

/**
 * @author GoldenRetriever
 * 利用数组实现栈的功能,包括栈相关元素定义,初始化方法(初始化一个固定大小的栈)
 * 以及push()和pop()方法、peek()方法
 * 新增isExist()、location()、以及重写toString()方法,在LRU算法中将得到使用。
 */
public class Stack {
    /**
     * storage 存放栈中元素的数组
     */
    private int[] storage;
    /**
     * capacity 栈的容量
     */
    private int capacity;
    /**
     * count 栈中元素数量
     */
    private int count;
    /**
     * @return 返回栈中元素的个数
     */
    public int getCount() {
        return count;
    }
    /**
     * @return 返回栈的容量
     */
    public int getCapacity() {
        return capacity;
    }
    /**
     * MyStack() 不带初始容量的构造方法。默认容量为8
     */
    private Stack() {
        this.capacity = 8;
        this.storage=new int[8];
        this.count = 0;
    }
    /**
     *  带初始容量的构造方法
     * @param initialCapacity 初始容量
     */
    public Stack(int initialCapacity) {
        if (initialCapacity < 1) {
            throw new IllegalArgumentException("Capacity too small.");
        }
        this.capacity = initialCapacity;
        this.storage = new int[initialCapacity];
        this.count = 0;
    }
    /**
     * 入栈
     * @param value 入栈值
     */
    public void push(int value) {
        if (count == capacity) {
            System.out.println("栈满");
        }else{
            storage[count++] = value;
        }
    }
    /**
     * @return 返回栈顶元素并出栈
     */
    public int pop() {
        count--;
        if (count == -1) {
            throw new IllegalArgumentException("Stack is empty.");
        }
        return storage[count];
    }
}

二、实现主类:

       用来确定程序的入口。main()中开始获取用户所输入的页面号序列以及进程分配的模块号。
       我们把获取页面号序列封装装成一个方法吧,省的显得main()函数过于庞大,说干就干。

	/**
     * 初始化页面号序列
     * @return a[] 一个整形数组,保存页面号序列
     */
    public static int[] initPageNumber(){
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入页面号序列: (如4 3 2 1 2 3)");
        String s = sc.nextLine();
        //正则表达式代表多个空格
        String regex = " +";
        String [] arr = s.split(regex);
        int[] a = new int[arr.length];
        for(int i = 0; i<arr.length;i++){
            a[i] = Integer.parseInt(arr[i]);
        }
        return a;
    }

那再封装一下模块号获取方法吧,也不在乎这一下。

	/**
     * 初始化进程分配的物理块个数
     * @return myStack 一个有容量无数据的栈
     */
    public static Stack initPhysicalBlock(){
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入进程分配的物理块个数: ");
        int n = sc.nextInt();
        Stack myStack =  new Stack(n);
        System.out.println("栈的容量是" + myStack.getCapacity());
        return myStack;
    }

然后在主函数中调用者两个方法,就可以获取到相应的输入值。

好了,与用户交互的那部分写完了,然后改怎么办呢?

好像应该写这个算法的核心部分,怎么调入调出页面了?

如果刚开始确实难以抓住关键的地方,不妨跟着我来分析分析(盲目分析开始)

三、盲目分析

就拿书上的小栗子来说吧:

假设页面号序列为:
       4, 7, 0, 7, 1, 0, 1, 2, 1, 2, 6
然后进程分配五个物理块。

则调入页面时栈的变化情况如下:

47071012126
2126
1011212
077100001
7700777770
44444444447

说明:前三次访问,没有遇到访问了的界面,所以直接调入到栈中,且栈也未满。从第四次开始,出现访问的页面在栈中存在,那么就要执行相关的算法进行页面调入调出。当栈满时的情况也存在,那么又如何调用呢?

不管如何调用,仔细观察我们可以得出以下规律:

1.栈中无数且未满
       当栈中不存在要访问的页面,且栈未满,直接将页面加入到栈顶。如前面表格中的前三列,栈中都不存在相同的数据,直接加入。

2.栈中无数但满
       当栈中不存在要访问的页面,且栈满时,那么应该将栈底页面pop出,上面元素依次下移一层,然后栈顶加入新页面。

3.栈中有数不管满不满
       当栈中存在要访问的页面时,不管栈满不满,都是找到该页面所在位置, 其上的页面保存下来,在本栈中pop出该页面号,然后把上面的页面下移一层,在栈顶加入该页面。

还有其他的情况吗?应该没有了。写到这,你是不是心里有个大概了。

但接下来的工作还远远没有结束,为什么呢?因为我做的时候到这一步时,本来以为下面的工作应该简单了(其实简单不简单因人而异),但发现缺少的东西还是有点多。

四、敲代码

       到底缺什么东西呢?当我分析到栈调用的规律时,我肯定想敲代码赶紧逐一实现了。
       敲代码真是一件好工作,帮你理清思路,同时也能够发现不足,比如我这里,开始想得挺简单,但是过程中还是遇到了一些问题,虽然不是很难,但是不可忽略。

1.判断栈满方法

       前两种方式中我都需要判断栈是否为满,所以在Stack.java我加入了 isFull() 方法。

	/**
     * @return 判断栈是否满
     */
    public boolean isFull() {
        if (count == capacity){
            return true;
        }else{
            return false;
        }
    }

2.判断页面号是否存在栈中方法

       在这三种调用方式中,都需要判断栈中是否存在该页面,如果存在,还需要返回该页面在栈中位置。所以在Stack.java我又加入了 isExist()location() 方法

	/**
     * @return 判断栈中是否存在一个数
     */
    public boolean isExist(int n, Stack stack){
        for(int i = 0; i<stack.getCount();i++){
            if(n == stack.storage[i]){
                return true;
            }
        }
        return false;
    }
	/**
     * @return 如果栈中存在某个元素,则返回该元素所在栈中位置
     */
    public int  location(int n, Stack stack) {
        for (int i = 0; i< stack.getCount();i++){
            if ( n == stack.storage[i]){
                return i;
            }
        }
        return 0;
    }

3.重写toString()

       因为在页面调入调出过程中,我要随时反映出栈内页面装载情况,所以我又重写了toString()方法。

	@Override
    public String toString() {
        String blank = "";
        for (int i = this.count - 1; i >= 0; i--){
            System.out.print(this.storage[i]);
        }
        return blank;
    }

但是重点还不在这些,在栗子中:

47071012126
2126
1011212
077100001
7700777770
44444444447

如何把上面的页面依次下降一层,这是个问题。所幸可以利用一个临时栈来一进一出操作,保持数据相对位置不发生变化。

	/**
     * 栈中不存在该数据,且栈满,则把栈底数据拿出,其他数据下降一层,栈顶元素加入
     * @param s 栈
     * @param a a[i]代表的那个数据
     * @return 更新后的栈数据
     */
    public static Stack dataNotExistAndStackFull (Stack s, int a){
        Stack tempStack = new Stack(s.getCount()-1 );
        //注意,这里的循环控制条件为临时栈的容量而不是栈中元素的个数
        //取tempStack.getCapacity而不是.getCount(),因为这个栈满了,上面的n-1个数据
        //都要pop出来,此时tempStack.getCount()为0,不能循环0次。
        for (int j = 0; j < tempStack.getCapacity(); j++) {
            tempStack.push(s.pop());
        }
        //把最后一个数据pop出来
        s.pop();
        for (int k = 0; k < tempStack.getCapacity();k++){
            s.push(tempStack.pop());
        }
        //把最新的栈顶元素push进去
        s.push(a);
        return s;
    }

注意代码,先获取临时栈的容量——原栈容量减1,然后循环容量次数, 每次把源栈的栈顶元素pop出,再push进临时栈中,栈顶元素变栈底元素,倒过来存放。然后把栈的最后一个数据pop出来。接下来再把临时栈中的元素pop出来后push到源栈中,将栈底元素变成栈顶元素,再倒过来存放,相当于执行两次操作,负负得正。

算法源码及运行结果

       不知道我有没有讲清楚,方便起见还是贴出我敲的代码来,如果各位有疑问的话,可以敲入到IDEA中运行,仔细揣摩下,或许就能明白啦!

最终版本的: Stack.java

/**
 * @author GoldenRetriever
 * 利用数组实现栈的功能,包括栈相关元素定义,初始化方法(初始化一个固定大小的栈)
 * 以及push()和pop()方法、peek()方法
 * 新增isExist()、location()、以及重写toString()方法,在LRU算法中将得到使用。
 */
public class Stack {
    /**
     * storage 存放栈中元素的数组
     */
    private int[] storage;
    /**
     * capacity 栈的容量
     */
    private int capacity;
    /**
     * count 栈中元素数量
     */
    private int count;
    /**
     * MyStack() 不带初始容量的构造方法。默认容量为8
     */
    private Stack() {
        this.capacity = 8;
        this.storage=new int[8];
        this.count = 0;
    }
    /**
     *  带初始容量的构造方法
     * @param initialCapacity 初始容量
     */
    public Stack(int initialCapacity) {
        if (initialCapacity < 1) {
            throw new IllegalArgumentException("Capacity too small.");
        }
        this.capacity = initialCapacity;
        this.storage = new int[initialCapacity];
        this.count = 0;
    }
    /**
     * 入栈
     * @param value 入栈值
     */
    public void push(int value) {
        if (count == capacity) {
            System.out.println("栈满");
        }else{
            storage[count++] = value;
        }
    }
    /**
     * @return 返回栈顶元素并出栈
     */
    public int pop() {
        count--;
        if (count == -1) {
            throw new IllegalArgumentException("Stack is empty.");
        }
        return storage[count];
    }
    /**
     * @return 返回栈顶元素不出栈
     */
    public int peek() {
        if (count == 0){
            throw new IllegalArgumentException("Stack is empty.");
        }else {
            return storage[count-1];
        }
    }
    /**
     * @return 判断栈是否为空
     */
    public boolean isEmpty() {
        return count == 0;
    }
    /**
     * @return 判断栈是否满
     */
    public boolean isFull() {
        if (count == capacity){
            return true;
        }else{
            return false;
        }
    }
    /**
     * @return 判断栈中是否存在一个数
     */
    public boolean isExist(int n, Stack stack){
        for(int i = 0; i<stack.getCount();i++){
            if(n == stack.storage[i]){
                return true;
            }
        }
        return false;
    }
    /**
     * @return 返回栈中元素的个数
     */
    public int getCount() {
        return count;
    }
    /**
     * @return 返回栈的容量
     */
    public int getCapacity() {
        return capacity;
    }
    /**
     * @return 如果栈中存在某个元素,则返回该元素所在栈中位置
     */
    public int  location(int n, Stack stack) {
        for (int i = 0; i< stack.getCount();i++){
            if ( n == stack.storage[i]){
                return i;
            }
        }
        return 0;
    }
    @Override
    public String toString() {
        String blank = "";
        for (int i = this.count - 1; i >= 0; i--){
            System.out.print(this.storage[i]);
        }
        return blank;
    }
}

最终版本的主函数:Run.java

import java.util.Scanner;
/**
 * @author GoldenRetirever
 * 本类利用定义的Stack类实现LRU(Least Recently Used 最近最久未使用置换算法).
 */
public class Lru {
    /**
     * 获取公共的输入
     */
    private static Scanner sc = new Scanner(System.in);

    public static void main(String[] args) {
        int[] arr = initPageNumber();
        Stack stack = initPhysicalBlock();
        updateStack(stack, arr);
    }
    /**
     * 初始化页面号序列
     * @return a[] 一个整形数组,保存页面号序列
     */
    static int[] initPageNumber(){
        System.out.println("请输入页面号序列: ");
        String s = sc.nextLine();
        //正则表达式代表多个空格
        String regex = " +";
        String [] arr = s.split(regex);
        int[] a = new int[arr.length];
        for(int i = 0; i<arr.length;i++){
            a[i] = Integer.parseInt(arr[i]);
        }
        return a;
    }
    /**
     * 初始化进程分配的物理块个数
     * @return myStack 一个有容量无数据的栈
     */
    private static Stack initPhysicalBlock(){
        System.out.println("请输入进程分配的物理块个数: ");
        int n = sc.nextInt();
        Stack myStack =  new Stack(n);
        System.out.println("栈的容量是" + myStack.getCapacity());
        return myStack;
    }
    /**
     * 栈中不存在该数据,且栈满,则把栈底数据拿出,其他数据下降一层,栈顶元素加入
     * @param s 栈
     * @param a a[i]代表的那个数据
     */
    private static void dataNotExistAndStackFull(Stack s, int a){
        Stack tempStack = new Stack(s.getCount()-1 );
        //注意,这里的循环控制条件为临时栈的容量而不是栈中元素的个数
        //取tempStack.getCapacity而不是.getCount(),因为这个栈满了,上面的n-1个数据
        //都要pop出来,此时tempStack.getCount()为0,不能循环0次。
        for (int j = 0; j < tempStack.getCapacity(); j++) {
            tempStack.push(s.pop());
        }
        //把最后一个数据pop出来
        s.pop();
        for (int k = 0; k < tempStack.getCapacity();k++){
            s.push(tempStack.pop());
        }
        //把最新的栈顶元素push进去
        s.push(a);
    }
    /**
     * 栈中存在该数据,不管栈满不满,则获取该元素所在位置,它上面的元素下降一层,该数据重新加入栈顶
     * @param s 栈
     * @param a a[i]代表的那个数据
     */
    private static void dataExist(Stack s, int a) {
    	//如果这个元素就是栈顶元素,那么不需要做任何变化。
        if (a == s.peek()) {
            return;
        }
        int location = s.location(a, s);
        //临时栈大小
        int tempSize = s.getCount() - location - 1 ;
        Stack tempStack = new Stack(tempSize);
        //数据上面的元素push进新栈
        for(int m = 0; m < tempSize; m++){
            tempStack.push(s.pop());
        }
        //原来的栈s pop掉找到的数据
        s.pop();
        //新栈中的元素pop出来给原来的栈,一进一出。
        for (int m = 0; m < tempSize; m++){
            s.push(tempStack.pop());
        }
        //栈s加入存在的数据到栈顶
        s.push(a);
    }
    /**
     *  根据不同的情况更新栈的排列顺序
     */
    private static void updateStack(Stack s, int[] a){
        //用来计算缺页率
        double existNumber = 0;
        for (int i = 0 ; i < a.length; i++){
            System.out.println("第" + (i+1) + "个页面号" + a[i] + "访问后的栈中元素序列:");
            if (!s.isExist(a[i], s) && !s.isFull()){
                //栈中不存在该数据,且栈未满,把数据加入到栈顶
                s.push(a[i]);
            }else if(!s.isExist(a[i], s) && s.isFull()){
                dataNotExistAndStackFull(s, a[i]);
            }else if(s.isExist(a[i], s)){
                dataExist(s, a[i]);
                existNumber++;
            }
            System.out.println(s.toString());
        }
        double lackGate = (a.length - existNumber) / a.length;
        System.out.println("缺页率为" + lackGate);
    }

}



小栗子运行结果:
在这里插入图片描述
在这里插入图片描述
再举个栗子:

假设页面序号为:
7、0、1、2、0、3、0、4、2、3、0、3、2、1、2、0、1、7、0、1
进程分配的物理块为3

则运行结果为:

在这里插入图片描述
在这里插入图片描述

算法总结

       不知道我讲没讲清楚哈!可能有点长,因为我想尽可能的整理清楚。能够坚持看到这里,十分感谢。如果有错误的地方,希望大家指出。我在想每次写一篇博客都好长啊,写少了怕讲不清,写多了手也累,暂时还没有找到办法,下次再看看吧!

  • 21
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值