模拟LRU缓存
链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助哈希表,可以通过 key 快速映射到任意一个链表节点,然后进行插入和删除。
这里一定要用双链表,因为需要有删除和增加节点的操作,需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。
- 【链表与顺序表(数组)的区别】
顺序表:
1、优点:顺序表空间连续,支持随机访问;
2、缺点:
中间或前面部分的插入删除操作时间复杂度 O(N)
增容的代价比较大
(方便查找,不方便插入和删除)
链表:
1、优点:
任意位置插入删除的时间复杂度为 O(1)
没有增容问题,插入一个开辟一个空间
2、缺点:以节点为单位存储,不支持随机访问
(不方便查找,方便插入删除)
**解题思路:**确定要使用的数据结构:双链表、哈希表,然后模拟LRU机制。
package Solution2;
import java.util.*;
// 链表节点
class Node {
public int key, val;
public Node next, prev;
public Node(int k, int v) {
this.key = k;
this.val = v;
}
}
// 构造双链表
class DoubleList {
// 头尾虚节点
private Node head, tail;
// 链表元素数
private int size;
public DoubleList() {
// 初始化双向链表的数据
head = new Node(0, 0); // 越接近表头则是最久未被使用的
tail = new Node(0, 0); // 每次把最新的节点插入到表尾之前
head.next = tail;
tail.prev = head;
size = 0;
}
// 在链表尾部添加节点 x,时间 O(1)
public void addLast(Node x) {
x.prev = tail.prev;
x.next = tail;
tail.prev.next = x;
tail.prev = x;
size++;
}
// 删除链表中的 x 节点(x 一定存在)
// 由于是双链表且给的是目标 Node 节点,时间 O(1)
public void remove(Node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// 删除链表中第一个节点,并返回该节点,时间 O(1)
public Node removeFirst() {
if (head.next == tail)
return null;
Node first = head.next;
remove(first);
return first;
}
// 返回链表长度,时间 O(1)
public int size() { return size; }
}
public class Solution2 {
/**
* lru design
* @param operators int整型二维数组 the ops
* @param k int整型 the k
* @return int整型一维数组
*/
public static int[] LRU(int[][] operators, int k) {
ArrayList<Integer> ans = new ArrayList<Integer>();
// HashMap是为了提高查找效率 O(1),链表的查找效率为O(N)
HashMap<Integer, Node> find_map = new HashMap<Integer, Node>();
DoubleList list = new DoubleList();
for(int i=0; i < operators.length; i++){
if(operators[i][0]==1){
// 若链表大小超过K
if(list.size() >= k){
// 删除表头最久未被使用的节点
find_map.remove(list.removeFirst().key);
}
// 更新双链表和HashMap
Node node = new Node(operators[i][1],operators[i][2]);
list.addLast(node);
find_map.put(operators[i][1], node);
} else{// 进行查找操作
Node node = find_map.get(operators[i][1]);
if (node==null){
ans.add(-1);
continue;
}
ans.add(node.val);
// 更新链表
list.remove(node);
list.addLast(node);
find_map.put(node.key, node);
}
}
int[]ans_ = new int[ans.size()];
for(int i=0;i<ans.size();i++){
ans_[i] = ans.get(i);
}
return ans_;
}
public static void main(String[] args) {
int [][]operators = {{1,1,1},{1,2,2},{1,3,2},{2,1},{1,4,4},{2,2}};
int k = 3;
LRU(operators, k);
}
}
寻找最小的K个数
解法一
要求一个序列中最小的k个数,按照惯有的思维方式,则是先对这个序列从小到大排序,然后输出前面的最小的k个数。
至于选取什么的排序方法,我想你可能会第一时间想到快速排序(我们知道,快速排序平均所费时间为n*logn),然后再遍历序列中前k个元素输出即可。因此,总的时间复杂度:O(n * log n)+O(k)=O(n * log n)。
解法二
咱们再进一步想想,题目没有要求最小的k个数有序,也没要求最后n-k个数有序。既然如此,就没有必要对所有元素进行排序。这时,咱们想到了用选择或交换排序,即:
1、遍历n个数,把最先遍历到的k个数存入到大小为k的数组中,假设它们即是最小的k个数;
2、对这k个数,利用选择或交换排序找到这k个元素中的最大值kmax(找最大值需要遍历这k个数,时间复杂度为O(k));
3、继续遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与kmax比较:如果x < kmax ,用x替换kmax,并回到第二步重新找出k个元素的数组中最大元素kmax‘;如果x >= kmax,则继续遍历不更新数组。
每次遍历,更新或不更新数组的所用的时间为O(k)或O(0)。故整趟下来,时间复杂度为nO(k)=O(nk)。
解法三
更好的办法是维护容量为k的最大堆,原理跟解法二的方法相似:
1、用容量为k的最大堆存储最先遍历到的k个数,同样假设它们即是最小的k个数;
2、堆中元素是有序的,令k1<k2<…<kmax(kmax设为最大堆中的最大元素)
3、遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与堆顶元素kmax比较:如果x < kmax,用x替换kmax,然后更新堆(用时logk);否则不更新堆。
这样下来,总的时间复杂度:O(k+(n-k)logk)=O(nlogk)。此方法得益于堆中进行查找和更新的时间复杂度均为:O(logk)(若使用解法二:在数组中找出最大元素,时间复杂度:O(k))。