通过单链表实现LRU算法

内存缓存淘汰策略:FIFO,LFU,LRU实现解析
文章介绍了内存缓存的三种淘汰策略——FIFO、LFU和LRU,并详细讲解了LRU算法的实现,包括自定义单链表和基于单链表的LRU算法。LRU算法通过维护一个有序链表,当缓存满时,最近最少使用的数据会被淘汰。
内存缓存淘汰机制
  1. FIFO(First In First Out,先进先出)
  2. LFU(Least Frequently Used,最不经常使用)
  3. LRU(Least Recently Used,最近最少使用)

LFU是从整个使用时间内判断使用次数最少的算法,而LRU是从最近一段时间内来判断使用次数最少的算法

LRU算法

  1. 新数据插入到链表的头部
  2. 当缓存命中(即缓存数据被访问),数据要移动到表头
  3. 当链表满的时候,将链表尾部数据丢弃
自定义单链表和继承自自定义单链表的Lru算法链表

1.自定义单链表类MyLinkedList.java

package com.tangkun.lru;

/**
 * 自定义单链表
 * */
public class MyLinkedList<T> {
	//头结点的指针
	public Node<T> node;
	//链表的结点总数量
	public int size;

	public MyLinkedList() {

	}

	/**
	 * 新增 指定索引位置新增
	 */
	public void put(int index, T data) {
		checkPositionIndex(index);
		//插入结点位置的前一个结点
		Node<T> prev = node;
		//插入结点位置原来那个结点,新结点插入后,这个结点就在新结点后面
		Node<T> cur = node;
		//遍历出插入位置上一个结点和原来的结点
		for (int i = 0; i < index; i++) {
			prev = cur;
			cur = cur.next;
		}
		//构建出插入索引位置的新结点,将上面遍历出来的原来结点cur作为新结点的next指针的值
		Node<T> newNode = new Node<>(data, cur);
		//将插入位置上一个结点的next指针指向新插入的结点
		prev.next = newNode;
		//链表长度加1
		size++;
	}

	//新增到头部
	public void putFirst(T data) {
		//插入结点位置原来那个结点,新结点插入后,这个结点就在新结点后面
		Node<T> cur = node;
		Node<T> newNode = new Node<>(data, cur);
		//将插入到头部的结点赋值为整个链表的头结点
		node = newNode;
		size++;
	}

	/**
	 * 删除
	 */
	public void remove(int index) {
		checkPositionIndex(index);
		//删除结点位置的前一个结点
		Node<T> prev = node;
		//删除结点位置原来那个结点
		Node<T> cur = node;
		//遍历出删除位置上一个结点和原来的结点
		for (int i = 0; i < index; i++) {
			prev = cur;
			cur = cur.next;
		}
		prev.next = cur.next;
		cur.next = null;
		//链表长度减1
		size--;
	}

	//删除最后一个结点
	public void removeLast() {
		//删除结点位置的前一个结点
		Node<T> prev = node;
		//删除结点位置原来那个结点
		Node<T> cur = node;
		//遍历出删除位置上一个结点和原来的结点
		for (int i = 0; i < size; i++) {
			prev = cur;
			cur = cur.next;
		}
		prev.next = null;
		//链表长度减1
		size--;
	}

	/**
	 * 修改
	 */
	public T set(int index, T data) {
		checkPositionIndex(index);
		//修改结点位置原来那个结点
		Node<T> cur = node;
		//遍历出修改位置的结点
		for (int i = 0; i < index; i++) {
			cur = cur.next;
		}
		cur.data = data;
		return cur.data;
	}

	/**
	 * 查询
	 */
	public T get(int index) {
		checkPositionIndex(index);
		//查询结点位置原来那个结点
		Node<T> cur = node;
		//遍历出查询位置的结点
		for (int i = 0; i < index; i++) {
			cur = cur.next;
		}
		return cur.data;
	}

	/**
	 * 检查index是否越界
	 */
	public void checkPositionIndex(int index) {
		if (!(index >= 0 && index <= size)) {
			throw new IndexOutOfBoundsException("index:" + index + "|size:" + size);
		}
	}

	@Override
	public String toString() {
		//打印出来所有结点中的数据域
		Node<T> cur = node;
		for (int i = 0; i < size; i++) {
			System.out.println(cur.data + " ");
			cur = cur.next;
		}
		return "";
	}

	static class Node<T> {
		T data;
		Node<T> next;

		Node(T data, Node<T> next) {
			this.data = data;
			this.next = next;
		}
	}

	public static void main(String[] args) {
		MyLinkedList<Integer> myLinkedList = new MyLinkedList<>();
		myLinkedList.putFirst(3);
		myLinkedList.putFirst(2);
		myLinkedList.putFirst(1);
		System.out.println("添加结点后打印:" + myLinkedList.toString() + "\n");
		myLinkedList.put(2, 0);
		System.out.println("在索引为2的位置,添加结点0,然后打印:" + myLinkedList.toString() + "\n");
		myLinkedList.remove(1);
		System.out.println("删除索引为1的结点后打印:" + myLinkedList.toString() + "\n");
		myLinkedList.set(1, 8);
		System.out.println("更改索引1的值为8,然后打印:" + myLinkedList.toString() + "\n");
		myLinkedList.get(1);
		System.out.println("查询索引1的值:" + myLinkedList.get(1) + "\n");
	}
}

2.自定义Lru算法单链表LruLinkedList.java

package com.tangkun.lru;

/**
 * Lru算法单链表
 * */
public class LruLinkedList<T> extends MyLinkedList<T> {

	//自定义缓存容量大小
	public int memorySize;
	//缓存默认容量大小
	public static final int DEFAULT_CAPACITY = 5;

	public LruLinkedList() {
		this(DEFAULT_CAPACITY);
	}

	public LruLinkedList(int capacity) {
		memorySize = capacity;
	}

	/**
	 * Lru新增
	 * 新数据插入到链表的头部
	 */
	public void lruPutFirst(T data) {
		//判断当前缓存占用大小是否等于最大缓存容量,如果小于,则memorySize加1;若大于等于,则memorySize=MAX_CAPACITY,同时移除链表中最后一个结点
		if (size < memorySize) {
		} else {
			//移除最后一个结点
			removeLast();
		}
		//新增结点到头部
		putFirst(data);
	}

	/**
	 * Lru 查询
	 * 当缓存命中(即缓存数据被访问),数据要移动到表头
	 */
	public T lruGet(int index) {
		checkPositionIndex(index);
		//当前索引index前一个结点
		Node<T> prev = node;
		//当前索引index那个结点
		Node<T> cur = node;
		for (int i = 0; i < index; i++) {
			prev = cur;
			cur = cur.next;
		}
		//此时需要将该结点从链表中删除,同时将当前结点移动到链表的头部
		prev.next = cur.next;
		//注意,这里将查询的结点拿出来后,链表长度要减1
		size--;
		//将查询的结点添加到链表头部
		putFirst(cur.data);
		return cur.data;
	}

	public static void main(String[] args) {
		LruLinkedList<Integer> lruLinkedList = new LruLinkedList<>(3);
		lruLinkedList.lruPutFirst(1);
		lruLinkedList.lruPutFirst(2);
		lruLinkedList.lruPutFirst(3);
		lruLinkedList.lruPutFirst(4);
		System.out.println("自定义lru集合长度为3,然后往lru缓存中添加结点,打印出lur集合中数据:" + lruLinkedList.toString() + "\n");
		lruLinkedList.lruGet(2);
		System.out.println("查询lru集合中索引为2的结点,然后打印出查询后的集合中元素的排序:" + lruLinkedList.toString());
	}
}
<think>好的,用户想了解实现LRU页面置换算法的最佳数据结构。首先,我需要回忆一下LRU的基本原理。根据引用[3],LRU算法是选择最近最久未使用的页面进行淘汰,所以关键在于跟踪页面最近被访问的时间顺序。 常见的页面置换算法如引用[1]提到的,LRU需要维护一个队列,但仅用队列可能不够。因为当页面被访问时,需要快速将其移动到最近使用的位置,队列在中间操作时效率不高。这时候可能需要更高效的数据结构。 根据引用[2],CLOCK算法LRU的近似算法,但用户问的是LRU本身的最佳结构。通常,LRU的理想实现需要快速访问和更新最近使用的顺序。这里哈希表加双向链表的结构比较合适。哈希表提供O(1)的查找,双向链表允许快速移动节点到头部或删除尾部。例如,Java中的LinkedHashMap就是这种结构的实现。 另外,需要考虑页面访问的时间戳。如果使用数组或普通链表,每次访问都需要遍历来找到页面,时间复杂度会很高,变成O(n),对于大规模数据不合适。而哈希表加双向链表可以保证插入、删除和查找都在O(1)时间内完成。 比如,当访问一个页面时,通过哈希表找到该页在链表中的位置,然后将其移动到链表头部。当需要置换时,直接删除链表尾部的页面。这样就能高效维护访问顺序。 所以,总结下来,哈希表加双向链表的结构是实现LRU的最佳选择,既保证了快速访问,又能高效调整顺序。而引用[3]中提到的OPT和LRU的区别,也说明LRU需要向前看,即维护历史访问记录,这正好由链表结构支持。</think>实现LRU(最近最久未使用)页面置换算法的最佳数据结构是**哈希表(Hash Table)结合双向链表(Doubly Linked List)**。这种组合可以在$O(1)$时间复杂度内完成页面访问、插入和删除操作。 ### 具体实现原理 1. **双向链表** 用于按访问时间顺序维护页面,链表头部表示最近被访问的页面,尾部表示最久未使用的页面。每次访问页面时,将其移动到链表头部;置换时淘汰链表尾部的页面。 2. **哈希表** 以页面号为键,存储对应链表节点的指针,实现快速定位页面在链表中的位置。 ### 操作示例 - **访问页面**:通过哈希表找到链表节点,将其移动到链表头部。 - **插入新页面**:将新节点插入链表头部,并更新哈希表。 - **置换页面**:删除链表尾部节点,并移除哈希表中对应条目。 ### 代码示例(Python简化版) ```python class LRUCache: def __init__(self, capacity): self.capacity = capacity self.cache = {} # 哈希表:键 → 链表节点 self.head = DLinkedNode() # 双向链表头 self.tail = DLinkedNode() # 双向链表尾 self.head.next = self.tail self.tail.prev = self.head def get(self, key): if key not in self.cache: return -1 node = self.cache[key] self._move_to_head(node) # 移动到链表头部 return node.value def put(self, key, value): if key in self.cache: node = self.cache[key] node.value = value self._move_to_head(node) else: if len(self.cache) >= self.capacity: removed = self._remove_tail() # 淘汰尾部页面 del self.cache[removed.key] new_node = DLinkedNode(key, value) self.cache[key] = new_node self._add_to_head(new_node) ``` ### 性能优势 - **时间复杂度**:哈希表保证$O(1)$的查找速度,双向链表保证$O(1)$的插入和删除速度,满足LRU的高效需求。 - **空间复杂度**:为$O(n)$,与页面容量成正比[^3]。 ### 其他替代方案的局限性 - **普通队列或数组**:无法快速调整页面顺序,时间复杂度为$O(n)$。 - **栈或单链表**:无法反向操作,移除尾部节点时需要遍历。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值