我的刷题之旅——栈、堆和队列

我的刷题之旅——栈、堆、队列、并查集


刷题常用数据结构

  • 我的刷题之旅——栈、堆、队列、并查集
  • (一)栈
    • 剑指offer09 用两个栈实现队列
    • 20 有效的括号——辅助栈法
    • 155 最小栈(单调栈思想——存在和左右比较的关系)
    • 394 字符串解码
    • 739 每日温度(单调栈——需要和左右比较的时候用)
    • 84 柱状图中的最大矩形
    • 85 最大矩形
    • 42 接雨水——dp 双指针 单调栈
  • (二)堆
    • 剑指offer41 数据流中的中位数
    • 23 合并k个升序链表(三种方法)
    • 215 数组的第K个最大的元素
    • 347 前K个高频元素(hash+快排/堆排)
  • (三)队列
    • 剑指offer59I 滑动窗口的最大值(单调队列)
    • 剑指offer59II 队列的最大值
    • 621 任务调度器(用优先队列太麻烦了,填桶策略)
  • (四)并查集(一般用于判断图中是否有环,也可以用于求连通分量个数)
    • 128最长连续序列——(字节原题,并查集和哈希表法)
    • 547 朋友圈——(并查集)
    • 被围绕的区域


前言

这一部分的题目相当相当重要哦,主要用于一些特殊的场合和某些特定题目的优化,大多数是那种表面很难,其实很套路的题目,经常和其他算法结合在一起,但是也不要担心,我感觉这些题目算是最套路的,永远是哪几个场合会用到。


(一)栈

剑指offer09 用两个栈实现队列

很简单,一个做队列,一个做缓存辅助
入队:队列栈全部放入缓存,将结果放回队列,然后缓存放回栈
出队:直接pop

20 有效的括号——辅助栈法

就是最简单的栈的应用,
如果当前元素是右括号,看看是不是栈不空并且栈顶弹出的是不是对应,不是提前返回false;
如果当前元素是左括号,直接入栈。
最后如果栈空才为真。

class Solution {
    public boolean isValid(String s) {
        HashMap<Character,Character> map = new HashMap<>();
        map.put('(',')');
        map.put('[',']');
        map.put('{','}');
        LinkedList<Character> stack = new LinkedList<>();//注意没有Stack类
        for(int i=0;i<s.length();i++){
            Character c = s.charAt(i);
            if(map.containsKey(c)){
                stack.push(c);
            }else{
                if(stack.isEmpty())
                    return false;
                char top = stack.pop();//栈的方法是push和pop
                if(map.get(top)==c)
                    continue;
                else return false;
            }
        } 
        return stack.size()==0;
    }
}

155 最小栈(单调栈思想——存在和左右比较的关系)

要用O(1)的复杂度求出栈中的最小元素,但是栈同时也在pop,所以我们不可以用一个变量保存当前栈的最小值,那么怎么办呢?——使用局部最小辅助栈,该辅助栈的栈顶存放当前栈状态的最小值。
主栈进新元素a:
如果辅助栈空,a放入;
a>辅助栈顶,辅助栈不变;
a<=辅助栈顶,辅助栈中将a放入;

主栈弹出元素a:
a>辅助栈顶,此时辅助栈顶是min;
a==辅助栈顶,此时辅助栈顶pop;

class MinStack {
    private Stack<Integer> stack;
    private Stack<Integer> help;
   
    public MinStack() {
        stack = new Stack<>();
        help = new Stack<>();
    }
    
    public void push(int x) {
        stack.push(x);
        if(help.isEmpty()||x<=help.peek())
            help.push(x);
    }
    
    public void pop() {
        if(stack.pop().equals(help.peek()))//一定要注意集合中装的是对象,比较的时候用equals方法,==是不可以的
            help.pop();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int getMin() {
        return help.peek();
    }
}

394 字符串解码

如题
输入:s = “3[a2[c]]”
输出:“accaccacc”
我们做的时候发现也是需要括号改变顺序的,使用辅助栈。

预先在栈中push一个new StringBuilder("");
如果当前元素是数字,转成数字push入栈;
如果当前元素是[,直接push入栈,并且在栈中push一个new StringBuilder("")
如果当前元素是字母,栈顶的StringBuilder串拼接之;
如果当前元素是],从栈中依次pop出栈顶的StringBuilder串、[、数字,然后该字母串倍增那个数字的次数。然后将倍增后的串追加到栈顶的串后面,回到开始接着遍历;

结束遍历的时候栈顶放着的是结果。

class Solution {
    public String decodeString(String s) {
        LinkedList stack = new LinkedList();
        stack.push(new StringBuilder(""));
        for(int i=0;i<s.length();i++){
            int digit=0;//
            while(s.charAt(i)>='0'&&s.charAt(i)<='9'){
                digit=digit*10 +s.charAt(i)-'0';
                i++;
            }
            if(digit!=0)
                stack.push(digit);

            if(s.charAt(i)=='['){
                stack.push('[');
                stack.push(new StringBuilder(""));
            }
            else if(s.charAt(i)==']'){
                StringBuilder temp = (StringBuilder)stack.pop();
                stack.pop();//pop左括号
                Integer count = (Integer)stack.pop();
                String ss = temp.toString();
                for(int j=0;j<count-1;j++)
                    temp.append(ss);
                StringBuilder sb = (StringBuilder)stack.peek();
                sb.append(temp);
            }else{
               StringBuilder sb = (StringBuilder)stack.peek();
               sb.append(s.charAt(i));
            }
            
        }
        return (String)stack.peek().toString();
    }
}

739 每日温度(单调栈——需要和左右比较的时候用)

遍历每日温度,维护一个单调栈——栈中保存着递减的下标,为什么要维持这样一个递减的栈呢?因为每次只有变成递增状态的时候能够找到增温点。
如果当日温度<=栈顶温度,也就是说
如果当日温度>栈顶温度,代表栈顶的那天的升温日找到了,栈元素依次出栈,直到把当日温度放在它合适的位置。

class Solution {
    public int[] dailyTemperatures(int[] T) {
        LinkedList<Integer> stack = new LinkedList<>();
        int[] res = new int[T.length];
        for(int i=0;i<T.length;i++)
            res[i] = 0;
        for(int i=0;i<T.length;i++){
            if(stack.isEmpty() || T[i] <= T[stack.peek()])
                stack.push(i);
            else{
                while(!stack.isEmpty() && T[stack.peek()] < T[i]){//注意,使用stack.peek的时候确保不空才行
                    int index = stack.pop();
                    res[index] = i-index;
                }
                stack.push(i);
            }
        }
        return res;
    }
}

84 柱状图中的最大矩形

85 最大矩形

42 接雨水——dp 双指针 单调栈

这一题有三种简单解法,每一种都很棒,全部要求掌握。雨水数组是a[]
(1)两遍遍历,用数组保存a[i]的前i个高度中的最大值(可以用动态规划思想)
第二遍遍历,从a最后一个元素向前,每次求出从当前到最后的最大值。此时当前元素左最大值得到,右最大值也得到,得到可积累雨水——最简单通俗容易想,时间复杂度O(n),空间O(n)。
代码如下:

class Solution {
    //遍历求左右最大高度
    public int trap(int[] height) {
        if(height.length==0)
            return 0;
        int res = 0;
        int[] left = new int[height.length];
        //第一遍,求每个节点左边最高(含自己)
        left[0] = height[0];
        for(int i=1;i<height.length;i++){
            left[i] = Math.max(left[i-1],height[i]);
        }
        //第二遍,求右边最高,并且同时比较左右最高,求出可积累雨水量
        int rightmx = 0;
        for(int i=height.length-1;i>=0;i--){
            rightmx = Math.max(rightmx,height[i]);
            res += Math.min(left[i],rightmx)-height[i];
        }
       return res;
    }
}

(2)最高效的方法——(双指针),时间O(n),空间O(1)
left指针:从左往右当前下标
right指针:从右向左当前下标
left_max:left指针左边能找的最大值(不含left自己)——这个值一定是当前left的左堤坝
right_max:right指针右边能找到的最大值(不含right)——这个值一定是当前right的优堤坝

对于left而言,左边最大值一定是left_max,右边最大值范围是“大于等于”right_max的,所以,如果当前的left_max < right_max的时候,说明将来右无论怎样移动都不会影响结果,水都会被从右边挡住,左边的left_max一定是短板。当这种情况下,我们可以去除了left指针了,并计算当前left的存水量,left++。
反之,若right_max < left_max,就可以处理右节点了。

class Solution {
    public int trap(int[] height) {
        if(height.length<=2)
            return 0;
        int res = 0;
        int left = 0,right = height.length-1;
        int leftMax = 0,rightMax = 0;
        while(left <= right){//注意结束条件,当left==right时,当前节点依然可以和左右最高值计算存雨量
            if(leftMax < rightMax){//左指针可计算存储量了
                res += Math.max(leftMax-height[left],0);
                leftMax = Math.max(leftMax,height[left]);
                left += 1;
            }else{
                res += Math.max(rightMax-height[right],0);
                rightMax = Math.max(rightMax,height[right]);
                right--;
            }
        }
        return res;
    }
}

(3)用单调栈,找到每次发生上升的位置,只有此时才可能出现囤积雨水的现象。
思路:
设置两个栈,一次存放递减高度的下标,一个存放递减的高度(每次囤积雨水时将高度更新为囤积后的高度)

public class Solution{
	int trap(int[] height){
		int n = height.length;
		if(n<=2) return 0;
		int res =0;//结果
		int curLeftMax=0;//当前节点左边最大的那个高度(不含当前)
		LinkedList<Integer> stackHeight = new LinkedList<Integer>();
		LinkedList<Integer> stackIndex = new LinkedList<Integer>();
		for(int i=0;i<n;i++){
			int cur = height[i];
			
			if(cur>=curLeftMax){//如果当前节点是新的制高点
				while(!stackHeight.isEmpty()){//更新递减栈,以原来的左边最高为短板,计算雨水
					int h = stackHeight.pop();
					int index = stackIndex.pop();
					if(stackHeight.isEmpty()) break;//最后一个元素,也就是那个原来的左边最高被pop了,可直接退出
					res += (index-stackIndex.peek())*(curLeftMax-h);//雨水=长*高,高是和左最高的差,长是递减栈距离自己左边第一个元素的距离。
				}
				curLeftMax = cur;
				stackHeight.push(cur);
				stackIndex.push(i);
			}else{//当前节点很普通
				while(stackHeight.peek()<=cur){
					int h = stackHeight.pop();
					int index = stackIndex.pop();
					res += (index - stackIndex.peek())*(cur-h);
				}
				stackHeight.push(cur);
				stackIndex.push(i);
			}
		}
		return res;
	}
}

(二)堆

剑指offer41 数据流中的中位数

思路:
首先能想到的是:维持一个有序的数组,每次数据流新加入一个元素的时候,通过二分查找的方式找到<=该元素的位置,右移插入,这种方法太垃圾了;
由于发现要找中位数,左边从小到大,右边从大到小,这样我们存放数据不再用数组,而是两个堆,左边搞一个大根堆Left,右边搞一个小根堆Right,各自存放一半元素。

  • 添加元素x
    • Left的大小==Right的大小,将x放入Right,然后Right新的堆顶放回Left
    • Left的大小>Right的大小,x放入Left,Left新的堆顶放回Right
  • 查找中位数
    • Left的大小==Right的大小,中位数是堆顶的算数平均
    • Left的大小>Right的大小,中位数是Left的堆顶

时间复杂度:查找中位数是O(1),插入元素是O(logN)
空间复杂度:O(N)

class MedianFinder{
	Queue<Integer> Left,Right;
	public MedianFinder(){
		Left = new PriorityQueue<>((x,y)->y-x);//大顶堆,放小的那一半
		Right = new PriorityQueue<>();//小顶堆
	}
	//添加元素
	public void add(int x){
		if(Left.size() == Right.size()){
			Right.add(x);
			Left.add(Right.poll());
		}else{
			Left.add(x);
			Right.add(Left.poll());
		}
	}
	//找到中位数
	public double findMedian(){
		if(Left.size() == Right.size())
			return (Left.peek()+Right.peek())/2;
		else
			return Left.peek();
	}
}

23 合并k个升序链表(三种方法)

方法一:堆选最小,遍历链表
优先队列存放K个链表的当前表头,每次poll()作为结果链表的下一个元素。
时间复杂度:O(KNlogK)KN个元素*堆深度
空间复杂度:O(K)


方法二:依次合并,知道合并完K个,每次用双指针的方法合并
时间复杂度:第一次O(n),第二次O(2N),第三次O(3N)……最后总共O(KKN)
空间复杂度:O(1)只用了双指针

在这里插入代码片

方法三:
分治合并,类似归并,
时间复杂度:第一次K/2组,每小组O(2N),第二次K/4组,每小组O(4N)……,一共O(KNlogK )
空间复杂度:O(logK),K个链表

在这里插入代码片

215 数组的第K个最大的元素

求第K个大的原素,我们首先本能要知道的是这一题可以用堆排序的方法,毕竟堆排序是一种性能为nlogn的排序算法,性能很棒,找到第k个非常合适。但是我们要知道堆排序不是这种题的最好做法——最好的是快速排序
(1)如何用快排找第k大的元素
先复习一下快排:快排是先从第一个元素出发,交换元素,使得该元素左边都比他小,右边都>=它,这样数组就被划分为左右两部分了,然后左右两部分分别快排。
我们可以利用这种划分+分治的思想找第k大的元素

  • 如果划分好后的位置下标是 n-k,返回之
  • 如果划分好后的位置下标在n-k前面,也就是说第k大的元素在它后面,递归调用 右边
  • 如果划分后的位置下标>n-k,递归调用左边
    时间复杂度:O(nlogn),
    空间复杂度:用栈,是logn
class Solution {
    public int findKthLargest(int[] nums, int k) {
        return quicksort(nums,0,nums.length-1,k);
    }
    private int quicksort(int[] a,int left,int right,int k){
        if(left>right)
            return -1;
        int base=a[left],i=left,j=right;
        while(i<j){
            while(a[j]>=base && j>i)
                j--;
            while(a[i]<=base && j>i)//都是带等号的,否则出错
                 i++;
            int temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        a[left] = a[i];
        a[i] = base;
        if(i==a.length-k)
            return a[i];
        else if(i<a.length-k)
            return quicksort(a,i+1,right,k);
        else 
            return quicksort(a,left,i-1,k);
        
    }
}

(2)经典做法——堆
这里也有要注意的地方,求第k个最大元素,建立一个大根堆还是小根堆呢?大跟堆——当堆建立好之后,依次pop出去k-1个元素,此时堆顶就是第k最大元素。
当然我们可以调用java的库函数

PriorityQueue<Integer> heap = new PriorityQueue(n,(a,b)->b-a);//大顶堆要写倒过来

但是面试官一般可能让我们自己建立堆,所以还是老老实实自己实现吧。

堆很简单的,从下到上煎堆,从上到下跟下面的孩子比较交换,最大的最为父亲,如果发生交换,交换后得节点接着和孩子比较更新。

时间复杂度O(nlogn),建立堆O(n),删除k个元素O(klogn)因为k 空间O(1)

public class Solution{
	public int findKthLargest(int[] nums,int k){
	    return heapsort(nums,k);
	}
	public int heapsort(int[] a,int  k){
		int len = a.length;
		for(int i = len/2-1;i>=0;i--)//初始化堆
			heapajust(a,i,len);
		swap(a,0,--len);//找到最大的元素了
		if(k==1)
			return a[len];
		while(len>0){//当未排序长度>0时
			heapajust(a,0,len);
			swap(a,0,--len);
			if(a.length-k==len)
				return a[len];
		}
        return a[0];
	}
	private void heapajust(int[] a,int index,int len){
		int left = index*2+1;
		while(left<len){
			int larger = left;
			if(left+1<len && a[left+1]>a[left])
				larger = left+1;
			if(a[index]<a[larger]){//更大和和当前节点交换,并且当前节点变成交换后的位置,注意更新左孩子left
				swap(a,index,larger);
				index = larger;
				left = index*2+1;
			}else{
				break;
			}
		}
	}
    private void swap(int[] s,int a,int b){
	    int temp = s[a];
	    s[a] = s[b];
	    s[b] = temp;
    }
}

347 前K个高频元素(hash+快排/堆排)

首先用一个HashMap记录每个元素出现的次数,得到一个次数数组,然后需要找到这个次数数组的第k大的元素,注意要求时间复杂度 找出数组第k大的元素不就是上面那一题嘛。
方法一:hash+快排
quick(a[],left,right),将a从小道大排序,以a[left]为基准元素,将a[]分成两部分,左边都=a[left],得到基准元素的位置
如果该位置 如果该位置>n-k,快排右部分
如果该位置=n-k,找到,将它和它右边的元素统统放入结果数组。
时间复杂度:原本的快排是O(nlogn),但是我们现在只需要递归其中一个分支,算法复杂度减低到了O(n);
空间复杂度:O(n)。

class Solution{
	public int[] topKeyFrequent(int[] nums ,int k){
		//哈希表放着num数组元素出现的次数
		Map<Integer,Integer> map = new HashMap<Integer,Integer>();
		for(int item:nums)
			map.put(item,map.getOrDefault(item,0)+1);
			
		//次数数组,如{[1,3],[2,1],[3,2]}代表元素1出现3次,元素2出现1次,元素3出现2次
		List<int[]> list = new ArrayList<int[]>();
		for(Map.Entry<Integer,Integer> entry : map.entrySet()){
			int key = entry.getKey();
			int count = entry.getValue();
			list.add(new int[]{key,count});
		}
		int[] res = new int[k];//结果集
		quicksort(list,0,list.size()-1,res,0,k);
		return res;
	}
	//快排求出list的出现次数前k做多的元素,放在res中
	public void quicksort(List<int[]>,int left,int right,int[] res,int index,int k){
		int base = list.get(left)[1];
		int i = left,j = right;
		while(i<j){
			while(i<j && list.get(j)[1] >= base)
				j--;
			while(i<j && list.get(i)[1] <= base)
				i++;
			int[] temp = list.get(i);
			list.set(i,list.get(j));
			list.set(j,temp);
		}
		int[] temp = list.get(left);
		list.set(left,list.get(i));
		list.set(i,temp);
		if(i==list.size()-k){
			while(i<list.size()){
				res[index] = list.get(i)[0];
				index++;
				i++;
			}
		}else if(i<list.size()-k){
			quicksort(list,i+1,right,res,0,k);
		}else{
			quicksort(list,left,i-1,res,0,k);
		}
	}
}

方法二:hash+堆排
就是用堆排序,这一题不能完全按照上面的那种搞一个n元素的大顶堆,全部放进堆里面,最后从堆里面pop出k个元素的方法,因为如果那么做的话时间复杂度会达到nlogn
所以我们建立维护一个k个元素的小顶堆,遍历次数数组,当前元素<=堆顶,不管当前元素;当前元素>堆顶,插入堆。
注意——求最大k个用小顶堆,求最小k个用大顶堆
时间复杂度O(nlogk)
空间复杂度:O(N)

class Solution{
	public int[] topKFrequent(int[] nums,int k){
	//哈希表放着num数组元素出现的次数
		Map<Integer,Integer> map = new HashMap<Integer,Integer>();
		for(int item:nums)
			map.put(item,map.getOrDefault(item,0)+1);
		//最小堆
		PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>(){
			public int compare(int[] m,int[] n){
				return m[1]-n[1];
			}		
		});
		//遍历维护这个最小跟堆
		for(Map.Entry<Integer,Integer> entry : map){
			int key = entry.getKey();
			int count = entry.getValue();
			if(queue.size()==k){
				if(queue.peek()[1] < count){
					queue.poll();
					queue.offer(new int[]{key,count});
				}
			}else{
				queue.offer(new int[]{key,count});
			}
		}
		//从堆中得到返回值
		int[] res = new int[k];
		for(int i=0;i<k;i++)
			res[i] = queue.poll()[0];
		return res;
	}
}

(三)队列

剑指offer59I 滑动窗口的最大值(单调队列)

题目:给定一个数组和滑动窗口大小为k,求出每次滑动窗口的最大值
思路:
用int[n-k+1] res存放每个滑动窗口的最大值,
用一个队列作为辅助,队列头存放当前窗口的最大值,每次向后滑动窗口的时候,能影响新窗口最大值取值的元素有

  1. 原来窗口的最大值,看看是否被移出
  2. 原来窗口的次最大值
  3. 当前新元素

我们可以发现,前面窗口的从最大值开始向后面遍历,次最大值、次次最大值、次次次最大值会影响结果。
所以我们可以维护一个单调非严格递减队列,保证队列中的元素是从头到尾的最大值、次最大值、次次最大值……,注意后面出现的元素如果比队列尾部的一些元素小,会覆盖他们变成新的次大元素,他们就没了。

class solution{
	public int[] maxSlidingWindow(int[] nums,int k){
		//边界
		if(nums.length == 0 || k==0)
			return new int[0];
		//我们写队列最好是用Deque这个API
		Deque<Integer> deque = new LinkedList<>();
		
		//先构造第一个窗口
		for(int i=0;i<k;i++){
			//后面出现的元素如果比队列尾部的一些元素小,会覆盖他们变成新的次大元素
			while(!deque.isEmpty() && deque.peekLast() < nums[i])
				deque.removeLast();
			deque.addLast(nums[i]);
		}
		res[0] = deque.peekFirst();
		
		//下面是滑动窗口阶段
		for(int i=k;i<nums.length; i++){
			if(deque.peekFirst() == nums[i-k])
				deque.removeFirst();
			while(!deque.isEmpty() && deque.peekLast() < nums[i])
                deque.removeLast();
            deque.addLast(nums[i]);
            res[i-k+1] = dequeue.peekFirst();
		}
		return res;
	}
}


剑指offer59II 队列的最大值

要求出队 入队 求最大值的函数都是O(1)复杂度
思路:单调队列
搞一个非递增辅助双端队列max
max的对头始终是当前队列的最大值,

  • 插入元素时,如果新元素是的东西,不管他,直接放进入,队伍前面有大家伙撑着;如果是大家伙,max就要更新了,将队伍前面新来的小的家伙淘汰掉,再将这个大家伙放进入当升职当长老。
  • poll元素的时候,如果和max顶相同也要一起poll,否则不管max
class MaxQueue{
	Queue<Integer> queue;
	Deque<Integer> max;
	public MaxQueue(){
		queue = new LinkedList<>();
		max = new LinkedList<>();
	}
	//入队
	public void push_back(int value){
		queue.add(value);
		while(!max.isEmpty() && max.peekLast() < value)
			max.removeLast();
		max.addFirst(value);
	}
	//出队
	public int pop_front(){
		if(!max.isEmpty() && queue.peek().equals(max.peekFirst()))
			max.removeFirst();
		return queue.size()==0?-1:queue.poll();
	}
	//最大值
	public int max_value(){
		return max.size()==0?-1:max.peek();
	}
}

621 任务调度器(用优先队列太麻烦了,填桶策略)

冷却n,一个桶子>n+1,桶子里智能放不同的任务,行数由频数最大 的那个元素数量决定。

  • 任务种类很少的时候,填不满n+1
    我的刷题之旅——栈、堆和队列_第1张图片
  • 任务种类很多的时候,桶子放不下,一行放不同可以尽情的放超过n+1都可以执行
    我的刷题之旅——栈、堆和队列_第2张图片
  • 我们就计算第一种就行了,和任务长度间取一个最大值做返回值
class Solution{
	public int leastInterval(char[] tasks,int n){
		int[] counts = new int[26];//存放的是每种任务数量
		for(char c : tasks)
			counts[c-'A']+=1;
		int max = 0;
		for(int count : counts) max = Math.max(max,count);
		int maxCount = 0;		//最后一行还剩的元素数量
		for(int count:counts)
			if(count == max)
				maxCount++;
		return Math.max(tasks.length, (n+1)*(max-1)+maxCount);
	}
}

(四)并查集(一般用于判断图中是否有环,也可以用于求连通分量个数)

求连通分量的个数,比如我们 200和547,其实最好还是用宽搜或者深搜,并查集效率太低了。
这里了解即可。

128最长连续序列——(字节原题,并查集和哈希表法)

(1)哈希表+优化
我们考虑枚举数组中的每个数 x,考虑以其为起点,不断尝试匹配 x+1, x+2, ⋯ 是否存在,假设最长匹配到了 x+y,那么以 x 为起点的最长连续序列即为 x, x+1, x+2,⋯,x+y,其长度为 y+1,我们不断枚举并更新答案即可。

其实更高效的方法是用一个哈希表存储数组中的数,这样查看一个数是否存在即能优化至 O(1) 的时间复杂度。

仅仅是这样我们的算法时间复杂度最坏情况下还是会达到 O(n^2),无法满足题目的要求。
但仔细分析这个过程,我们会发现其中执行了很多不必要的枚举,如果已知有一个 x, x+1, x+2⋯,x+y 的连续序列,而我们却重新从 x+1,x+2 或者是 x+y 处开始尝试匹配,那么得到的结果肯定不会优于枚举 x为起点的答案,因此我们在外层循环的时候碰到这种情况跳过即可。

那么怎么判断是否跳过呢?由于我们要枚举的数 x 一定是在数组中不存在前驱数 x-1 的,不然按照上面的分析我们会从 x-1开始尝试匹配,因此我们每次在哈希表中检查是否存在 x-1 即能判断是否需要跳过了。

class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for(int a:nums)
            set.add(a);
        int maxlen = 0;
        for(int a:nums){
            if(a==Integer.MIN_VALUE || !set.contains(a-1)){//小心边界
                int curlen = 1;
                int curnum = a;
                while(a != Integer.MAX_VALUE && set.contains(curnum+1)){//小心边界
                    curnum++;
                    curlen++;
                }
                maxlen = Math.max(maxlen,curlen);
            }
        }
        return maxlen;
    }
}

方法二:并查集
能够想到使用并查集来解决,关键是将连续的整数视为一个集合,当然最小的集合大小就是1,元素本身。因此在合并时将该数x与x+1合并,(如果x+1也在数组中),并计算集合大小的最大值就好了

class Solution {
	//并查集类
    class UnionFind{
        Map<Integer, Integer> parents;
        public UnionFind(int[] arr) {//初始化,map<自己,祖先>
            parents = new HashMap<>();
            for (int i : arr) {
                parents.put(i, i);
            }
        }
        public Integer find(int x) {//查找x的祖宗
            if (!parents.containsKey(x)) return null;
            int t = parents.get(x);
            if(x != t) 
            	parents.put(x, find(t));//如果x的祖先不是自己,更新x的祖先
            return parents.get(x);//最后返回x的祖先
        }
        public boolean union(int x, int y) {//将x和y变成一个集合关系(具有祖先子孙关系)
            Integer rootX = find(x), rootY = find(y);
            if (rootX == null || rootY == null) return false;
            if(rootX.equals(rootY)) return false;
            parents.put(rootX, rootY);
            return true;
        }
    }
//-------------------------------------------
	//解题
    public int longestConsecutive(int[] nums) {
        if (nums.length == 0) return 0;
        UnionFind u = new UnionFind(nums);
        for (int num : nums) {//构造并查集,每次合并num 和 num+1
            u.union(num, num + 1);
        }
        int max = 1;
        for (int num : nums) {
            max = Math.max(max,u.find(num) - num + 1);
        }
        return max;
    }
}

547 朋友圈——(并查集)

给你二维数组,找出数组中岛屿数量。

int fa[1000];
int find(int x){//查找x的祖先节点
        if(fa[x]==x) return x;
        return fa[x]=find(fa[x]);
}
//查找连通分量个数
int findCircleNum(int** M, int MSize, int* MColSize){
    for(int i=0;i<MSize;i++)   fa[i]=i;
    int ans=MSize;//连通分量默认有MSize个
    for(int i=0;i<MSize;i++)//遍历行
    {
        for(int j=0;j<i;j++)//遍历列
        {
            if(M[i][j]==0) 
            	continue;
            if(find(fa[i])!=find(fa[j]))//如果该两个点间不同祖先
            {
                fa[find(i)]=fa[find(j)];//祖先合并为一家
                ans--;//连通分量--
            }
        }
    }
    return ans;
}

被围绕的区域

找到所有被X包围的O,将这些O改为X

  • 将各个坐标映射到一维,范围在[0, mn-1],同时定义一个超级源点src在最后位置mn,这个源点与所有边缘的’O’相连
  • 对于在里边的’O’来说,将其与上下左右四个方向的’O’连接起来
  • 最后在遍历一遍矩阵,将没有与src连接在一块的’O’置为’X’
class Solution {
    // 定义并查集——很简单,并查集都是这么定义的
    class UnionFind{
        int[] parents;
       	public UnionFind(int size){
            parents = new int[size];
            for(int i = 0; i < size; i++)
                parents[i] = i;
        }
        //找x的祖先
        public int find(int x){
            if(parents[x] == x)
                return x;
            return parents[x] = find(parents[x]);
        }
        //合并x和y
        public void union(int x, int y){
            int px = find(x);
            int py = find(y);
            if(px == py)
                return ;
            parents[px] = py;
        }
		//x和y是否在同一个连通图里
        public boolean isConnect(int x, int y){
            return find(x) == find(y);
        }
    }

    int[][] d = new int[][]{{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
    int m;
    int n;
	//求解
    public void solve(char[][] board) {
        m = board.length;
        if(m == 0) return;
        n = board[0].length;    
        int size = m * n + 1;
        int src = m * n;
        UnionFind u = new UnionFind(size);
        //遍历这个图,处理并查集
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(board[i][j] == 'X')
                    continue;
                if(i == 0 || i == m-1 || j == 0 || j == n-1)// 是边缘的'O'与超级源点src相连接
                    u.union(src, i * n + j);
                else{//不是边缘的‘O’
                    for(int k = 0; k < 4; k++){ // 将周围的'O'与(i, j)连接
                        int x = i + d[k][0];
                        int y = j + d[k][1];
                        if(board[x][y] == 'O')
                            u.union(i * n + j , x * n + y);
                    }
                }
            }
        }
        //最后根据并查集的结果处理图,不和周围的‘O’相联的O都改成“X”
        for(int i = 1; i < m-1; i++){
            for(int j = 1; j < n-1; j++){
                if(board[i][j] == 'O' && !u.isConnect(src, i * n + j)){
                    board[i][j] = 'X';
                }
            }
        }
    }
}

你可能感兴趣的:(算法刷题,算法,队列)