#P2321. 第1题-操作系统作业
-
1000ms
Tried: 663
Accepted: 165
Difficulty: 5
所属公司 :
华为
时间 :2024年5月15日-暑期实习
-
算法标签>链表
第1题-操作系统作业
题面描述:
给定一个缓存最多存放N页,输入K次操作,操作包括插入(A)、查询(Q)和删除(D)。插入时如果页面已存在则刷新访问时间,否则插入新页面,若超过容量N则淘汰最久未使用页面。查询和删除操作也会刷新页面的访问时间。最后输出缓存内页面编号,按LRU(最近最少使用)顺序排列。
思路
双向链表 + 哈希表
这是一道经典原题146. LRU缓存 。
每次查询或插入 key 成功后,都会更新 key 的优先级,这个优先级会影响大小满了之后,如何弹出旧元素,以有空间插入新元素。
朴素情况是,我们维护一个链表,每次遍历链表找到 key ,单次查找的时间复杂度是 O(n) ,这会超时,考虑优化单次查找的时间。
单次查找这部分可以用哈希表来维护,这样插入和删除都是O(1)
此外,我们还需要维护一个优先级,这个优先级可以用一个链表维护。
我们需要弹出 key ,所以要从链表中插入删除节点,这就需要链表的前后节点,所以用双向链表维护。
时间复杂度:O(n)
题解详解
这道题的目标是实现一个支持 LRU(Least Recently Used)缓存淘汰算法 的缓存系统。LRU 算法旨在淘汰最近最少使用的元素,当缓存空间已满时,将最久未被使用的页面移出缓存,并保持常数时间复杂度完成插入、查询和删除操作。
为了高效地实现上述功能,通常使用以下两种数据结构的结合:
- 哈希表(unordered_map):用于 O(1) 时间复杂度查找某个页面是否存在缓存中。
- 双向链表(doubly linked list):用于维护页面的访问顺序。最近使用的页面放在链表头部,最久未使用的页面放在链表尾部。
通过将这两种数据结构结合,能够在 O(1) 的时间内完成插入、删除、和查询操作。
双向链表的作用
在 LRU 缓存中,每次查询或插入页面时,需要更新页面的访问顺序。最近访问的页面应当移到链表头部,而最久未使用的页面会移到链表尾部。一旦缓存满了,需要删除最久未使用的页面(即链表尾部的节点)。
双向链表便于我们在 O(1) 时间内完成以下操作:
- 在链表头部插入新的页面。
- 将某个已存在的页面移动到链表头部。
- 当缓存满时,删除链表尾部的页面。
哈希表的作用
哈希表用于 O(1) 时间内查找页面是否在缓存中。哈希表的键是页面的编号,值是该页面在双向链表中的节点指针。
题解流程
- 初始化:创建一个可以容纳
N个页面的缓存(双向链表),并配合哈希表来加速页面的查询操作。 - 插入操作
A:如果页面已经存在缓存中,则将该页面移到链表头部;如果不存在且缓存未满,则直接插入链表头部;如果缓存已满,则先删除最久未使用的页面(链表尾部),再插入新页面。 - 查询操作
Q:如果页面存在于缓存中,则将其移动到链表头部;如果不存在,则不进行任何操作。 - 删除操作
D:如果页面存在于缓存中,则将其从链表和哈希表中删除;如果不存在,则忽略。 - 输出:最后按照缓存中的 LRU 顺序,输出所有缓存中的页面编号。
代码
Python
class DoubleLinkedNode:
def __init__(self, key=0, value=0):
# 初始化双向链表节点
# key 为节点的键值
# value 为节点的值
self.key = key
self.value = value
self.prev = None # 前驱节点
self.next = None # 后继节点
class LRUCache:
def __init__(self, cap):
# 初始化 LRU 缓存
self.cache = dict() # 用哈希表存储 key-value 键值对
self.head = DoubleLinkedNode() # 创建头部节点
self.tail = DoubleLinkedNode() # 创建尾部节点
self.head.next = self.tail # 头尾相连
self.tail.prev = self.head
self.cap = cap # 缓存容量
self.size = 0 # 当前缓存大小
def output(self):
# 输出当前缓存中的所有值
temp = self.head.next
ans = []
while temp and temp != self.tail:
ans.append(temp.value)
temp = temp.next
print(*ans)
def get(self, key):
# 获取指定 key 的值
if key not in self.cache:
# 如果 key 不在缓存中,返回 -1
return -1
node = self.cache[key]
# 将节点移到链表头部
self.moveToHead(node)
return node.value
def put(self, key, value):
# 设置指定 key 的值
if key not in self.cache:
# 如果 key 不在缓存中
node = DoubleLinkedNode(key, value)
self.cache[key] = node
# 将节点添加到链表头部
self.addToHead(node)
self.size += 1
if self.size > self.cap:
# 如果缓存容量超限,删除尾部节点
removed = self.removeTail()
self.cache.pop(removed.key)
self.size -= 1
else:
# 如果 key 在缓存中
node = self.cache[key]
node.value = value
# 将节点移到链表头部
self.moveToHead(node)
def addToHead(self, node):
# 将节点添加到链表头部
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def removeNode(self, node):
# 从链表中删除节点
node.prev.next = node.next
node.next.prev = node.prev
def moveToHead(self, node):
# 将节点移到链表头部
self.removeNode(node)
self.addToHead(node)
def removeTail(self):
# 删除链表尾部节点
node = self.tail.prev
self.removeNode(node)
return node
C++
#include <sstream>
#include <map>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <queue>
#include <stack>
using namespace std;
// 定义链表节点结构体
struct node{
int val; // 节点值
struct node * next; // 指向下一个节点的指针
struct node * pre; // 指向上一个节点的指针
};
// 解决问题的主函数
int solu4(){
int n, k; // n表示链表最大长度, k表示操作次数
cin>> n>>k; // 读入n和k
// 创建链表头尾节点
struct node* head = new(struct node);
struct node* back = new(struct node);
head->next = back; // 头节点指向尾节点
back->pre = head; // 尾节点指向头节点
int len= 0; // 当前链表长度
char c; // 操作类型
int val; // 操作值
// 处理k次操作
while(k--){
cin>>c>>val; // 读入操作类型和值
// 如果是添加操作
if(c == 'A') {
bool a = false; // 标记是否找到相同值的节点
struct node *tmp = head->next; // 从头节点的下一个节点开始遍历
while (tmp != back) { // 遍历到尾节点前
if (val == tmp->val) { // 如果找到相同值的节点
// 将该节点从链表中删除
tmp->pre->next = tmp->next;
tmp->next->pre = tmp->pre;
// 将该节点添加到头节点之后
tmp->next = head->next;
head->next->pre = tmp;
tmp->pre = head;
head->next = tmp;
a = true; // 标记已找到
break;
}
tmp = tmp->next; // 继续遍历下一个节点
}
if(!a){ // 如果没有找到相同值的节点
// 创建新节点并添加到头节点之后
struct node *cur = new(struct node);
cur->val = val;
cur->next = head->next;
head->next->pre = cur;
cur->pre = head;
head->next = cur;
len++; // 链表长度增加
if(len>n){ // 如果长度超过n
// 删除尾节点之前的节点
struct node *tmp = back->pre;
back->pre = tmp->pre;
back->pre->next = back;
free(tmp);
}
}
}
// 如果是查询操作
else if(c == 'Q') {
struct node *tmp = head->next;
while (tmp != back) {
if (val == tmp->val) { // 找到该值的节点
// 将该节点移动到头节点之后
tmp->pre->next = tmp->next;
tmp->next->pre = tmp->pre;
tmp->next = head->next;
head->next->pre = tmp;
tmp->pre = head;
head->next = tmp;
break;
}
tmp = tmp->next;
}
}
// 如果是删除操作
else if(c == 'D') {
struct node *dtmp = head->next;
while (dtmp != back) {
if (val == dtmp->val) { // 找到该值的节点
// 将该节点从链表中删除
dtmp->pre->next = dtmp->next;
dtmp->next->pre = dtmp->pre;
free(dtmp);
len--; // 链表长度减少
break;
}
dtmp =dtmp->next;
}
}
}
// 输出链表中的所有节点值
struct node* tmp = head->next;
while(tmp != back->pre){
cout << tmp->val << ' ';
tmp = tmp->next;
}
cout << tmp->val;
return 0;
}
int main(){
return solu4();
}
Java
import java.io.*;
import java.util.HashMap;
class Node {
int no;
Node left;
Node right;
public Node(int no) {
this.no = no;
}
}
public class Main{
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
// 总存放页数
int n = ((int) in.nval);
in.nextToken();
HashMap<Integer, Node> map = new HashMap<>();
Node dummyHead = new Node(-1);
dummyHead.right = dummyHead;
dummyHead.left = dummyHead;
// 总操作数
int k = ((int) in.nval);
in.nextToken();
for (int i = 0; i < k; i++) {
String s = in.sval;
in.nextToken();
int a = ((int) in.nval);
in.nextToken();
if (s.equals("A")) {
if (!map.containsKey(a)) {
if (map.size() == n) {
// 删除最后的元素
Node del = dummyHead.left;
del.left.right = del.right;
del.right.left = del.left;
del.left = null;
del.right = null;
map.remove(del.no);
}
// 插入新元素
Node node = new Node(a);
node.right = dummyHead.right;
node.left = dummyHead;
dummyHead.right.left = node;
dummyHead.right = node;
map.put(a, node);
} else {
//从原来位置删除
Node node = map.get(a);
node.left.right = node.right;
node.right.left = node.left;
// 插入新位置
node.right = dummyHead.right;
node.left = dummyHead;
dummyHead.right.left = node;
dummyHead.right = node;
}
} else if (s.equals("Q")) {
if (!map.containsKey(a)) continue;
//从原来位置删除
Node node = map.get(a);
node.left.right = node.right;
node.right.left = node.left;
// 插入新位置
node.right = dummyHead.right;
node.left = dummyHead;
dummyHead.right.left = node;
dummyHead.right = node;
} else if (s.equals("D")) {
if (!map.containsKey(a)) continue;
//从原来位置删除
Node node = map.get(a);
node.left.right = node.right;
node.right.left = node.left;
node.left = null;
node.right = null;
map.remove(a);
}
}
Node node = dummyHead.right;
for (int i = 0; i < map.size(); i++, node = node.right) {
out.print(node.no + " ");
}
out.println();
out.flush();
return;
}
}
}
javaScript
const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;
// 双向链表节点
class Node {
constructor(no) {
this.no = no;
this.left = null;
this.right = null;
}
}
(async function () {
let line1 = (await readline()).split(" ").map(Number);
let n = line1[0]; // 读取总存放页数
let k = line1[1]; // 读取总操作数
let map = new Map();
let dummyHead = new Node(-1);
dummyHead.right = dummyHead;
dummyHead.left = dummyHead;
for (let i = 0; i < k; i++) {
let line = (await readline()).split(" ");
let s = line[0];
let a = parseInt(line[1]);
if (s === "A") {
if (!map.has(a)) {
if (map.size === n) {
// 删除最后的元素
let del = dummyHead.left;
del.left.right = del.right;
del.right.left = del.left;
map.delete(del.no);
}
// 插入新元素
let node = new Node(a);
node.right = dummyHead.right;
node.left = dummyHead;
dummyHead.right.left = node;
dummyHead.right = node;
map.set(a, node);
} else {
// 从原来位置删除
let node = map.get(a);
node.left.right = node.right;
node.right.left = node.left;
// 插入新位置
node.right = dummyHead.right;
node.left = dummyHead;
dummyHead.right.left = node;
dummyHead.right = node;
}
} else if (s === "Q") {
if (!map.has(a)) continue;
// 从原来位置删除
let node = map.get(a);
node.left.right = node.right;
node.right.left = node.left;
// 插入新位置
node.right = dummyHead.right;
node.left = dummyHead;
dummyHead.right.left = node;
dummyHead.right = node;
} else if (s === "D") {
if (!map.has(a)) continue;
// 从原来位置删除
let node = map.get(a);
node.left.right = node.right;
node.right.left = node.left;
map.delete(a);
}
}
// 输出链表中的元素
let result = [];
let node = dummyHead.right;
while (node !== dummyHead) {
result.push(node.no);
node = node.right;
}
console.log(result.join(" "));
rl.close();
})();
题目描述
小明有一门专业课,叫做操作系统,简称OS。操作系统的页式存储管理中,当主存满并且需要的存储页不在主存中,需要对主存中的页面进行置换,其中有一个非常重要的算法,即LRU置换算法。
LRU (Least Recently Used)缓存算法理一种常用于管理缓存的策略,其目标是保留最近使用过的数据,而淘汰最久未被使用的数据。实现简单的LRU缓存算法,支持查询、插入、删除操作。
最久未被使用定义:查询、插入和删除操作均为一次访问操作,每个元素均有一个最后一次被访问时间,按照最后一次被访问时间排序,时间最早的即为最久未使用。插入操作:当缓存中已经存在,则刷新值,不存在,则插入,如果超过上限,则淘汰最久未被使用的元素。
输入描述
第一行两个数N和K,分别表示缓存内最多可以存放页数,以及操作序列中的总操作数。其中N∈[1,100],K∈[1,10000]。
第二至第K+1行,每行两个输入,两个输入用空格分隔。第一个输入是一个字符chi:A表示插入,Q表示查询,D表示删除。第二个输入是一个整数ai,表示一个页面的编号。其中ai∈[1,100000]。
输出描述
输出一行,表示缓存内各页面的编号,按照LRU优先级。
样例一
输入
2 5
A 103
A 102
A 102
Q 103
A 101
输出
101 103
解释
缓存大小为2,依次进行插入103、102,重复插入102,查询一次103,插入101时,最久未使用的元素为102,所以淘汰102。
Limitation
1s, 1024KiB for each test case.