一)并查集
在一些应用问题中,需要将N个不同的元素划分成一些互不相交的集合,开始的时候,每一个元素自成一个单元素集合,然后按照一定的规律将归于同一组元素的集合进行合并,并且在此过程中需要反复使用到查询某一个元素是属于哪一个集合,适合于描述这种抽象问题的数据类型称之为是并查集
1)假设某公司今年校招全国一共招生10人,西安招4人,成都招三人,武汉招四人,10个人来自于不同的学校,起初这10个人之间相互都不认识,每一个学生都是一个独立的小团体,现在给这些学生进行编号,{0,1,2,3,4,5,6,7,8,9},给以下数组用于存储小集体
2)毕业以后发现,学生们要去公司上班,每一个地方的学生自发的组织小分队开始上路,于是西安学生小分队{0,6,7,8},武汉学生小分队{2,3,5},成都学生小分队{1,4,9},每一个小分队的人的第一位同学当队长,带领大家找工作,这样的原来每一个地区互相不认识的几个人现在都相互认识了
4)从上图可以看出:编号6,7,8同学属于0号小分队,该小分队中有4人(包含队长0),编号为4和9的同学属于1号小分队,该小分队有3人(包含队长1),编号为3和5的同学属于2号小分队,该小分队有3个人(包含队长1)
5)从这里可以总结:
5.1)数组的下标代表集合中元素的编号
5.2)数组中下标的数如果代表的是负数,那么数组的下标的编号就是队长,此时这个负号代表这个下标就是此树形关系中的根节点,数字代表以当前下标为根节点的这棵二叉树中有多少元素
5.3)数组中如果某一个下标的数代表是非负数那么这个非负数代表的是这个下标的父亲节点
6)假设此时在公司工作一段时间后,西安小分队中8号同学与成都小分队1号同学奇迹般的走到了一起,两个小圈子的学生相互介绍,最后成为了一个小圈子
7)现在0集合中有7个人,2集合中有3个人,一共有两个朋友圈,通过上述例子可知,并查集可以解决下面的几个问题:
7.1)查找元素属于哪一个集合:沿着数组表示的属性关系以上找到根,根节点就是数组下标中元素是负数的位置
7.2)查看两个元素是否属于同一个集合:沿着数组表示的属性关系向上一直找到树的根,如果跟相同则表明是在同一个集合中,否则不在同一个集合中
7.3)将两个集合合并成1个集合:可以将两个集合中的元素进行合并,将一个集合中的名称改成另一个集合的名称
7.4)集合的个数:遍历整个数组,数组中元素是负数的个数就是集合中元素的个数
合并两个集合:
1)当合并两个数的时候, 这两个数必须来自于不同的集合,就拿上面的6和7来进行举例,这两个数本身都在同一个集合中了,因此对于他们的合并是没有任何意义的,所以如果给定两个数,如果他们的根节点相同,那么就不需要进行合并,所以合并一定是两个不同的集合中的元素;
2)以上面的这种图进行举例,假设要合并4和8所在的元素集合,首先找到根节点,分别是index1和index2
2.1)array[index1]=array[index1]+array[index2],这里面是更新新的根节点中的元素个数,更新孩子的数量
2.2)array[index2]=index1,更新另一棵树的根节点
public class UnionFindSet { public int[] array; public int usedSize; public UnionFindSet(int n){//n代表数组长度 this.array=new int[n]; Arrays.fill(array,-1); } public boolean isUnionSet(int x,int y){//查询x和y是否在同一个集合,就需要判断x和y的根节点是否相同 int father1=findRoot(x); int father2=findRoot(y); return father1==father2; } public int findRoot(int data){//查找数据x的根节点 if(data<0) throw new ArrayIndexOutOfBoundsException("此时data不能不是负数"); while(array[data]>=0){ data=array[data]; } return data;//此时返回的是根节点,而不是根节点对应的数字,此时下标已经代表的是根节点的数值了 } //合并两个集合 public void union(int x,int y){ int father1=findRoot(x); int father2=findRoot(y); if(father1==father2){ System.out.println("两个元素是属于同一个集合中的不需要进行合并"); return; } array[father1]=array[father1]+array[father2];//选取father1来充当合并以后的根节点3 array[father2]=father1;//更新被合并的那个数的根节点 } //查找集合的个数 public int GetCount(){ int count=0; for(int num:array){ if(num<0) count++; } return count; } public static void main(String[] args) { //根据上面这个合并的代码要重点理解合并的过程: UnionFindSet set=new UnionFindSet(100); //1.先合并0,6,7,8 set.union(0,6); set.union(7,8); set.union(0,7); //2.在合并1 4 9 set.union(1,4); set.union(4,9); //3.最后合并2 3 5 set.union(2,3); set.union(3,5); //4.观察最终结果 System.out.println(Arrays.toString(set.array)); set.union(8,1); //set.union(1,8)得到的就是index2也就是8充当根节点 System.out.println(Arrays.toString(set.array)); System.out.println(set.isUnionSet(6,9));//也可以判断他们是否是同一个亲戚 } }
并查集——亲戚(洛谷 P1551)_并查集试炼之亲戚_是一只派大鑫的博客-CSDN博客
并查集的应用:
1)省份数量:
547. 省份数量 - 力扣(LeetCode)
class UnionFindSet { public int[] array; public int usedSize; public UnionFindSet(int n){//n代表数组长度 this.array=new int[n]; Arrays.fill(array,-1); } public boolean isUnionSet(int x,int y){//查询x和y是否在同一个集合,就需要判断x和y的根节点是否相同 int father1=findRoot(x); int father2=findRoot(y); return father1==father2; } public int findRoot(int data){//查找数据x的根节点 if(data<0) throw new ArrayIndexOutOfBoundsException("此时data不能不是负数"); while(array[data]>=0){ data=array[data]; } return data;//此时返回的是根节点,而不是根节点对应的数字,此时下标已经代表的是根节点的数值了 } //合并两个集合 public void union(int x,int y){ int father1=findRoot(x); int father2=findRoot(y); if(father1==father2){ System.out.println("两个元素是属于同一个集合中的不需要进行合并"); return; } array[father1]=array[father1]+array[father2];//选取father1来充当合并以后的根节点3 array[father2]=father1;//更新被合并的那个数的根节点 } //查找集合的个数 public int GetCount(){ int count=0; for(int num:array){ if(num<0) count++; } return count; } } class Solution { public int findCircleNum(int[][] array) { UnionFindSet set=new UnionFindSet(array.length); for(int i=0;i
2)等式方程的可满足性
990. 等式方程的可满足性 - 力扣(LeetCode)
1)根据题目解析可以看到,如果发现字符串的第二个位置是等于号,那么代表这两个位置的字符是相等的,就可以合并,如果是!那么代表这两个字符不是相等的,就不能合并
2)算法原理:
2.1)如果发现str.charAt(1)=='='那么说明这个字符串主要做的就是一个合并操作,那么只需要把str.charAt(0)和str.charAt(3)对应的字符进行合并即可
2.2)如果在发现遍历整个字符串的过程中,str.charAt(1)=="!",说明此时这个字符串的功能执行的不是一个将两个字符串的合并操作,而是一个检查操作,此时就需要检查str.charAt(0)和str.charAt(3)是否合并过,如果是合并过的,直接返回false,否则直接返回true;
class UnionFindSet { public int[] array; public int usedSize; public UnionFindSet(int n){//n代表数组长度 this.array=new int[n]; Arrays.fill(array,-1); } public boolean isUnionSet(int x,int y){//查询x和y是否在同一个集合,就需要判断x和y的根节点是否相同 int father1=findRoot(x); int father2=findRoot(y); return father1==father2; } public int findRoot(int data){//查找数据x的根节点 if(data<0) throw new ArrayIndexOutOfBoundsException("此时data不能不是负数"); while(array[data]>=0){ data=array[data]; } return data;//此时返回的是根节点,而不是根节点对应的数字,此时下标已经代表的是根节点的数值了 } //合并两个集合 public void union(int x,int y){ int father1=findRoot(x); int father2=findRoot(y); if(father1==father2){ System.out.println("两个元素是属于同一个集合中的不需要进行合并"); return; } array[father1]=array[father1]+array[father2];//选取father1来充当合并以后的根节点3 array[father2]=father1;//更新被合并的那个数的根节点 } //查找集合的个数 public int GetCount(){ int count=0; for(int num:array){ if(num<0) count++; } return count; } } class Solution { public boolean equationsPossible(String[] strings) { UnionFindSet set=new UnionFindSet(26); for(String str:strings){ if(str.charAt(1)=='='){ set.union(str.charAt(0)-'a',str.charAt(3)-'a');//防止数组越界 } } for(String str:strings){ if(str.charAt(1)=='!'){ int father1=set.findRoot(str.charAt(0)-'a'); int father2=set.findRoot(str.charAt(3)-'a'); if(father1==father2) return false; } } return true; } }
二)LRUCache
1)linkedHashMap双向链表+哈希表,会根据你插入的顺序来输出最终的结果
2)LRU是Least Rencently Useds的缩写,意思是最近最少使用,它是一种Cache替换算法,因为Cache的容量是有限的,因此当新的Cache容量用完以后,此时又需要有新的内容添加进来的时候,这时就需要挑选出并且舍弃原有的部分内容,从而腾出新的空间来放新内容,而LRUCache的替换原则就是将最近最少的内容给直接替换掉,LRU其实译成最久未使用最形象,因为该算法每一次替换掉的就是最久没有被使用过的内容
3)LinkedHashMap中的参数说明:
3.1)初始容量大小,当使用无参构造方法的时候默认值是16
3.2)负载因子,默认是0.75f
3.3)一个boolean值,当这个值是true的时候调用get方法和put方法都会最终调用recordAccess方法使得最近使用的Entry移动到双向链表的末尾,当这个boolean值是false的时候,从源码就可以看出recordAccess什么也不会做,最近最常用的就是在尾巴节点,最近不经常使用的是头节点;如果是false,是基于插入顺序,如果是true是基于访问访问顺序
3.4)当进行存放的时候,当缓存中的元素已经满了的时候会删除头节点,因为头节点此时最近最少使用的,双向链表的数据越靠近尾巴,说明这个数据是经常使用的,双向链表的头部是最近最少使用的,当数据过多的时候,就会把最不经常访问的元素优先删除
public class LRUCache extends LinkedHashMap
{ public int capacity; public LRUCache(int capacity){ super(capacity,0.75f,true); this.capacity=capacity; } public Integer get(int key){ return super.getOrDefault(key,-1); } public void put(int key,int value){ super.put(key,value); } @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size()>capacity; } public static void main(String[] args) { LRUCache cache=new LRUCache(3); cache.put(1,1); cache.put(2,2); cache.put(3,3); cache.put(4,4); System.out.println(cache); } } 解决问题:为什么要重写removeOldestEntry()方法?
1)因为在源码中我们看下面的代码,只有当removeEldestEntry是true的时候才会移除节点,而此时我们所制定的规则就是当要存放的元素大于原始哈希表中的元素的个数的时候,就需要将最近最少使用的元素移除掉,而下面代码中的removeNode是删除的是头节点
2)必须重写后达到某一个条件是true,如果不重写,那么就永远是false
LRUcache的底层实现:
哈希表(查找时间复杂度就是O(1))+双向链表插入和删除的时间复杂度就是O(1)
解法1:只是创建一个虚拟头节点,没有创建虚拟尾巴节点,边界条件处理直接崩溃:
package Demo; import java.util.HashMap; import java.util.List; class ListNode{ public int key; public int value; public ListNode prev; public ListNode next; public ListNode(int key,int value){ this.key=key; this.value=value; } } public class LRUCache { public ListNode head=new ListNode(-1,-1);//代表双向链表的虚拟头节点 public ListNode tail=head;//代表双向链表的的尾巴节点 public int capacity;//代表最大容量 public int usedSize;//代表双向链表能够存储的最大元素个数 public HashMap
hash=new HashMap<>();//里面所存放的数据和key是key,value是链表所在的位置 public LRUCache(int capacity){ this.capacity=capacity; } //1.存储元素 public void put(int key,int value){ //1.检查当前这个key是否已经存储过 //1.1如果存储过,需要更新这个key对应的value值 //1.2然后把这个节点移动到放到链表的尾巴,因为这个是新插入的数据是最近访问到的数据 //2.如果没有出现过,实例化成一个节点,先存储到HashMap中一份,然后将这个节点放到链表中,useSize++,放入之后先判断有效数据个数是否超过了capacity //判断链表是否已经满了,usedSize==capacity,如果是,那么将头节点直接删除掉,usedSize--; ListNode node=hash.get(key); if(node!=null){ node.value=value; if(tail==node) return; //修改node前后结点的指向 ListNode prev=node.prev; ListNode next=node.next; prev.next=next; if(next!=null) next.prev=node.prev; //修改tail指针的指向 tail.next=node; node.prev=tail; node.next=null; tail=tail.next; }else{ ListNode newNode=new ListNode(key,value); hash.put(key,newNode); //添加节点到尾巴 tail.next=newNode; newNode.prev=tail; tail=tail.next; usedSize++; //如果容量已经满了 if(usedSize>capacity){ hash.remove(head.next.key); head=head.next; head.key=-1; head.value=-1; head.prev=null; usedSize--; } } } public int get(int key){ ListNode node=hash.get(key); if(tail==node) return node.value; if(node==null) return -1; ListNode prev=node.prev; ListNode next=node.next; prev.next=next; if(next!=null) next.prev=node.prev; //修改tail指针的指向 tail.next=node; node.prev=tail; node.next=null; tail=tail.next; return node.value; } public static void main(String[] args) { LRUCache cache=new LRUCache(2); cache.put(2,1); cache.put(2,2); cache.get(2); // cache.put(4,4); // System.out.println(cache.get(3)); } }
解法2:创建虚拟头节点和虚拟尾巴节点
import java.util.HashMap; import java.util.List; class ListNode{ public int key; public int value; public ListNode prev; public ListNode next; public ListNode(int key,int value){ this.key=key; this.value=value; } } public class LRUCache { public ListNode head=null;//代表双向链表的虚拟头节点 public ListNode tail=null;//代表双向链表的的尾巴节点 public int capacity;//代表最大容量 public int usedSize;//代表双向链表能够存储的最大元素个数 public HashMap
hash=new HashMap<>();//里面所存放的数据和key是key,value是链表所在的位置 public LRUCache(int capacity){ this.capacity=capacity; this.head=new ListNode(-1,-1); this.tail=new ListNode(-1,-1); head.next=tail; tail.prev=head; head.prev=null; tail.next=null; } //1.存储元素 public void put(int key,int value){ //1.检查当前这个key是否已经存储过 //1.1如果存储过,需要更新这个key对应的value值 //1.2然后把这个节点移动到放到链表的尾巴,因为这个是新插入的数据是最近访问到的数据 //2.如果没有出现过,实例化成一个节点,先存储到HashMap中一份,然后将这个节点放到链表中,useSize++,放入之后先判断有效数据个数是否超过了capacity //判断链表是否已经满了,usedSize==capacity,如果是,那么将头节点直接删除掉,usedSize--; ListNode node=hash.getOrDefault(key,null); if(node==null){ ListNode newNode=new ListNode(key,value); hash.put(key,newNode);//将这个节点加入到哈希表中 ListNode prev=tail.prev; prev.next=newNode; newNode.prev=prev; newNode.next=tail; tail.prev=newNode; usedSize++; if(usedSize>capacity){ //此时需要移除头节点 hash.remove(head.next.key);//把这个节点从哈希表中删除掉 head=head.next; head.prev=null; head.key=-1; head.value=-1; usedSize--; } }else{ node.value=value; //见这个节点移动到最后面 ListNode prev=node.prev; ListNode next=node.next; prev.next=next; next.prev=prev; prev=tail.prev; prev.next=node; node.prev=prev; node.next=tail; tail.prev=node; } } public int get(int key){ ListNode node=hash.getOrDefault(key,null); if(node==null) return -1; ListNode prev=node.prev; ListNode next=node.next; prev.next=next; next.prev=prev; prev=tail.prev; prev.next=node; node.prev=prev; node.next=tail; tail.prev=node; return node.value; } // public static void main(String[] args) { // LRUCache cache=new LRUCache(2); // cache.put(1,1); // cache.put(2,2); // System.out.println(cache.get(1)); // cache.put(3,3); // // System.out.println(cache.get(2)); // // cache.put(4,4); // // System.out.println(cache.get(1)); // // System.out.println(cache.get(3)); // // System.out.println(cache.get(4)); // } }