LeetCode [239. 滑动窗口最大值]
题目:给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length
思路:
// 利用双端队列手动实现单调队列
/**
* 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
* 单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (--> head) (右边为头结点,元素存的是下标)
*/
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> deque = new LinkedList<Integer>();
int n = nums.length;
// n - k + 1是滑块要滑动的次数
int[] ans = new int[n - k + 1];
int index = 0;
// 遍历数组
// 根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点
// 队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
for(int i = 0;i < n;i++){
// 当队列不为空,且队列头部存放的数组下标超出界定范围的情况,将该下标弹出
while(!deque.isEmpty() && deque.peekFirst() < i - k + 1){
deque.pollFirst();
}
// 当队列不为空,且每次放进去的数字要比末尾的都大,则把当前队列末尾所存储的下标弹出,
// 并把当前最大值的下标存入队列
while(!deque.isEmpty() && nums[i] > nums[deque.peekLast()]){
deque.pollLast();
}
deque.offerLast(i);
// 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
if(i >= k - 1){
ans[index++] = nums[deque.peekFirst()];
}
}
return ans;
}
}
LeetCode [347. 前 K 个高频元素]
题目:给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
1 <= nums.length <= 105
k
的取值范围是[1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前
k
个高频元素的集合是唯一的
**进阶:**你所设计算法的时间复杂度 必须 优于 O(n log n)
,其中 n
是数组大小。
思路:
//基于小根堆实现
class Solution {
public int[] topKFrequent2(int[] nums, int k) {
Map<Integer,Integer> map = new HashMap<>();//key为数组元素值,val为对应出现次数
for(int num:nums){
map.put(num,map.getOrDefault(num,0)+1);
}
//在优先队列中存储二元组(num,cnt),cnt表示元素值num在数组中的出现次数
//出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
//小顶堆只需要维持k个元素有序
for(Map.Entry<Integer,Integer> entry:map.entrySet()){
//小顶堆元素个数小于k个时直接加
if(pq.size()<k){
pq.add(new int[]{entry.getKey(),entry.getValue()});
}else{
//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
if(entry.getValue()>pq.peek()[1]){
//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
pq.poll();
pq.add(new int[]{entry.getKey(),entry.getValue()});
}
}
}
int[] ans = new int[k];
//依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
for(int i=k-1;i>=0;i--){
ans[i] = pq.poll()[0];
}
return ans;
}
}
扩展:PriorityQueue
基本使用
默认:最小堆,每次可获得最小元素
优先队列按照其作用不同,可以分为以下两种:
- 最大优先队列: 可以获取并删除队列中最大的值
- 最小优先队列: 可以获取并删除队列中最小的值
将元素放入队列:add,offer
将队首元素从队列删除:remove,poll
查看队列内的对首元素:element,peek
123
和标准队列不同的是,当删除队首元素的时候,删除的是
priority queue
中最小的元素。但是,priority queue
并不是对所有的元素排序,其内部是用heap
(堆)实现的。堆是一个自组织的二叉树,在这个二叉树里,add
和remove
操作使得最小的元素“吸引”到二叉树的根部,而不用在排列整个队列上耗费时间。
PriorityQueue<Integer> heap = new PriorityQueue<>(
(n1,n2) -> n1-n2 //这一句加不加结果是一样的
);
heap.add(4);
heap.add(1);
heap.add(2);
heap.add(3);
while(!heap.isEmpty())
{
System.out.println(heap.poll());
}
123456789101112
输出顺序是1,2,3,4。
可以看到默认的比较规则就是n1-n2
,如果我们改成n2-n1
PriorityQueue<Integer> heap = new PriorityQueue<>(
(n1,n2) -> n2-n1 //这一句加不加结果是一样的
);
123
输出顺序就是4,3,2,1。
可以想见优先队列内部是按照比较器返回的结果进行处理的,一般认为a,b比较结果大于0,就是a大于b,等于0就是a等于b;小于0就是a小于b。
现在我们知道当比较n1,n2的时候,如果结果大于0(也就是默认的n1-n2>0),那么要把n1下沉(默认的小根堆),当我们改成n2-n1的时候,结果大于0,证明n2大于n1,但还是n1(值较小的元素)下沉,于是这个堆就成了大根堆!
单元素优先级队列
就如上面的基本用法,下面换一个比较字符的例子:
将字母分为三个等级输出
输入一个字符串,高级为"bdfhkl",中级为"aceimnorstuvwxz",低级为"gjqpy",请将字符串按字母等级分割为3个字符串,每个字符串内按字典序输出。如果字符串为空输出null。
import java.util.*;
public class demo5 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String s = scanner.next();
PriorityQueue<Character> q1 = new PriorityQueue<>();
PriorityQueue<Character> q2 = new PriorityQueue<>();
PriorityQueue<Character> q3 = new PriorityQueue<>();
for (int i = 0; i < s.length(); i++) {
switch (fun(s.charAt(i))) {
case 1 :
q1.offer(s.charAt(i));
break;
case 2 :
q2.offer(s.charAt(i));
break;
case 3 :
q3.offer(s.charAt(i));
break;
default:
break;
}
}
if (q1.size() != 0) {
while (q1.size() != 0) {
System.out.print(q1.poll());
}
System.out.println();
} else {
System.out.println("null");
}
if (q2.size() != 0) {
while (q2.size() != 0) {
System.out.print(q2.poll());
}
System.out.println();
} else {
System.out.println("null");
}
if (q3.size() != 0) {
while (q3.size() != 0) {
System.out.print(q3.poll());
}
System.out.println();
} else {
System.out.println("null");
}
}
static int fun(char c) {
switch (c) {
case 'b' :
case 'h':
case 'f':
case 'k':
case 'l':
case 'd':
return 1;
case 'g':
case 'j':
case 'p':
case 'q':
case 'y':
return 3;
default:
return 2;
}
}
}
再举一个链表节点比较的例子:
基本写法:
就像TreeSet一样,一个priority queue要么存储的是实现了Comparable接口的元素,要么在构造函数中传入一个Comparator对象。
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
//自定义比较类,升序排列
static Comparator<ListNode> cLNode = new Comparator<ListNode>() {
public int compare(ListNode o1, ListNode o2) {
return o1.val-o2.val;
}
};
public static void main(String[] args) {
//自定义类的优先队列需要重写比较类作为传入参数
Queue<ListNode> que = new PriorityQueue<>(cLNode);
//简单写法
// Queue<ListNode> que = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
}
合并K个有序链表
把链表节点放入一个最小堆,就可以每次获得 k
个节点中的最小节点。
优先队列
pq
中的元素个数最多是k
,所以一次poll
或者add
方法的时间复杂度是O(logk)
;所有的链表节点都会被加入和弹出pq
,所以算法整体的时间复杂度是O(Nlogk)
,其中k
是链表的条数,N
是这些链表的节点总数。
ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) return null;
// 虚拟头结点
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
// 优先级队列,最小堆
PriorityQueue<ListNode> pq = new PriorityQueue<>(
lists.length, (a, b)->(a.val - b.val));
// 将 k 个链表的头结点加入最小堆
for (ListNode head : lists) {
if (head != null)
pq.add(head);
}
while (!pq.isEmpty()) {
// 获取最小节点,接到结果链表中
ListNode node = pq.poll();
p.next = node;
if (node.next != null) {
pq.add(node.next);
}
// p 指针不断前进
p = p.next;
}
return dummy.next;
}
class Solution {
class Status implements Comparable<Status> {
int val;
ListNode ptr;
Status(int val, ListNode ptr) {
this.val = val;
this.ptr = ptr;
}
public int compareTo(Status status2) {
return this.val - status2.val;
}
}
PriorityQueue<Status> queue = new PriorityQueue<Status>();
public ListNode mergeKLists(ListNode[] lists) {
for (ListNode node: lists) {
if (node != null) {
queue.offer(new Status(node.val, node));
}
}
ListNode head = new ListNode(0);
ListNode tail = head;
while (!queue.isEmpty()) {
Status f = queue.poll();
tail.next = f.ptr;
tail = tail.next;
if (f.ptr.next != null) {
queue.offer(new Status(f.ptr.next.val, f.ptr.next));
}
}
return head.next;
}
}
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
import java.util.*;
public class Solution {
public ListNode mergeKLists(ArrayList<ListNode> lists) {
if(lists.size() == 0){
return null;
}
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
PriorityQueue<ListNode> pq = new PriorityQueue<ListNode>(lists.size(), (a, b) -> (a.val - b.val));
for(ListNode x : lists){
if(x != null){
pq.offer(x);
}
}
while(!pq.isEmpty()){
ListNode node = pq.poll();
p.next = node;
if(node.next != null){
pq.offer(node.next);
}
p = p.next;
}
return dummy.next;
}
}
双元素优先队列
PriorityQueue<int[]> heap = new PriorityQueue<>(
(pair1,pair2) -> pair1[0]-pair2[0]
);
heap.add(new int[]{1,9});
heap.add(new int[]{2,8});
heap.add(new int[]{3,7});
while(!heap.isEmpty())
{
int[] cur = heap.poll();
System.out.println(cur[0] + "," + cur[1]);
}
这里我们必须是要设置比较规则的,因为默认里面没有比较数组的规则。这里我们设置第一个元组的第一个数减去第二个元组的第一个数。
按照前面设想的逻辑,此时如果结果大于0,证明pair2的第一个数比pair1的第一个数大,而pair1下沉,说明此时是一个以元组的第一个数为比较依据的大根堆。
现在我们可以自由地依照我们的想法来设置排序规则了,比如我现在想要以元组的和为比较依据,并且是小根堆。根据猜想,我们需要让大的值下沉,也就是比较结果大于0的时候,应该是第一个元组是大值。
PriorityQueue<int[]> heap = new PriorityQueue<>(
(pair1,pair2) -> pair1[0]+pair1[1]-pair2[0]-pair2[1]
);
heap.add(new int[]{1,9});
heap.add(new int[]{2,6});
heap.add(new int[]{3,4});
while(!heap.isEmpty())
{
int[] cur = heap.poll();
System.out.println(cur[0] + "," + cur[1]);
}
这里结合TreeSet使用一下,类似的道理:
import java.io.IOException;
import java.util.*;
public class demo02 {
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
scanner.nextLine();
int[] height = new int[n];//身高
String[] name = new String[n];//姓名
for(int i = 0 ; i < n; i++){
height[i] = scanner.nextInt();
}
scanner.nextLine();
for(int i = 0 ; i < n; i++){
name[i] = String.valueOf(scanner.next());
}
TreeMap<Integer, PriorityQueue<String >> treeMap = new TreeMap<>();
for(int i = 0; i < n; i++){
if(treeMap.containsKey(height[i])){
treeMap.get(height[i]).add(name[i]);
}else{
PriorityQueue<String > queue = new PriorityQueue<>();
treeMap.put(height[i], queue);
queue.add(name[i]);
}
}
List<String> list = new ArrayList<>();
PriorityQueue<String > queue = new PriorityQueue<>();
while(!treeMap.isEmpty()){
queue = treeMap.pollFirstEntry().getValue();
while(!queue.isEmpty()){
list.add(queue.poll());
}
}
for(int i = 0; i < n; i++){
System.out.print(list.get(i) + " ");
}
System.out.println();
}
}
常用的场景——调度
单线程CPU
这个面试时候遇到过,现场手撕
这题的难度不算大,就是有些复杂,难点在于你要同时控制三个变量(开始时间、处理时间、索引)的有序性,而且这三个变量还有优先级:
- 首先应该考虑开始时间,因为只要到了开始时间,任务才进入可执行状态;
- 其次应该考虑任务的处理时间,在所有可以执行的任务中优先选择处理时间最短的;
- 如果存在处理时间相同的任务,那么优先选择索引最小的。
所以这道题的思路是:
先根据任务「开始时间」排序,维护一个时间线变量
now
来判断哪些任务到了可执行状态,然后借助一个优先级队列pq
对「处理时间」和「索引」进行动态排序。利用优先级队列动态排序是有必要的,因为每完成一个任务,时间线
now
就要更新,进而产生新的可执行任务。
import java.util.*;
public class demo6 {
public static void main(String[] args) {
int[][] tasks = {{1, 2}, {2, 4}, {3, 2}, {4, 1}};
int[] res = getOrder(tasks);
System.out.print(Arrays.toString(res));
// 输出[0, 2, 3, 1]
}
static int[] getOrder(int[][] tasks) {
int n = tasks.length;
// 把原始索引也添加上,方便后面排序用
ArrayList<int[]> triples = new ArrayList<>();
for (int i = 0; i < n; i++) {
triples.add(new int[]{tasks[i][0], tasks[i][1], i});
}
// 数组先按照任务的开始时间排序
triples.sort(Comparator.comparingInt(a -> a[0]));
//上面一行等价于
// triples.sort((a, b) -> {
// return a[0] - b[0];
// });
// 按照任务的处理时间排序,如果处理时间相同,按照原始索引排序
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> {
if (a[1] != b[1]) {
// 比较处理时间
return a[1] - b[1];
}
// 比较原始索引
return a[2] - b[2];
});
ArrayList<Integer> res = new ArrayList<>();
// 记录完成任务的时间线
int now = 0;
int i = 0;
while (res.size() < n) {
if (!pq.isEmpty()) {
// 完成队列中的一个任务
int[] triple = pq.poll();
res.add(triple[2]);
// 每完成一个任务,就要推进时间线
now += triple[1];
} else if (i < n && triples.get(i)[0] > now) {
// 队列为空可能因为还没到开始时间,
// 直接把时间线推进到最近任务的开始时间
now = triples.get(i)[0];
}
// 由于时间线的推进,会产生可以开始执行的任务
for (; i < n && triples.get(i)[0] <= now; i++) {
pq.offer(triples.get(i));
}
}
// Java 语言特性,将 List 转化成 int[] 格式
int[] arr = new int[n];
for (int j = 0; j < n; j++) {
arr[j] = res.get(j);
}
return arr;
}
}
Arrays.sort()
在Arrays.sort中也可以自定义排序规则,试验后发现是一样的譬如:
int[][] arr = {{1,9},{2,8},{3,7}};
Arrays.sort(arr,(a,b)->b[0]-a[0]);
当结果大于0的时候,是a向后,也就是默认的是升序,这里的代码的含义是以元组的第一个值为依据,降序排列。
需要注意的是如果是一维数组,是无法自定义这样的规则的,如果想换成降序,最简单的办法就是翻转:
Arrays.sort(arr,Collections.reverseOrder());
注意这里的数组不能是int[],需要是Integer[]