文章目录
一、单调队列
1. 理论基础
- 定义
单调队列为何说单调,因为是队列中的元素始终保持着单增或者单减的特性。(注意始终保持这四个字)
其实单调队列不只是做到了排序,还可以实现一个功能:在每次加入或者删除元素时都保持序列里的元素有序,即队首元素始终是最小值或者最大值,这个功能非常重要,单调队列我们就是使用的这个功能。 - 例子
我们依次加入5个元素,分别为5,8,2,4,1
那么我们假设队列是单减的,那么队首元素始终是最大的,五次操作后的队列元素排列情况如下:
1: 5
2: 8
3: 8 2
4: 8 4
5: 8 4 1
1.首先队列里面没有元素,5加进去。
2.第二个元素8大于队尾的元素,所以5要弹出去,8加进去。保持队首最大
3.第三个元素2小于队尾元素8,可以加进去,变为8 2
4.4大于队尾元素2,2弹出,4小于8,8不弹出,4加进去
5.1小于队尾元素4,1加进去,最后队列为8 4 1
- 设计方案
1.队首(左边)判定元素是否出队
由于单调队列左边必定是最大值(最小值),根据题目要求判定单调队列左边是否要出队
2.队尾(右边)通过循环不断删除结点
右边通过一个 while 循环不断删除队尾元素,来保证较大(较小)的元素成功在右边入队,以此来维护单调队列
3.最右边新元素入队
当最右边元素入队之后,就成功维护一个新的单调队列
2. T239 滑动窗口最大值
- 代码实现
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 1){return nums;}
int len = nums.length - k + 1;//滑窗数组的长度
int[] res = new int[len];//存放结果
//初始化结果
MyQueue que = new MyQueue();
for(int i =0;i<k;i++){
que.add(nums[i]);
}
res[0] = que.peek();
int num = 1;
for(int i = k;i<nums.length;i++){//i记录每次滑窗的最右端
que.pop(nums[i-k]);//移动滑窗,删除第一个元素
que.add(nums[i]);//把最后的元素加进去队列
res[num] = que.peek();
num++;
}
return res;
}
}
//单调队列
//每次用peek取出的元一定是该队列的最大值
class MyQueue{
Deque<Integer> deque = new LinkedList<>();
//入队操作:保证单调递减。while循环 如果该值比最后一个元素大,那就弹出最后一个元素,继续比较,知道满足条件
void add(int val){
while(!deque.isEmpty() && val > deque.getLast()){
deque.removeLast();
}
deque.add(val);
}
//出队操作:并不是针对队列出队,而是需要将队列与数组进行比较。仅仅当peek出来的元素与需要弹出的数据相等时,才将其弹出。因为仅仅peek出来的数据影响取最大值
void pop(int val){
if(!deque.isEmpty() && val == deque.peek()){
deque.poll();
}
}
//返回最大值
int peek(){
return deque.peek();
}
}
二、优先队列(堆)
1. 理论
1.1 语法
- 定义
对于堆(使用PriorityQueue实现):从队头到队尾按从小到大排就是最小堆(小顶堆),
从队头到队尾按从大到小排就是最大堆(大顶堆)—>队头元素相当于堆的根节点 - 比较器
PriorityQueue 默认是小根堆,大根堆需要重写比较器(一定要记住相关语法,哪个是大哪个小顶堆)。
可以在 new PriorityQueue<>() 中的参数部分加入比较器。
具体写法是:(v1, v2) -> v2 - v1。
Queue 类的输入是 offer() 方法/add,弹出是 poll() 方法。
1.2 优先队列 单调队列的区别
5 8 4依次入队
优先队列 前几个最大或者最小值,java内置的PriorityQueue
5
8 5
8 5 4
单调队列 滑窗求最值,需要自己构造java不提供
5
8
8 4
1.3 PriorityQueue示例代码
class Debug{
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(3);
pq.offer(2);
pq.offer(6);
System.out.println(pq);
pq.poll();
System.out.println(pq);
}
//结果
//[2, 5, 3, 6]
//[3, 5, 6]
}
T347. 前k个高频元素 (大顶堆) **
- 思路分析
建立个大顶堆,频率次数高的元素在队头,优先出来(有一些题解强调要小顶堆 不理解!)
小技巧:如何遍历一个map集合
getOrDefault方法 - 代码实现
class Solution {
//优先队列实现大顶堆
//getOrDefault() 方法获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。
public int[] topKFrequent(int[] nums, int k) {
//1.计算值与出现频率
Map<Integer,Integer> map = new HashMap<>();
for(int i=0;i<nums.length;i++){
map.put(nums[i],map.getOrDefault(nums[i],0)+1);
}
//2.创建大顶堆(从大到小),然后把map的值放到里面
//大顶堆 [ [key,count], ... ]
PriorityQueue<int[]> pq = new PriorityQueue<>( (x,y) -> y[1]-x[1]);
//用map.entrySet 把map解析为 entry类型
for(Map.Entry<Integer,Integer> entry:map.entrySet()){
pq.add(new int[]{entry.getKey(),entry.getValue()});
}
//3.存放结果
int[] res = new int[k];
for(int i = 0;i<k;i++){
res[i] = pq.poll()[0];
}
return res;
}
}
面试题 17.14 最小K个数 (小顶堆) *
class Solution {
//小顶堆
public int[] smallestK(int[] arr, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<Integer>((x,y)->(x-y));//小顶堆
for(int i=0;i<arr.length;i++){
pq.offer(arr[i]);//队头到队尾:从小到大
}
int[] res = new int[k];
for(int i=0;i<k;i++){
res[i] = pq.poll();
}
return res;
}
}
面试题 17.09. 第 k 个数
T692. 前k个高频单词
三、单调栈
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
- 相关题目
T739. 每日温度(单调递增)
T496. 下一个更大元素
T503. 下一个更大元素Ⅱ (循环数组)
T42. 接雨水
见单调栈总结
四、归并排序
1. 理论基础: 递归+合并
- 1.1 合并两个有序数组
a[5] = {3,5,7,8,10}
b[7] = {1,2,4,5,8,11,12,}
核心思想:
-
定义一个新数组c,可以容纳a和b两个数组中的所有元素;
-
初始化三个下标(都指向第一个元素),i给a数组,j给b数组,k是新数组c的;
-
a[i]和b[j]进行比较:若a[i]<b[j],将a[i]填入c[k],i++,k++;若a[i]>b[j],将b[j]填入c[k],j++,k++;
-
循环第三步,直至其中一个数组中的数据全部填入数组c中,再将另外一个还有剩余的数组中的元素放入新数
-
1.2 归并排序(合并+递归思想)
我们的归并排序就是先使用递归将数组中元素进行划分,直至划分得到单个元素作为一个数组,此时就可以将其看作一个有序数组(只有一个元素自然是有序的)进行归并。最终将所有元素归并得到一个有序数组,所以这种排序方法称作为归并排序。
-
1.3 代码实现
//归并排序入口
public void mergeSort(int[] nums){
MergeSort(nums,0,nums.length-1);
}
/**
* 归并排序,左闭右闭原则
* @param nums 待排序数组
* @param start 数组开始的下标
* @param end 数组结束的下标
*/
private void MergeSort(int[] nums,int start,int end){
if(start<end){
int mid=start+(end-start)/2;
MergeSort(nums,start,mid); //将无序数组划分
MergeSort(nums,mid+1,end); //将无序数组划分
merge(nums,start,mid,end); //再将两个有序数组合并
}
}
/**
* 双指针合并两个有序数组
* @param nums
* @param start
* @param mid
* @param end
*/
private void merge(int[]nums, int start, int mid, int end){
int P1=start;//第一个数组的开始索引
int P2=mid+1;//第二个数组的开始索引
int tmp[]=new int[end-start+1]; //需要借助额外的O(n)空间来存储合并后的数组
int cur=0;
while (P1<=mid&&P2<=end){//先对两个数组一一比较
if(nums[P1]<nums[P2]){
tmp[cur]=nums[P1];
P1++;
}else {
tmp[cur]=nums[P2];
P2++;
}
cur++;
}
//若两个数组还有剩余的数 再进行比较
while (P1<=mid){
tmp[cur]=nums[P1];
P1++;
cur++;
}
while (P2<=end){
tmp[cur]=nums[P2];
P2++;
cur++;
}
for (int i = 0; i < res.length ; i++) {
nums[i+start]=tmp[i];
}
}
2. 剑指Offer 51. 数组中的逆序对
- 思路分析
进行归并排序,再合并的时候计算逆序对
具体可以参考以上图,或者此题解的动态ppt - 代码实现
class Solution {
//归并排序:从小到大
int count = 0;
public int reversePairs(int[] nums) {
mergeSort(nums,0,nums.length-1);
return count;
}
/** 归并排序 左闭右闭
*/
private void mergeSort(int[] nums,int start,int end){
if(start<end){
int mid = start + (end-start)/2;
mergeSort(nums,start,mid);//划分左数组
mergeSort(nums,mid+1,end);//划分右数组
merge(nums,start,mid,end);//合并数组
}
}
/**双指针合并数组
*/
private void merge(int[] nums,int start,int mid,int end){
int index1 = start;//第一个数组的开始索引
int index2 = mid+1;//第二个数组的开始索引
int cur = 0;
int[] temp = new int[end-start+1];
while(index1<=mid&&index2<=end){
if(nums[index1]<=nums[index2]){
temp[cur] = nums[index1];
index1++;
}else{//前面比后面大
temp[cur] = nums[index2];
index2++;
count += (mid-index1+1);
}
cur++;
}
while(index1<=mid){
temp[cur] = nums[index1];
index1++;
cur++;
}
while(index2<=end){
temp[cur] = nums[index2];
index2++;
cur++;
}
for(int i=0;i<temp.length;i++){
nums[i+start] = temp[i];
}
}
}
3. T315. 计算右侧小于当前元素的个数
- 思路分析
归并排序,在合并的过程中计算右侧小于当前元素的个数 - 代码实现
class Solution {
//难点1:保证在排序过程中坐标对应起来,用index和tempIndex数组
int[] index ;
int[] tempIndex;
int[] res;
//难点2:计算右侧小于当前元素个数:当此元素a比第二个数组的元素b小时 第二个数组中 从头到元素b的元素个数(不算b)
public List<Integer> countSmaller(int[] nums) {
index = new int[nums.length];
tempIndex = new int[nums.length];
res = new int[nums.length];
for(int i=0;i<nums.length;i++){
index[i]=i;
}
mergeSort(nums,0,nums.length-1);
List<Integer> list = new ArrayList<>();
for(int num:res){
list.add(num);
}
return list;
}
/** 归并排序 左闭右闭
*/
private void mergeSort(int[] nums,int start,int end){
if(start<end){
int mid = start + (end-start)/2;
mergeSort(nums,start,mid);//划分左数组
mergeSort(nums,mid+1,end);//划分右数组
merge(nums,start,mid,end);//合并数组
}
}
/**双指针合并数组
*/
private void merge(int[] nums,int start,int mid,int end){
int index1 = start;//第一个数组的开始索引
int index2 = mid+1;//第二个数组的开始索引
int cur = 0;
int[] temp = new int[end-start+1];
while(index1<=mid&&index2<=end){
if(nums[index1]<=nums[index2]){
temp[cur] = nums[index1];
tempIndex[cur] = index[index1];
res[index[index1]] += (index2-mid-1);
index1++;
}else{//前面比后面大
temp[cur] = nums[index2];
tempIndex[cur] = index[index2];
index2++;
}
cur++;
}
while(index1<=mid){
temp[cur] = nums[index1];
tempIndex[cur] = index[index1];
res[index[index1]] += (index2 -mid -1);
index1++;
cur++;
}
while(index2<=end){
temp[cur] = nums[index2];
tempIndex[cur] = index[index2];
index2++;
cur++;
}
for(int i=0;i<temp.length;i++){
index[i+start] = tempIndex[i];//把临时的index赋值给index 算结果res的时候始终用index
nums[i+start] = temp[i];
}
}
}