Leetcode题目-239. Sliding Window Maximum
链接: 239. Sliding Window Maximum
思路
滑动窗口的移动,就带有很明显的先进先出的特点。所以可以想到此题需要用到队列。但是若使用普通的队列,要求窗口内的最大值,必然在每一次窗口移动后,遍历队列中的元素。这样做的话,复杂度是O(n × k)。
为了降低复杂度,我们期望构成一个队列,能在队头始终取到当前队列的最大值。大顶栈能满足这一条件,但是大顶栈在此题中又存在一个问题,在移动窗口时,无法正确获得要被移出的元素,所以要考虑构造新的队列形式。
我们希望它:
- 队列中最大值始终在队头,且队内元素按单调递减排列。
- 元素进入时,将其与队尾数(当前最小值)比较,如果它比队尾数值大,则当这个元素进入后,队尾元素则没有了成为最大值的潜力(因为只要当前元素进入了,队尾元素作为先进入的元素,一定比当前元素先出去,而它又比当前元素小,只要当前元素在,它就没办法是最大的元素),所以队尾元素可以提前出队了,没有必要参与后续的流程了。
- 移出元素时,如果这个元素为最大值,则其一定处在队头,正常移除即可;如果其不为最大值,则它在进队时已被提前移出了。
如此构建我们期望的一个单调队列,移动窗口的过程中就是使用这个队列进行add和remove。此时因为每个元素仅进队、出队一次,复杂度为O(n)。
代码实现
class Solution {
class MyQueue {
Deque<Integer> deque;
public MyQueue() {
deque = new LinkedList<>();
}
public void add(int x) {
while (!deque.isEmpty() && deque.peekLast() < x) {
deque.removeLast();
}
deque.addLast(x);
}
public void remove(int x) {
if (!deque.isEmpty() && deque.peekFirst() == x) {
deque.removeFirst();
}
}
public int peak() {
return deque.peekFirst();
}
}
public int[] maxSlidingWindow(int[] nums, int k) {
MyQueue deque = new MyQueue();
int[] res = new int[nums.length - k + 1];
int j = 0;
for (int i = 0; i < k; i++) {
deque.add(nums[i]);
}
res[j++] = deque.peak();
for (int i = k; i < nums.length; i++) {
deque.remove(nums[i - k]);
deque.add(nums[i]);
res[j++] = deque.peak();
}
return res;
}
}
总结
时间复杂度:O(n)
空间复杂度:O(k),因为构造了大小为k的辅助队列
Leetcode题目-347. Top K Frequent Elements
链接: 347. Top K Frequent Elements
思路
首先因为要统计元素出现的次数,容易想到使用一个map来管理元素值与元素出现的次数。但如何获取map中value最大的k个值,如果直接对map进行排序,即使使用快速排序,其复杂度也是O(nlogn)。而题目中要求了,复杂度必须优于O(nlogn)。
这里考虑使用优先级队列来优化map中元素排序的流程。
我们期望达到以下效果:
- 队列内元素顺序排列,头部为最小的元素,通过不断add新元素和remove队头的最小元素,维护最大的k个值。
- 队列对元素进行处理的复杂度小于O(nlogn)。
根据优先级队列的概念,可知第一条可以满足,对于第二条,需要了解下优先级队列的实现方式。
优先级队列
特点
- PriorityQueue中放置的元素按自然顺序排列,自己构造的类或无法比较的类需自己实现Comparable 和 Comparator 接口。
- 不支持null元素。
- 可以自动扩容。
- 插入和删除元素的时间复杂度均为 O(log n)
- PriorityQueue底层使用了堆数据结构
底层实现
优先级队列的底层数据结构是二叉堆,即完全二叉树。PriorityQueue的默认顺序是小的元素为优先级高的元素(小顶堆),则堆中任一节点的值都不小于其父节点的值。堆的底层是数组。如下图。
PriorityQueue可以通过传入自定义的比较器来规定排序规则,所以其也可以改造为大顶堆。
add()方法
add元素之后,为了维持原有的堆结构,会进行以下步骤:
- 将元素加入数组末尾,即最后一个叶子结点上;
- 与父节点比较,若小于父节点,则与父节点交换位置;
- 重复2直至满足添加节点大于其父节点。
如图:
poll()方法
poll元素之后,为了维持原有的堆结构,会进行以下步骤:
- 因为poll的为根节点的元素(数组的第一个),根节点不能直接删除,先用数组最后一个元素代替根节点。
- 再按照堆的规则进行调整,若父节点大于子节点,则将父节点与最小的子节点进行交换,直至满足父节点小于两子节点为止。
如图:
由以上可知,插入和删除元素的时间复杂度均为 O(log n)
我们使用一个优先级队列,将每次的map键值对(即数据元素及其出现的次数)塞进去,并按出现次数排列,当数量超过k个的时候,将队首元素(当前count最小的键值对)poll出,这样当map遍历完时,优先级队列内的k个元素即为前k个高频元素。将其对应的key值放到一个int数组里返回即可。
代码实现
这里我自定义了一个数据结构作为优先级队列的元素类型,并根据数据类型实现了其比较的规则。
class Solution {
public class NumWithCount {
int num;
int count;
public int getCount() {
return count;
}
public int getNum() {
return num;
}
public NumWithCount(int num, int count) {
this.num = num;
this.count = count;
}
}
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
PriorityQueue<NumWithCount> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(num -> num.count));
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (priorityQueue.size() < k) {
priorityQueue.add(new NumWithCount(entry.getKey(), entry.getValue()));
} else if (entry.getValue() > priorityQueue.peek().getCount()) {
priorityQueue.poll();
priorityQueue.add(new NumWithCount(entry.getKey(), entry.getValue()));
}
}
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = priorityQueue.poll().getNum();
}
return result;
}
}
总结
时间复杂度:O(n log k),遍历元素并计数到map里的复杂度是O(n),当每个元素进入优先级队列时,复杂度是O(log k),一共有n个元素需要进出优先级队列,所以复杂度为O(n log k)。
空间复杂度:O(n+k)=O(n),因为构造了大小为k的辅助队列和大小为n的map,已知k<n。