面试官大大你看,我不仅会 LRU 的原理,还能手写 LRU,录取我吧!

1 谈谈页面置换算法

页面置换算法指示了在将新页面载入内存时,如何选择合适的旧页面进行淘汰。

为什么要使用页面置换算法呢?在操作系统中,内存不够时,我们需要对旧内存进行淘汰。

在计算机体系中,硬盘容量大且可靠,内容也可以固化,不过它有一个致命的缺点,那就是访问速度太慢,对用户来说是无法忍受的,故需要把要使用的内容加载进内存中。

内存的性质与硬盘恰恰相反,它容量有限,不可靠(断电后内容会丢失),但它访问速度却很快。速度越快的地方,单位成本就越高,容量就越小,新的内容被载入,旧的需要被淘汰,LRU 就是这样的一种淘汰算法。

2 LRU 原理

LRU,即 Least Recently Used,是一种常见的页面置换算法,它会淘汰掉最近最久未使用的页面。

我们做这样一个假设,当数据在最近一段时间经常被访问时,那么它可能在之后也会被经常访问。对于经常访问的数据,我们为了能够让其快速命中,将其保存在内存之中。而对于不经常访问的数据,在容量超出限制之后,我们需要将其淘汰。

假设内存只能容纳3个页大小,访问页的次序分别是7 0 1 2 0 3 0 4(每个数字代表一个页面),为实现 LRU 算法,我们需要实现一个大小为3的链表进行辅助。

访问每一个页面时,对链表进行如下的操作:

  1. 访问7:链表大小为1,将7插入链表头部
  2. 访问0:链表大小为2,将0插入链表头部
  3. 访问1:链表大小为3,将1插入链表头部
  4. 访问2:链表大小为3,且2不位于链表之中,故将链表底部的7淘汰,然后将2插入链表头部
  5. 访问0:链表大小为3,且0位于链表之中,将0转移到链表头部
  6. 访问3:链表大小为3,且3不位于链表之中,故将链表底部的1淘汰,然后将3插入链表头部
  7. 访问0:链表大小为3,且0位于链表之中,将0转移到链表头部
  8. 访问4:链表大小为3,且4不位于链表之中,故将链表底部的2淘汰,然后将4插入链表头部

面试官大大你看,我不仅会 LRU 的原理,还能手写 LRU,录取我吧!_第1张图片
在链表头部的是最近访问的页面,而链表底部是最远时间访问的页面。如果我们访问的页面不在内存且链表大小已经到达上限,我们需要移除链表底部的页面。

3 选择什么样的数据结构实现 LRU?

一提到设计算法,我们脑子里一般第一反应就会想到数组与链表这两种数据结构,那么它们真的适合用来实现 LRU 吗?

我们知道 LRU 即需要查询效率高,也需要增删效率高,在这里忍不住吐槽一句,太贪心啦,怎么总想着十全十美呢?(不过我喜欢哈哈哈,做事就要追求完美)

在学习数据结构这门课程的时候,我们都知道数组查询速率快,增删速率慢;链表查询速率慢,增删速率快,这两兄弟怎么水火不容啊,就没有一种数据结构即查询速率快,增删速率也快的吗?

其实是有的,我们可以选择链表+哈希表的数据结构,链表增删速率快,然后在链表的基础之上使用哈希表辅助查询,由于哈希表的查询速率可以达到 O(1) 的时间复杂度,这样便可完美解决链表查询速率慢的问题了!

4 LRU 设计思路

我们使用双向链表和哈希表来设计一个 LRU。

首先,我们先建一个 Node 内部类,该类包含一个 key-value 对,且拥有一个前驱节点与一个后继节点,双向链表在 Node 内部类的基础上得以构建。

LRU 会记录最大容量大小与当前容量大小,根据 LRU 的性质,如果容量满了,需要对双向链表的尾部进行淘汰处理。在每次新增或访问数据时,都需要将对应的 Node 转移至双向链表的头部。

LRU 的读操作

哈希表的键用来存储 Node 的 key,且哈希表的值用来指向对应该 key 的 Node 节点,这样,当我们根据 key 进行查询时,可根据哈希表直接得到该 key 的 Node 节点,其时间复杂度为 O(1)。注意,在查询的同时需要将该 Node 节点转移至双向链表头部。

LRU 的写操作

当需要执行插入操作时,首先会去哈希表中查询一下是否存在该节点,存在则在哈希表中更新节点的值,然后将该节点移动至双向链表头部;若不存在,构建新节点且插入双向链表头部,并将其添加入哈希表中,如果此时 LRU 空间不足,则淘汰双向链表的尾部节点,同时在哈希表中对尾部节点进行移除。

5 完整代码

package test;

import java.util.HashMap;
import java.util.Map;

public class LRUCache {
	
	//内部类
	private class Node {
		String key;
		int value;
		Node pre;
		Node next;
		
		Node(String key, int value) {
			this.key = key;
			this.value = value;
		}
	}
	
	//最大容量
	private int capacity;
	
	//当前大小
	private int count;
	
	//双向链表
	private Node head, tail;
	
	//哈希表
	private Map<String,Node> map;
	
	public LRUCache(int capacity) {
		this.capacity = capacity;
		this.count = 0;
		head = tail = null;
		map = new HashMap();
	}
	
	//读操作
	public int get(String key) {
		Node node = map.get(key);
		//不存在返回-1
		if(node == null) {
			return -1;
		}
		
		//存在就将该Node节点移至双向链表头部
		moveHead(node);
		
		return node.value;
	}
	
	//写操作
	public void put(String key, int value) {
		Node node = map.get(key);
		//该节点不在LRUCache
		if(node == null) {
			//构建新节点
			Node newNode = new Node(key,value);
			//将新节点添加至头部
			moveNewHead(newNode);
			//在哈希表中添加该节点
			map.put(key,newNode);
		} else {
			//直接更新哈希表
			node.value = value;
			//将该节点移至双向链表头部
			moveHead(node);
		}
	}
	
	//正向遍历
	public void findForward() {
		System.out.println("正向遍历:当前容量大小为" + count);
		Node node = head;
		while(node != null) {
			System.out.println("key = " + node.key + " value = " + node.value);
			node = node.next;
		}
	}
	
	//反向遍历
	public void findBackward() {
		System.out.println("反向遍历:当前容量大小为" + count);
		Node node = tail;
		while(node != null) {
			System.out.println("key = " + node.key + " value = " + node.value);
			node = node.pre;
		}
	}
 	
	//将节点移至双向链表头部,该节点已存在
	private void moveHead(Node node) {
		if(head == node) {
			return ;
		} else if(tail == node) {
			tail = tail.pre;
			tail.next = null;
			node.pre = null;
			node.next = head;
			head.pre = node;
			head = node;
		} else {
			node.pre.next = node.next;
			node.next.pre = node.pre;
			node.pre = null;
			node.next = head;
			head.pre = node;
			head = node;
		}
	}
	
	//将节点移至双向链表头部,该节点不存在
	private void moveNewHead(Node newNode) {
		count++;
		if(head == null) {
			head = newNode;
			tail = newNode;
		} else {
			newNode.next = head;
			head.pre = newNode;
			head = newNode;
		}
		
		//容量超出
		if(count > capacity) {
			//将该节点移出哈希表
			String key = tail.key;
			map.remove(key);
			//修改尾节点
			tail = tail.pre;
			tail.next = null;
			count--;
		}
	}
}

你可能感兴趣的:(Java基础)