0410-算法复盘-设计LRU缓存结构、并查集Union-Find算法(路径压缩递归、迭代)

1.设计LRU缓存结构

LRU算法是指最近最久未使用,初次接触是在操作系统中,在虚拟内存,当内存空间不足时需要将一些段或者页淘汰出内存中,从而加载新的段页进来

设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。

它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

面试题 16.25. LRU 缓存

LRUCache cache = new LRUCache( 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

思路:对于LRU缓存算法,Java原生的集合LinkedHashMap就已经具备了这种能力,LinkedHashMap是HashMap的子类,与HashMap不同的是,LinkedHashMap可以通过插入和访问来确定顺序,它的底层维护了一个双向链表

具体通过下面代码即可很好理解

Map<Integer,Integer> map=new LinkedHashMap<>(10,0.75f,true);
map.put(9,3);
map.put(7,4);
map.put(5,9);
map.put(3,4);
//现在遍历的话顺序肯定是9,7,5,3
//下面访问了一下9,3这个键值对,输出顺序就变喽~
map.get(9);
//现在的顺序为:7 5 3 9

所以LinkedHashMap(true)中如果调用一次get方法,会将该元素放置在总体顺序的最后,当长度达到最大时,可以将LinkedHashMap首部的元素淘汰,这样就完成了最近最久未使用,也就是LinkedHashMap是天生支持LRU缓存结构的

代码如下

class LRUCache {
    HashMap<Integer, Integer> map = null;
    int capacity;
    public LRUCache(int capacity) {
        map = new LinkedHashMap<Integer, Integer>((int)((float)capacity / 0.75f + 1f), 0.75f, true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
        return map.get(key) == null ? -1 : map.get(key);
    }
    
    public void put(int key, int value) {
        map.put(key, value);
        if(map.size() > capacity){
            //达到最大值 那么移除首部元素
            map.remove(map.keySet().iterator().next());
        }
    }
}

但是,如果在面试中遇到一般不让使用原生的,我们完全可以手写一个LRU缓存,首先考虑LinkedHashMap的底层是使用了双向链表,而且该LRU缓存结构需要支持快速访问,所以必须要有一个map,综上,LRU应该具有以下结构:

  • 一个节点LinkedNode,里面包含属性key、value、prev、next
  • 一个map,存储(key, LinkedNode)

接着思考get方法,首先肯定是通过map.get(key)获取到该LinkedNode,接着需要将该节点放置在双向链表的尾部,作为最近使用的,所以可以同时维护一个head和tai的虚拟节点,这样移动到尾部的工作就比较简单

接着考虑put方法,这里有可能的情况是进行value的覆盖,所以首先要做的是使用map.get(key)判断是否存在该节点,如果存在该节点,应该做两件事情:①改变该节点value的值 ②将该节点移动到双向链表的尾部,完成最近使用的工作;如果不存在该节点,那么新创建该节点,同时将该节点add至双向链表的尾部。同时还需要判断当前加入节点之后整个链表的值是否到达最大值,如果是那么需要将链表的第一个值(head.next)进行移除,因为这就是最近最久未使用的

代码如下

class LRUCache {
    //LinkedNode节点定义
    class LinkedNode{
        int key;
        int value;
        LinkedNode next;
        LinkedNode prev;
        public LinkedNode(int key, int value){
            this.key = key;
            this.value = value;
        }
        public LinkedNode(){}
    }
    //虚拟的头和尾节点,便于添加和删除操作
    LinkedNode head;
    LinkedNode tail;
    //维护整个双向链表的size
    int size;
    int capacity;
    Map<Integer, LinkedNode> cache = new HashMap<>();
    //初始化
    public LRUCache(int capacity) {
        head = new LinkedNode();
        tail = new LinkedNode();
        head.next = tail;
        tail.prev = head;
        this.capacity = capacity;
    }
    
    public int get(int key) {
        //从map中获取LinkedNode
        LinkedNode node = cache.get(key);
        if(node != null){
            //将该节点移动到链尾部
            moveToTail(node);
            return node.value;
        }else{
            return -1;
        }
    }
    
    public void put(int key, int value) {
        //判断是否需要覆盖
       LinkedNode node = cache.get(key);
       if(node != null){
           node.value = value;
           //将该节点移动到链尾部
           moveToTail(node);
           return;
       }
       size++;
        //创建新节点
       LinkedNode temp = new LinkedNode(key, value);
       addNode(temp);
       cache.put(key, temp);
       if(size > capacity){
           //注意要同时更新map结构和双向链表的值
           cache.remove(head.next.key);
           remodNode(head.next);
       }
    }
    public void moveToTail(LinkedNode node){
        remodNode(node);
        addNode(node);
    }
    public void remodNode(LinkedNode node){
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    //每次添加都是添加到链表的尾部
    public void addNode(LinkedNode node){
        LinkedNode tailPrev = tail.prev;
        tailPrev.next = node;
        node.next = tail;
        node.prev = tailPrev;
        tail.prev = node;
    }
}

2. 并查集Union-Find算法

题目描述

洛谷1551

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。

以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。

接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。

P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。

输入输出样例:

6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
Yes
Yes
No

并查集的入门题,并查集通常用于判断两点之间是否连通的问题。可以采取如下的思想,对于具有连通性的元素具有相同的标识,例如上面题目有6个元素

元素123456
组别123456

初始化状态,任何元素之间都没有连通性,那么6个元素自然对于6个组别,当输入(1,2)说明元素1和元素2是同一个组的,那么可以做如下变化,当然这个组标识同时为2也是一样的

元素123456
组别113456

当输入(1,5)

元素123456
组别113416

当输入(3,4)

元素123456
组别113316

当输入(1,3)这个时候麻烦来了,我们需要将所有组别为3的组合并到组别为1的组上,这还是一个麻烦事,时间复杂度是O(n),因为需要遍历寻找所有的组别为3的组

上面是典型的牵一发而动全身,主要是各个节点组别之间无关系,可以采用如下方法解决:使用数组模拟树,对于每一个组都是一棵树,所以在修改组别的时候值需要将数的根节点进行修改就可以,对于数组arr[i] = i表示i节点的父节点是arr[i],例如arr[2] = 1表示2的父节点是1,那么在修改整个组别的时候只需要找到root根节点即可,找根节点可以使用while循环


while(arr[i] != i){
    i = arr[i]
} 

这样找下去可以找到根节点,上面采用的是一种树的思想,再次考虑极端情况如果对于该数组组成的树退化成为了链表,那么寻找的效率是非常低的,所以要采取路径压缩,因为上述没有具体指定谁是谁的上级只规定了总root节点就是所有人的最终上级,可以让每一个元素的直接父节点都是root,那么无论数据怎么变化,树的层级都只有两级,这就是咋很大程度上压缩了路径,提高了算法的效率

//递归的方法
int find(int x)     				//查找结点 x的根结点 
{
    if(pre[x] == x) return x;		//递归出口:x的上级为 x本身,即 x为根结点        
    return pre[x] = find(pre[x]);	//此代码相当于先找到根结点 rootx,pre[x]=rootx 
}
//迭代的方法
public static int find(int i){
    int fi = i;
    while(arr[fi] != fi) fi = arr[fi];
    //依次修改为fi
    while(fi != i){
        int temp = arr[i];
        arr[i] = fi;
        i = temp;
    }
    return fi;
}

上面例题代码为

import java.util.*;
public class Main{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] arr = new int[n + 1];
        for(int i = 1;i < arr.length;i++){
            arr[i] = i;
        }
        int m = sc.nextInt();
        int p = sc.nextInt();
        while(m-- > 0){
            int x = sc.nextInt();
            int y = sc.nextInt();
            unio(arr, x, y);
        }
        while(p-- > 0){
            int x = sc.nextInt();
            int y = sc.nextInt();
            if(find(arr, x) == find(arr, y)){
                System.out.println("Yes");
            }else{
                System.out.println("No");
            }
        }
    }
    public static int find(int[] arr, int i){
        int fi = i;
        while(arr[fi] != fi) fi = arr[fi];
        while(fi != i){
            int temp = arr[i];
            arr[i] = fi;
            i = temp;
        }
        return fi;
    }
    public static void unio(int[] arr, int x, int y){
        int fx = find(arr, x);
        int fy = find(arr, y);
        if(fx != fy){
            arr[fx] = fy;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值