在操作系统中,常见的内存页面存置换算法有:FIFO (先进先出置换算法,LRU(最近未使用算法),OPT)等。这些思想经常会在ACM竞赛中出现,下面我们用代码来实现一下。
题目推荐:农大oj
1862: Page Replacement (LRU)
1874: Kevin的书架 (OPT)
①FIFO:顾名思义,FIFO就是最先进入缓冲区的页,先退出缓冲区。(如果某个页面命中,不更新该页面进入缓冲区的时间)这也是FIFO与LRU的区别。FIFO与OPT的区别: FIFO算法利用页面进入内存后的时间长短作为置换依据,而OPT算法的依据是将来使用页面的时间。
#define M 10003
#define CLS(x,v) memset(x,v,sizeof(x))
int x[M];
bool vis[M];
int FIFO(int k,int n) {
int ans,head,rear;
ans=head=rear=0;
CLS(vis,0);
for(int i=0;i<n;i++)
if(!vis[x[i]])//不在缓冲区
{
ans++;//记录缺页次数
vis[x[i]]=1;
x[rear++]=x[i];//加入到缓冲区
if(rear-head>k)//如果缓冲区已满,最先来的离开
vis[x[head++]]=0;
}
return ans;
}
int main() {
int cas, k,n;
scanf("%d", &cas);
while (cas--) {
scanf("%d%d", &k, &n);
assert(k>0);
for (int i = 0; i < n; i++) {
scanf("%d", &x[i]);
}
printf("%d\n", FIFO(k,n));
}
return 0;
}
/*
1
3 21
7 0 1 2 0
3 0 4 2 3
0 3 2 1 2
2 0 1 7 0 1
ans :15
*/
②LRU:是操作系统中经常采用的页面置换算法。我们可以想到,采用双向链表保存当前缓冲区页面的下标(缓冲区页面个数最多为m),vis保存当前缓冲区页面的编号;每来一个页面,维护双向list 和 vis[]. 时间复杂度:O(n)
#include <unordered_map>
#include <iostream>
using namespace std;
/*
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。
它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。
当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
LRUCache cache = new LRUCache(2); /* 缓存容量为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
进阶:
你是否可以在 O(1) 时间复杂度内完成这两种操作?
*/
// 双向链表
struct DListNode {
DListNode *next;
DListNode *prev;
int k, v;
DListNode(int key, int val) : k(key), v(val), prev(NULL), next(NULL) {}
};
class DList {
private:
DListNode *head, *rear;
public:
DList() {
head = new DListNode(-1, -1);
rear = new DListNode(-1, -1);
head->next = rear;
rear->prev = head;
}
// 删除最久未访问的cache
DListNode* remove_oldest_node() {
auto oldest = remove_node_from_list(rear->prev);
return oldest;
}
// 从双向链表,删除一个node
DListNode* remove_node_from_list(DListNode *node) {
//move out
node->next->prev = node->prev;
node->prev->next = node->next;
node->prev = NULL;
node->next = NULL;
return node;
}
void insert_to_head(DListNode *node) {
//right
node->next = head->next;
head->next->prev = node;
//left
head->next = node;
node->prev = head;
}
void move_to_head(DListNode *node) {
remove_node_from_list(node);
insert_to_head(node);
}
};
class LRUCache {
private:
int cap;
unordered_map<int, DListNode*> cache;
DList dlist;
public:
LRUCache(int capacity) : cap { capacity } {
cache.reserve(capacity);
}
int get(int key) {
auto it = cache.find(key);
if (it == cache.end()) {
return -1;
}
dlist.move_to_head(it->second);
return it->second->v;
}
void put(int key, int value) {
auto it = cache.find(key);
if (it == cache.end()) {
// add new k-v
if (cache.size() >= cap) {
// 删除最久未访问 k-v
auto oldest = dlist.remove_oldest_node();
cache.erase(oldest->k);
}
auto node = new DListNode(key, value);
dlist.insert_to_head(node);
cache[key] = node;
} else {
// update existed k-v
it->second->v = value;
dlist.move_to_head(it->second);
}
}
};
方法二:还有一种O(n+m)的实现,和FIFO的实现方式类似,相同的页面重复入队,并增加内存页面数,当队列中互不相同的页面个数大于m,就将最先进来的页面出队,如果当前出队的元素在缓冲区还有相同的,就减小内存页面数。始终使 内存中页面数==m。
#include <cstdio>
#include <cstring>
#include <map>
#include <queue>
#define M 100004
using namespace std;
#define CLS(x,v) memset(x,v,sizeof(x))
#define filew freopen("d:\\in.out","w",stdout);
int vis[M];
queue<int> Q;
int main()
{
int T;
int n,m,x;
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&m,&n);
while(!Q.empty())Q.pop();
CLS(vis,0);
int ans=0;//ans记录未命中的次数 //k=m 实际上缓冲区大小为k
for(int i=0;i<n;i++)
{
scanf("%d",&x);
if(vis[x]>0)m++;//相同的元素增加缓冲区大小
else ans++;
Q.push(x);
vis[x]++;
while(Q.size()>m)//当缓冲区中大于k个互不相同的页面时,将最先进来的出队
{
int h=Q.front();
Q.pop();
vis[h]--;
if(vis[h]>0)m--;
}
}
printf("%d\n",ans);
}
return 0;
}
/**
2 6
1 2 3 2 4 3
4
*/
③ 最佳页面置换算法 (OPT)O(nlog(m))
OPT:是在理论上提出的一种算法。当需要替换某个页面时,所选择的老页面将来不再被使用,或者是在最远的将来才被访问。
但是最优页面置换算法的实现是困难的,因为它需要人们预先就知道一个进程整个运行过程中页面走向的全部情况。不过,这个算法可用来衡量(如通过模拟实验分析或理论分析)其他算法的优劣。
下面,采用set维护一个有序页面编号来模拟实现opt算法。
#define M 10003
#define CLS(x,v) memset(x,v,sizeof(x))
int x[M];
int nextpos[M];//记录x[i]下一次出现的下标
int pre[M];//记录x[i]的下标
set<int> s;
/*
* 最佳页面置换算法思想:
* 1.如果当前页面在缓冲区中: 更新当前页面下一次出现的下标
* 2 不在缓冲区: 缓冲区已满,替换掉最远才会用到的页面
* 未满,新页面加入缓冲区即可
*/
//optimal_page_replacement_algorithm
int optimal(int k,int n) {
int ans = 0, i;
CLS(pre,-1);CLS(nextpos,-1);s.clear();
//预处理页面x[i]下一次最近出现的下标
for(i=0;i<n;i++)
{
if(pre[x[i]]!=-1)
nextpos[pre[x[i]]]=i;
pre[x[i]]=i;
}
set<int>::iterator it;
for (i = 0; i < n; i++) {
//如果当前页面在 缓冲区
if((it=s.find(i))!=s.end())
{
s.erase(it);
}else
{
ans++;//如果页面已满,则将最远将来才被访问的页面替换
if(s.size()==k)s.erase(--s.end());
}
if(nextpos[i]!=-1)s.insert(nextpos[i]);
}
return ans;
}
int main() {
int cas, k,n;
scanf("%d", &cas);
while (cas--) {
scanf("%d%d", &k, &n);
assert(k>0);
for (int i = 0; i < n; i++) {
scanf("%d", &x[i]);
}
printf("%d\n", optimal(k,n));
}
return 0;
}