代码随想录算法训练营Day11|栈与队列part02

119 阅读5分钟

LeetCode 150 逆波兰表达式求值

题目链接:https://leetcode.cn/problems/evaluate-reverse-polish-notation/

文档讲解:programmercarl.com/0150.逆波兰表达式…

视频讲解:www.bilibili.com/video/BV1kd…

思路

逆波兰表达式可以用栈来计算,对表达式中每个字符:

  1. 如果是数字,入栈
  2. 如果是运算符,栈顶元素b出栈,取栈顶元素a出栈。计算a运算符b,把结果入栈
  3. 当字符被遍历完,栈中剩余的数字就是表达式的值

解法

class Solution {

	public int evalRPN(String[] tokens) {
	
		Stack<Integer> stack = new Stack<>();
		
		for (int i = 0; i < tokens.length; i++) {
		
			if (tokens[i].equals("+")) {
			
				int b = stack.pop();
				
				int a = stack.pop();
				
				stack.push(a + b);
			
			}
			
			else if (tokens[i].equals("-")) {
			
				int b = stack.pop();
				
				int a = stack.pop();
				
				stack.push(a - b);
			
			}
			
			else if (tokens[i].equals("*")) {
			
				int b = stack.pop();
				
				int a = stack.pop();
				
				stack.push(a * b);
			
			}
			
			else if (tokens[i].equals("/")) {
				
				int b = stack.pop();
				
				int a = stack.pop();
				
				stack.push(a / b);
			
			}
			
			else {
			
				stack.push(Integer.parseInt(tokens[i]));
			
			}
		
		}
		
		return stack.pop();
	
	}

}

LeetCode 239 滑动窗口最大值

题目链接:leetcode.cn/problems/sl…

文档讲解:programmercarl.com/0239.滑动窗口最大…

视频讲解:www.bilibili.com/video/BV1XS…

思路

暴力解法

对每个长为k的滑动窗口,遍历其中元素寻找最大值,这样的滑动窗口有n-k+1个。故时间复杂度为O(nk)O(nk).那么如何在线性时间内解决这个问题呢?

单调队列法

由于我们需要求出的是滑动窗口的最大值,如果当前的滑动窗口中有两个下标 i 和 j,其中 i 在 j 的左侧(i<j),并且 i 对应的元素不大于 j 对应的元素(nums[i]≤nums[j]),那么会发生什么呢?

当滑动窗口向右移动时,只要 i 还在窗口中,那么 j 一定也还在窗口中,这是 i 在 j 的左侧所保证的。因此,由于 nums[j] 的存在,nums[i] 一定不会是滑动窗口中的最大值了,我们可以将 nums[i] 永久地移除。

作者:力扣官方题解 链接:leetcode.cn/problems/sl… 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

因此我们可以使用一个队列存储所有仍有可能作为滑动窗口中最大值的数的下标。在队列中,这些下标下标从小到大的顺序被存储,并且它们在nums中的值严格单调递减。 当滑动窗口向右移动时,我们需要考虑对元素的删除和添加。

添加元素nums[i] (下述元素指代下标对应的值)

如果当前元素大于等于队尾元素,那就不断弹出队尾元素直到队列为空或队尾元素大于当前元素,再把元素入队

删除元素

当前滑动窗口最后一个下标为i,窗口宽度为k,故下标小于等于i-k的都不在窗口内。故我们需要在队首弹出小于i-k的下标

获取当前最大值

队列中下标对应的值单调递减,完成队元素的添加和删除后,队首下标对应的元素就是当前窗口内的最大值。

单调队列的实现(Java)

Deque 是 Java 中的一个接口,表示双端队列(Double-Ended Queue),它是一个支持在队列的两端插入和移除元素的线性数据结构。双端队列既可以用作队列(FIFO),也可以用作栈(LIFO)。 它在 java.util 包中定义,常见实现类包括:

  • LinkedList
  • ArrayDeque

解法

class Solution {

	public int[] maxSlidingWindow(int[] nums, int k) {
	
		Deque<Integer> deque = new LinkedList<>();
		
		int[] result = new int[nums.length - k + 1];
		
		for (int i = 0; i < k; i++) {
		
			while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
			
				deque.pollLast();
			
			}
			
			deque.offerLast(i);
		
		}
		
		result[0] = nums[deque.peekFirst()];
		
		for (int i = k; i < nums.length; i++) {
		
			if (deque.peekFirst() <= i-k) {
			
				deque.pollFirst();
			
			}
			
			while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
			
				deque.pollLast();
			
			}
			
			deque.offerLast(i);
			
			result[i-k+1] = nums[deque.peekFirst()];
		
		}
		
		return result;
	
	}

}

LeetCode 347 前K个高频元素

题目链接:leetcode.cn/problems/to…

文档讲解:programmercarl.com/0347.前K个高频元…

视频讲解:www.bilibili.com/video/BV1Xg…

思路

此题主要包含以下三个操作:

  1. 统计元素出现的频率
  2. 对频率非递增排序
  3. 找到前K个频率 统计频率可以使用哈希表Map。 那么要如何排序键值对呢?如果只是简单的对频率排序,时间复杂度无法优于O(nlogn)O(nlogn)。在这里我们可以使用优先级队列解决这个问题。

优先级队列

优先级队列本质上等同于堆,队内元素非递减排列即最小堆,非递增排列即最大堆。 堆是由二叉树实现的,是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。 堆每次更新后可以在堆顶获得最大值/最小值,更新的复杂度为O(logn)O(logn)

那我们应该选择最大堆or最小堆? 如果选择最大堆,需要维护所有节点,在最后依次弹出K个元素;而选择最小堆只要维护K个节点,每次遇到比堆顶元素大的元素,就弹出堆顶,把当前元素加入堆,最后留下来的K个节点就是前K个元素。

优先级队列PriorityQueue

PriorityQueue基于最小堆(小顶堆) 实现。队列头部总是最小的元素(自然顺序)或优先级最高的元素(自定义顺序)。

算法

  1. 把元素频率记录在HashMap中,数值为键,频率为值
  2. 建立一个容量为K的优先级队列,先加入K个键,更新一次
  3. 对后续所有键遍历:
    1. 如果频率大于堆顶,堆顶弹出,当前键加入堆
    2. 否则跳过
  4. 把队列转为整型数组返回

解法

class Solution {

	public int[] topKFrequent(int[] nums, int k) {
	
		HashMap<Integer, Integer> map = new HashMap<>();
		
		for (int i = 0; i < nums.length; i++) {
		
			int key = nums[i];
			
			int value;
			
			if (map.containsKey(key)) {
			
				value = map.get(key);
			
			}
			
			else {
			
				value = 0;
			
			}
			
			map.put(key, value+1);
		
		}
		
		PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
		
			@Override
			public int compare(Integer a, Integer b) {
			
				return map.get(a) - map.get(b);
			
			}
		
		});
		
		Integer[] keys = map.keySet().toArray(new Integer[0]);
		
		for (int i = 0; i < k; i++) {
		
			priorityQueue.offer(keys[i]);
		
		}
		
		for (int i = k; i < keys.length; i++) {
		
			if (map.get(keys[i]) > map.get(priorityQueue.peek())) {
				
				priorityQueue.poll();
				
				priorityQueue.add(keys[i]);
			
			}
		
		}
		
		Integer[] tmp = priorityQueue.toArray(new Integer[0]);
		
		int[] result = new int[tmp.length];
		
		for (int i = 0; i < result.length; i++) {
		
			result[i] = tmp[i];
		
		}
		
		return result;
	
	}

}

栈与队列总结

哈希表算法总结.png

今日收获总结

今日学习4小时,利用栈与队列的自有属性是解决问题很巧妙的方法,所以实战里对这种题最重要的是能想起来用什么数据结构。