摘要
本文介绍了几个栈相关的LeetCode问题,包括239.滑动窗口最大值和347.前 K 个高频元素,并在最后由ChatGPT对栈和队列进行了总结。
1、239.滑动窗口最大值
1.1 暴力法
- 双层遍历,外层控制遍历次数,内层求滑动窗口的最大值
public int[] maxSlidingWindow(int[] nums, int k) {
int size = nums.length-k+1;
int[] arr = new int[size];
for(int i=0; i<size; i++) {
int max = nums[i];
for(int j=i+1; j<i+k; j++) {
max = Math.max(max, nums[j]);
}
arr[i] = max;
}
return arr;
}
1.2 单调栈
- 使用单调队列(LinkedList),队列中元素单调递减
- 遍历数组,准备将当前元素加入到单调队列中,注意待加入的是元素下标,通过元素下标可以获取元素的值
- 从尾部开始遍历单调队列,若当前元素的值大于等于单调队列的元素的值,删除单调队列的元素,反之则终止遍历
- 将元素的下标加入单调队列
- 校验单调队列队首的下标是否在有效范围,如果不在有效范围, 则移除队首元素
- 将单调队列中的队首元素加入的返回结果中
1、单调队列元素满了怎么办?
首先单调队列中存储的是数组中元素的下标,通过下标可以获取到数组中相应元素的值。并且通过下标,我们可以判断单调队列中的队首元素是否在滑动窗口的有效的下标范围,如果不存在于有效的下标范围则移除队首元素。
2、LinkedList 的API
- peekLast:获取列表的最后一个元素
- pollLast:删除并返回列表的最后一个元素
- addLast:将元素添加到列表的末尾
- peek:获取队列的第一个元素,但不删除
- poll:从队列中获取并移除第一个元素
public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length;
int[] arr = new int[len-k+1];
LinkedList<Integer> queue = new LinkedList<>();
for(int i=0; i<len; i++) {
while(!queue.isEmpty() && nums[i] >= nums[queue.peekLast()]) {
queue.pollLast();
}
queue.addLast(i);
if(queue.peek() <= i-k) {
queue.poll();
}
if(i+1-k >= 0) {
arr[i+1-k] = nums[queue.peek()];
}
}
return arr;
}
2、347.前 K 个高频元素
2.1 思路
- 遍历数组,构建HashMap,key 表示元素的值,value 表示次数
- 遍历HashMap,按次数从小到大排序,构建优先级队列PriorityQueue,并且队列大小为 k
- 取出优先级队列PriorityQueue中的元素,加入到结果中
1、PriorityQueue 的API
-
自定义比较器:
-
PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() { @Override public int compare(Integer a, Integer b) { return map.get(a) - map.get(b); } });
-
-
offer: 将指定的元素添加到队列中,如果队列已满,则返回
false,否则返回true。 -
peek: 返回队列中具有最高优先级的元素,但不会从队列中删除它。如果队列为空,返回
null。 -
remove: 同 poll,如果队列为空,会抛出异常。
-
poll:移除并返回队列中具有最高优先级的元素。如果队列为空,返回
null。
2.2 代码
public int[] topKFrequent(int[] nums, int k) {
// 构建HashMap
Map<Integer, Integer> map = new HashMap<>();
for(int num: nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
// 构建优先级队列
PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return map.get(a) - map.get(b);
}
});
for (Integer key : map.keySet()) {
if(pq.size() < k) {
pq.offer(key);
} else {
if(map.get(key) > map.get(pq.peek())) {
pq.remove();
pq.offer(key);
}
}
}
// 构建返回结果
int[] arr = new int[k];
int i = 0;
while(!pq.isEmpty()) {
arr[i] = pq.poll();
i++;
}
return arr;
}
3、总结
栈与队列是常见的数据结构,它们在算法和编程中起着重要作用。以下是几个使用栈与队列解决的LeetCode问题的总结:
-
用栈实现队列(232. Implement Queue using Stacks) :
- 使用两个栈,一个用于入队列,一个用于出队列。
- 入队列时,将元素压入入队栈中。
- 出队列时,如果出队栈为空,将入队栈的元素逐个弹出并压入出队栈,然后弹出出队栈顶元素。
- 时间复杂度:入队列O(1),出队列均摊O(1)。
-
用队列实现栈(225. Implement Stack using Queues) :
- 使用两个队列,一个主队列,一个辅助队列。
- 入栈时,将元素添加到主队列中。
- 出栈时,将主队列中的元素移至辅助队列,直到只剩一个元素,然后弹出该元素。
- 时间复杂度:入栈O(1),出栈均摊O(1)。
-
有效的括号(20. Valid Parentheses) :
- 使用栈来检查括号的有效性。
- 遍历字符串,遇到左括号入栈,遇到右括号时检查栈顶是否匹配,若匹配则出栈。
- 最终栈为空则括号有效。
- 时间复杂度:O(n)。
-
删除字符串中的所有相邻重复项(1047. Remove All Adjacent Duplicates In String) :
- 使用栈来消除相邻重复项。
- 遍历字符串,将字符与栈顶元素比较,若相同则弹出栈顶元素,否则入栈。
- 最终栈中的字符即为结果。
- 时间复杂度:O(n)。
-
逆波兰表达式求值(150. Evaluate Reverse Polish Notation) :
- 使用栈来计算逆波兰表达式。
- 遍历表达式,遇到数字入栈,遇到运算符弹出栈顶两个数字进行运算,将结果入栈。
- 最终栈中的元素即为结果。
- 时间复杂度:O(n)。
-
滑动窗口最大值(239. Sliding Window Maximum) :
- 使用双端队列来维护滑动窗口的最大值。
- 遍历数组,将元素依次入队列。
- 维护队列的单调性,保证队列首元素为当前窗口的最大值。
- 时间复杂度:O(n)。
-
前 K 个高频元素(347. Top K Frequent Elements) :
- 使用哈希表统计元素频率,并将元素按频率排序。
- 使用最小堆(优先队列)维护前 K 个高频元素。
- 遍历哈希表,将元素入堆,保持堆大小为 K。
- 时间复杂度:O(nlogK)。
这些问题展示了如何巧妙地运用栈和队列来解决各种不同类型的问题,包括字符串处理、数值计算和数据筛选等。熟练掌握栈与队列的使用对算法和数据结构的理解和应用非常重要。