239.滑动窗口最大值
题目详细:LeetCode.239
看到滑动窗口,我立马想起了双指针,利用双指针可以非常清晰地理解解题思路:
- 定义一个变量 max_i 用于记录窗口中的最大值的索引
- 每次窗口滑动后
- 如果出去的值是最大值,那么新的窗口则需要重新查找最大值并设置 max_i
- 这里使用双指针查找窗口中的最大值
- 如果出去的值不是最大值,则判断新进来的值是否大于当前的最大值
- 不大于则最大值可不变
- 大于则将进来的值作为最大值
- 如果出去的值是最大值,那么新的窗口则需要重新查找最大值并设置 max_i
这里我让 max_i 维护的是一个最大值的索引,当然维护一个最大值也是可以的,思路都是一样的
Java解法(双指针,维护最大值索引):
class Solution {
public int getMaxIndex(int[] nums, int l, int r){
while(l < r){
if(nums[l] < nums[r]){
l++;
}else{
r--;
}
}
return l;
}
public int[] maxSlidingWindow(int[] nums, int k) {
int max_i = -1;
List<Integer> ans = new ArrayList<>();
for(int i = 0; i <= nums.length - k; i++){
int l = i, r = i + k - 1;
if(max_i == -1 || max_i < l){
//如果出去的值是最大值(的索引),那么新的窗口需要重新查找最大值并设置
max_i = getMaxIndex(nums, l, r);
}else{
//如果出去的值不是最大值(的索引),则判断新进来的值是否大于当前的最大值
//不大于则最大值(的索引)不变
//大于则将进来的值(的索引)作为最大值(的索引)
if(nums[r] >= nums[max_i]){
max_i = r;
}
}
ans.add(nums[max_i]);
}
return ans.stream().mapToInt(Integer::valueOf).toArray();
}
}
- 解题思路非常好理解,通过观察滑动窗口的特点也可以发现,其具备了队列先进先出的特点,所以也可以视作是利用双指针模拟了一个单向队列
- 但这道题用Java来解,这样写的话在经过LeetCode测试用例时,会出现超出时间限制的问题
- 所以我尝试使用队列来解题,
思路是一样的,只是利用队列来维护最大值的索引
,发现就不会出现超出时间限制的问题了
Java解法(队列,维护最大值索引):
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
List<Integer> ans = new ArrayList<>();
Deque<Integer> dq = new LinkedList<>();
for(int i=0; i < nums.length; i++){
if(!dq.isEmpty() && i - dq.peek() == k){
dq.poll();
}
while(!dq.isEmpty() && nums[i] > nums[dq.peekLast()]){
dq.pollLast();
}
dq.add(i);
if(i >= k - 1){
ans.add(nums[dq.peek()]);
}
}
return ans.stream().mapToInt(Integer::valueOf).toArray();
}
}
347.前 K 个高频元素
题目详细:LeetCode.347
基本思路:
- 为了统计每一个元素出现的频率,可以定义一个HashMap<数值, 频率>来进行统计
- 统计完后,我们只需要对每个元素的频率从大到小进行排序,然后取出前K个频率对应元素即可
难点:
- 本题的难点在于怎么对元素的出现频率进行排序,能够降低时间复杂度
- 若使用快排对频率进行排序,其算法的时间复杂度是O(n*logn)
由题目可知:结果只需要前K个高频的元素
- 那么我们可以始终维护一个大小为K的小顶堆
- 对每次进堆的元素都做一次排序,使出现频率小的作为堆顶弹出
- 最后堆中只会留下K个频率高的元素
- 这样的算法的时间复杂度是O(n*logK)
在Java中, 是使用优先队列(PriorityQueue)来实现堆结构的,小顶堆即是指维护元素从小到大排序的队列。
Java解法(小顶堆排序(优先队列)):
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for(int n : nums){
map.put(n, map.containsKey(n) ? map.get(n) + 1 : 1);
}
PriorityQueue<Map.Entry<Integer, Integer>> heap = new PriorityQueue<>((a,b) -> a.getValue()-b.getValue());
for(Map.Entry<Integer, Integer> entry: map.entrySet()){
heap.offer(entry);
if(heap.size() > k){
heap.poll();
}
}
return heap.stream().mapToInt(e -> e.getKey()).toArray();
}
}
这道题让我第一次接触到了优先队列,发现优先队列的实现其实就是堆的实现
- Java给我们提供的优先队列容器(PriorityQueue)也就是堆容器,有大顶堆和小顶堆两种实现
- PriorityQueue 默认实现的是小顶堆
new PriorityQueue<>((a,b) -> b - a)
,利用匿名重写其底层的比较方法,可以使其实现为大顶堆
总结
栈和队列是两个常用的数据结构:
- 栈的特点是
后进先出
,队列的特点是先进先出
在Java中,队列的形式是多种多样的:
- 栈(Stack)其底层是用双向队列(Deque)实现的
- 因为双向队列在两端都实现了进队和出队操作
- 既有
后进先出
也有先进先出
的特点,所以只要堵住一端,就变成了栈
- 常见的队列实现类有LinkedList、ArrayQeque和PriorityQueue
- LinkedList:实现了Queue接口,所以可以将LinkedList直接当作队列来使用,Queue接口只是窄化了LinkedList方法的访问权限
- ArrayQeque:实现了Queue接口,底层是利用数组存储,模拟队列的存储方式,是队列的线性实现
- PriorityQueue:继承自AbstractQueue抽象类,在队列的实现基础上增加了维持队内元素次序的方法,常用于维护一个堆结构(大顶堆/小顶堆)
- 还有阻塞队列(BlockingQueue)等等
经过对栈和队列的学习,让我对栈和队列各自的特点都有了非常深的印象,同时发现这两种数据结构虽然存储方式差异挺大,但两者其实相辅相成,灵活运用起来的时候可以非常迅速,非常清晰地解决一些问题。
而且在以前遇到滑动窗口相关的问题时,只会往双指针法或者暴力法方面思考,在学习过后,发现滑动窗口的滑动过程,其实也类似于队列左端出队,右端进队的过程,在以后解决相关问题时,又多了一种可以尝试的数据结构。
还有以前经常听人说优先队列,但是一直不知道优先队列是什么,但是通过刷题才发现,原来优先队列就是堆结构,还是算收获蛮大的。
栈和队列的理论基础很简单,但是它们在实际应用中,却是千变万化、难上加难,即各有千秋又相辅相成,可以延伸出许许多多各具特点的数据结构和各种殊途同归的解决思路来,真叫人:
纸上得来终觉浅,绝知此事要躬行。