本篇记录下一下关于单调队列和优先队列(堆)的方法以及解题思路.
文章目录
- 一. 单调队列
- 1. 绝对差不超过限制得最长连续子数组
- 2. 跳跃游戏 VI
- 3. 设计自助结算系统
- 4. 和至少为k的最短子数组
- 5. 满足不等式的最大值
- 二. 优先队列
- 1. 最后一块石头的重量
- 2. 数据流中的第k大元素
- 3. 拼车
- (1)前缀和
- (2)优先队列(堆)
- 4. 滑动窗口最大值
- (1)单调队列
- (2)优先队列
一. 单调队列
1. 绝对差不超过限制得最长连续子数组
这道题是要求我们求出连续子数组中最大值和最小值差,差小于等于所给的limit。
然后返回连续子数组最大的长度。
- 找出子数组中最大值和最小值
- 两值相减去,小于等于limt,取其长度,选择那个最长的。
所以我们可以利用两个指针锁定窗口,也即是长度,然后在窗口中更新不断的更新最大值和最小值。
- 所以我们可以维护两个单调增减队列,使队首永远是最大值或者最小值。
- 当窗口中最大值 - 最小值 > limit 时候就需要修改窗口大小,使left++
- 如果说nums[left] 这个值呢正好就是最大值或者最小值,那么相应的它也得出队列。
#define MAX_SIZE 100000int Max(int x , int y)
{return x > y ? x : y;
}int longestSubarray(int* nums, int numsSize, int limit)
{//增减单调队列int* deQueue = (int*)malloc(sizeof(int) * MAX_SIZE);int* inQueue = (int*)malloc(sizeof(int) * MAX_SIZE);int deFront = 0, deRear = 0, inFront = 0, inRear = 0;//int left = 0, right = 0, result = 0;while(right < numsSize){//维护单调递减队列while(deFront < deRear && deQueue[deRear - 1] < nums[right]){deRear--;}//维护单调递增队列while(inFront < inRear && inQueue[inRear - 1] > nums[right]){inRear--;}//入队列deQueue[deRear++] = nums[right];inQueue[inRear++] = nums[right++];while(deFront < deRear && inFront < inRear && deQueue[deFront] - inQueue[inFront] > limit){//修改窗口大小,即left往前。//对于两个队列来说,如果删除的left 是当前窗口的最大值或者最小值,那么相对应得队首也得删除if(deQueue[deFront] == nums[left]){deFront++;}if(inQueue[inFront] == nums[left]){inFront++;}left++;}//更新窗口大小 取最大值result = Max(result, right - left);}return result;
}
2. 跳跃游戏 VI
这道题也是要求我们从0下标位置开始,然后跳k步,返回经过之和的最大值。
说白了就是说,在给定的k窗口大小中,找出从0起点位置,和窗口中各个数据总和的最大值,然后让那个数成为了新的起点,接着运算。
- 需要一个dp数组和单调队列来实现整个代码。
- dp数组里面存放的是和,队列中存储的下标,而下标所对应的dp一定是递增的,保证越来越大,这样子才可以使最后的结果越来越大。
在整个遍历数组的过程中,对于每个数据分为三个步骤。
- 首先判断当前的窗口大小是否满足 k,又因为使单调增队列,所以将队首更新成下一个就好了。
- 接下来就是算出当前的dp[i],也就是front + nums[i]的值。
- 算出当前的dp[i]后,需要保证队列是单调增的,所以如果发现队尾的数据和当前的dp不满足单调增时候,需要将队尾进行出队操作。
int maxResult(int* nums, int numsSize, int k)
{int* dp = (int*)malloc(sizeof(int) * numsSize);int* queue = (int*)malloc(sizeof(int) * numsSize);int front = 0, rear = 0;//dp数组存放的最大的分数dp[0] = nums[0];//队列中,队首永远会是当前的最优解,整个队列也是呈递增的。queue[rear++] = 0;for (int i = 1; i < numsSize; i++){//最多可以跳K步,判断是否需要收缩窗口大小if(front < rear && queue[front] < i - k){front++;}//dpdp[i] = dp[queue[front]] + nums[i];//维护单调队列while(front < rear && dp[queue[rear - 1]] <= dp[i]){rear--;}//入队列queue[rear++] = i;}return dp[numsSize - 1];
}
3. 设计自助结算系统
这道题其实就是要求我们设计一个队列出来,遵循现进先出的原则,但是在此基础上增加了一个要求,就是获取最大值的要求,你可以遍历整个队列获取,但是不符合题,题目中要求是O(1).
所以我们可以维护一个单调递减的队列出来,这样就可以保证说这个递减队列的队首位置永远是最大的。
这道题和栈中的设计一个最小栈题目是很类似的,那道题目是设计两个栈,而这一道是设计两个队列。
#define MAX_SIZE 10000typedef struct
{int* queue; //正常队列,待结算的商品int* deQueue; //单调递减队列,也就是说队首永远是最大的待结商品int front,rear,deFront,deRear;
} Checkout;Checkout* checkoutCreate()
{Checkout* obj = (Checkout*)malloc(sizeof(Checkout));obj -> queue = (int*)malloc(sizeof(int) * MAX_SIZE);obj -> deQueue = (int*)malloc(sizeof(int) * MAX_SIZE);obj -> front = obj -> rear = obj -> deFront = obj -> deRear = 0;return obj;
}int checkoutGet_max(Checkout* obj)
{if(obj -> front == obj -> rear){//队列为空return -1;}//递减队列的队首return obj -> deQueue[obj -> deFront];
}void checkoutAdd(Checkout* obj, int value)
{//入正常队列obj -> queue[(obj -> rear)++] = value; //入单调递减队列while(obj -> deFront != obj -> deRear && obj -> deQueue[obj -> deRear - 1] < value){obj -> deRear--;}obj -> deQueue[(obj -> deRear)++] = value;
}int checkoutRemove(Checkout* obj)
{if(obj -> front == obj -> rear){//队列为空return -1;}if(obj -> queue[obj -> front] == obj -> deQueue[obj -> deFront]){//同时出队列obj -> deFront++;}return obj -> queue[(obj -> front)++];
}void checkoutFree(Checkout* obj)
{free(obj -> queue);free(obj -> deQueue);free(obj);
}/*** Your Checkout struct will be instantiated and called as such:* Checkout* obj = checkoutCreate();* int param_1 = checkoutGet_max(obj);* checkoutAdd(obj, value);* int param_3 = checkoutRemove(obj);* checkoutFree(obj);
*/
4. 和至少为k的最短子数组
这道题要求我们求出连续的子数组的和最少是k的最短长度是多少。
经典的滑动窗口 + 单调队列的解法。
- 这道题需要反复的取求数组的和,所以我们先预处理一下前缀和数组。
- 当求出前缀和数组后,知道一些基本的道理有助于下面的运算。
- 一下这个公式应该都会吧,
给一个前缀和数组 prefixSum, 然后 i < j
pre[i] = nums[0] + nums[1] + nums[2] + nums[i].
pre[j] = nums[0] + nums[1] + nums[2] + nums[i] + nums[i + 1] +…+ nums[j]
pre[i,j] = pre[j] - pre[i].
- i 和 j 其实就是滑动窗口中的左右边界。
- 当 i ~ j 的和求出来后,发现和大于等与k的话。
- 我们可以计算出该窗口的长度 i - j;
- 同时缩小窗口大小。
- 如果没有大于等于k,我们继续扩大窗口的大小。
下图是滑窗口简单的运算过程:举例有点特殊,k是6,所以一直在不断的更新
- 但是这个不就是用双指针就可以做到吗?为什么需要优先队列,因为还有一个细节需要注意。
- 我们在遍历nums数组的时候,必须保证前缀和是单调递增的,
- 如果发现当前的前缀和比队尾前缀和小,那么我们就将当前的变成新的队尾
- 看下下面的例子,当只是用滑动窗口处理这道题的时候,最后的结果一定是4,也就是整个数组的和35,窗口的大小是从0到4.(绿色)
- 但是呢,发现15+15=30长度为2,2才是最后的答案,所以前缀和数组中收0 2 3的方式,因为prefix[2] < prefix[1] ,将其更换掉了。
int Min(int x, int y)
{return x < y ? x : y;
}int shortestSubarray(int* nums, int numsSize, int k)
{int n = numsSize, i;//前缀和long long* prefixSum = (long long*)malloc(sizeof(long long) * (n + 1));prefixSum[0] = 0;for (i = 1; i <= n; i++){prefixSum[i] = prefixSum[i - 1] + nums[i - 1];}//递增队列,队列中存储的是前i个的前缀和 queue[i]。int* dequeue = (int*)malloc(sizeof(int) * (n + 1));int front = 0, rear = 0;dequeue[rear++] = 0; //初始化,也就是前0个的前缀和。int ans = n + 1;for (i = 1; i <= n; i++){//判断是否需要更新窗口大小,left,左边的窗口while (front != rear && prefixSum[i] - prefixSum[dequeue[front]] >= k){ ans = Min(ans,i - dequeue[front]);front++;}//维护单调队列,保证队列中下标所存储的前缀和是递增的.while (front != rear && prefixSum[i] <= prefixSum[dequeue[rear - 1]]){rear--;}dequeue[rear++] = i;}return ans == n + 1 ? - 1 : ans;
}
5. 满足不等式的最大值
这道题是要求我们在一个范围,按公式求值,返回最大的那个值。
范围就是| xi - xj | <= k. 公式是:yi + yj + | xi - xj |.
对公式简单的进行推导一下:
既然说题目中说了是升序排列,所以一定有i < j。
那么|xi - xj| 可以变换成 xj - xi。
yi + yj + xj - xi = (xj + yj) + (yi - xi)的形式
- 有了上面新的公式后呢,我们规定(xj + yj)为新节点,(yi - xi)为队列中存储的节点。
- 那么要想使此式子变大,新节点大小不可控,遍历到什么使什么,但是(yi - xi)既然使遍历过的节点,存放在队列中的,那么我们可以在存放的时候,就选择越来越大的存放,不用讲小的存进去,就好了。
对于整体的数组遍历可分成一下几种步骤:
- 判断窗口大小是否符合,队首即窗口left
- 更新ans,利用这个公式去更新(xj + yj) + (yi - xi)。
- 维护队列,使队列成一个单调递增的形式,越来越大的,用当前(yi - xi)和队尾的进行比较。
int Max(int x, int y)
{return x > y ? x : y;
}int findMaxValueOfEquation(int **points, int pointsSize, int *pointsColSize, int k)
{int* queue = (int*)malloc(sizeof(int) * pointsSize);int front = 0, rear = 0;int ans = INT_MIN;queue[rear++] = 0;for (int i = 1; i < pointsSize; i++){//先判断窗口大小是否符合while(front != rear && points[i][0] - points[queue[front]][0] > k){front++;}if(front != rear){//队列不为空则尝试更新ans//(xj + yj) + (yi - xi)ans = Max(ans,points[i][0] + points[i][1] + points[queue[front]][1] - points[queue[front]][0]);}//维护单调递增队列,使队尾永远保持大的值。//判断那一个的 yi - xi 更大 while(front != rear && points[queue[rear - 1]][1] - points[queue[rear - 1]][0] <= points[i][1] - points[i][0]){rear--;}//pushqueue[rear++] = i;}free(queue);return ans;
}
二. 优先队列
1. 最后一块石头的重量
这道题目要求我们每次从数组中选择出两个最大值,如果他俩相等抵消,否则的话就将差入再入数组,最后返回仅省一个石头的重量,也可以没有石头(return 0).
- 这道题我们肯定能使用排序来做,选出两个最大值后,判断是否需要插入差值。
- 然后再进行排序,直到数组中的元素不够两个时候。
- 我们可以在此思想上进一步优化优化一下,不用对数组每个元素都进行排序。
- 我们理由优先队列(堆),建立一个大顶堆,是堆的首元素永远是最大的。
- 然后对其进行相应的出队列,入队列即可。
void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}//维护堆
void Heapify(int* nums, int numsSize, int i)
{int maxIndex = i, lc = 2 * i + 1, rc = 2 * i + 2;if (lc < numsSize && nums[lc] > nums[maxIndex]){maxIndex = lc;}if (rc < numsSize && nums[rc] > nums[maxIndex]){maxIndex = rc;}if (maxIndex != i){Swap(&nums[maxIndex], &nums[i]);Heapify(nums, numsSize, maxIndex);}
}//将val插入堆中去
void HeapPush(int* nums, int* numsSize, int val)
{//在最后面插入nums[(*numsSize)++] = val;//维护当前插入后的父亲节点for (int parent = (*numsSize - 2) / 2; parent >= 0; parent = (parent - 1) / 2){Heapify(nums, *numsSize, parent);//最后一次if (parent == 0){break;}}
}//删除堆顶元素
void HeapPop(int* nums, int* numsSize)
{//将堆顶元素和最后一个交换Swap(&nums[0], &nums[--(*numsSize)]);//重新维护堆Heapify(nums, *numsSize, 0);
}int lastStoneWeight(int* stones, int stonesSize)
{if (stonesSize == 1){return stones[0];}if (stonesSize == 2){return abs(stones[0] - stones[1]);}int i, n = stonesSize;//创建堆for (i = (n - 1) / 2; i >= 0; i--){Heapify(stones, n, i);}while (n >= 2){//获取两次堆顶的元素int x = stones[0];HeapPop(stones, &n);int y = stones[0];HeapPop(stones, &n);//如果两次不一样,那么相减后入堆if (x > y){HeapPush(stones, &n, x - y);}}//堆为空的话,说明抵消没了。return n == 0 ? 0 : stones[0];
}
下面是对上述代码中的所提到堆函数进行简单的一些讲解:
堆这种数据结构是二叉树的一种形式,其在数组中存放,实现起来还是挺方便的。
下面简单的讲一下heap函数中一些细节。
- Pop: 删除堆顶元素,其实就跟堆排序一样,将堆顶数据和最后一个交换,然后size减小,重新维护堆顶就好了。
- Push:插入元素,这个有些细节需要注意,在当前队列(堆)得末尾直接插入元素,同样也是需要进行维护的,在数组中存储二叉树,我们可以用利用下标直接得到父亲节点和左右孩子。
- 对于我上面的是根节点存放在0下标的位置,
- 已知parent = i, leftchild = 2 * i + 1, rightChild = 2 * i + 2;
- 已知(左右通用)child = i,parent = (i - 1) / 2;
还有一种是根节点存放在1号索引处是又一种方式,这里就不仔细说了。
2. 数据流中的第k大元素
这道题要求我们设计一种数据结构,可以返回第k大的元素,简单来讲就是说给你一个数组,然后对其进行降序排序,第k个就是第k大的元素,用数组可以做出来,但是对于这道题目来说,时间耗费非常大,这道题可以使用堆来实现。
- 我们来维护一个小顶堆出来,保证每次堆顶的数据都是最小的。
- 接下来终点来了:整个堆的大小一定得是 k,那么既然大小是k,所以堆顶的元素肯定就是第k个最大的数据了,有点抽象还是看图吧:
就比如上图中,nums数组大小是4,但是我们的堆中只放k个,至于是如何舍弃2的,待会再说。 - 当后续新的数据需要add到堆中时候,如果数据比堆顶小,那么就不需要插入堆中,你插进来不会影响第k大元素,没有意义,就不插入了,
- 相反,如果说新的数据比堆顶的大,那么将其将堆顶数据替换后,继续维护堆。
下面是测试用例1的模拟过程:
那对于刚开始的初始化堆,从刚开始初始化的时候,就选择数组中的前k个最大的就好了,如果数组中不够k个,将整个数组入堆就好了。
typedef struct
{int* heap;int capacity,k; // k是整个堆的大小,capacity 是当前堆中的元素
} KthLargest;int cmp_int(const void* x, const void* y)
{return *(int*)x - *(int*)y;
}
void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}void Heapfiy(int* nums, int size, int i)
{int minIndex = i, lc = 2 * i + 1, rc = 2 * i + 2;if(lc < size && nums[lc] < nums[minIndex]){minIndex = lc;}if(rc < size && nums[rc] < nums[minIndex]){minIndex = rc;}if(minIndex != i){Swap(&nums[i],&nums[minIndex]);Heapfiy(nums,size,minIndex);}
}KthLargest* kthLargestCreate(int k, int* nums, int numsSize)
{KthLargest* obj = (KthLargest*)malloc(sizeof(KthLargest));obj -> heap = (int*)malloc(sizeof(int) * k); //堆中最多放k个数据,然后维护一个小顶堆,堆顶就是第k个最大元素。int i;qsort(nums,numsSize,sizeof(int),cmp_int);if(numsSize < k){//将数组中的所有元素入堆即可for (i = 0; i < numsSize; i++){obj -> heap[i] = nums[i];}obj -> capacity = numsSize;}else{int pos = 0;//只入后k个for (i = numsSize - k; i < numsSize; i++){obj -> heap[pos++] = nums[i];}obj -> capacity = k;}obj -> k = k;return obj;
}int kthLargestAdd(KthLargest* obj, int val)
{if(obj -> capacity != obj -> k){//需要插入堆obj -> heap[(obj -> capacity)++] = val;for (int parent = (obj -> capacity - 2) / 2; parent >= 0; parent = (parent - 1) / 2){Heapfiy(obj -> heap,obj -> capacity, parent);if(parent == 0){break;}}return obj -> heap[0];}if(val > obj -> heap[0]){//新元素大于堆顶则需要入堆obj -> heap[0] = val;Heapfiy(obj -> heap,obj -> capacity, 0);}return obj -> heap[0];
}void kthLargestFree(KthLargest* obj)
{free(obj -> heap);free(obj);
}
3. 拼车
这道题是给我们一个二维数组,里面放着当前站上几个人,然后到哪里下。
问我们会不会超载。
(1)前缀和
- 最直观的我们遍历一遍所给的数组,将其起点和终点的位置进行相应的赋值
- 看下图,我在每个位置进行相应的增人减人操作
- 那total 进行相应的自增,如果total > capacity 时候就说明超载了
代码如下:
#define MAX_SIZE 1001bool carPooling(int** trips, int tripsSize, int* tripsColSize, int capacity)
{int* bucket = (int*)malloc(sizeof(int) * MAX_SIZE);int i, total = 0;memset(bucket,0,sizeof(int) * MAX_SIZE);for (i = 0; i < tripsSize; i++){int num = trips[i][0], from = trips[i][1], to = trips[i][2];bucket[from] += num;bucket[to] -= num;}for (i = 0; i < MAX_SIZE; i++){total += bucket[i];if(total > capacity){return false;}}return true;
}
(2)优先队列(堆)
也可以使用优先队列进行模拟实现
- 首先对原来的数组进行排序,按照它的上车顺序进行升序排序。
- 然后使用优先队列,其队首永远是最先下车的那批乘客。
- 就是说最先下车的位置 <= 当前上车的位置。
- 就将其出队列。
- 还是如果total大于了capacity的话返回false
下面是代码思路还是比较简单的:
bool carPooling(int** trips, int tripsSize, int* tripsColSize, int capacity)
{ //首先给trips 排序,使其上车起点是递增的qsort(trips,tripsSize,sizeof(trips[0]),cmp_from);int* heap = (int*)malloc(sizeof(int) * tripsSize);int size = 0,total = 0; //size 是堆的大小, total 是当前车上的乘客for (int i = 0; i < tripsSize; i++){//最先下车的位置 <= 当前上车的位置while(size != 0 && trips[heap[0]][2] <= trips[i][1]){total -= trips[heap[0]][0];HeapPop(trips,heap,&size);}//入堆HeapPush(trips,heap,&size,i);total += trips[i][0];if(total > capacity){return false;}}return true;
}
C语言需要自己实现一下堆.
//因为堆中存储的是下标,需要将trips整个数组传过来。
void Heapfiy(int** trips, int* nums, int size, int i)
{int minIndex = i, lc = 2 * i + 1, rc = 2 * i + 2;if(lc < size && trips[nums[lc]][2] < trips[nums[minIndex]][2]){minIndex = lc;}if(rc < size && trips[nums[rc]][2] < trips[nums[minIndex]][2]){minIndex = rc;}if(minIndex != i){Swap(&nums[minIndex],&nums[i]);Heapfiy(trips,nums,size,minIndex);}
}//插入堆
void HeapPush(int** trips,int* nums, int* size, int i)
{nums[(*size)++] = i;for (int parent = (*size - 2) / 2; parent >= 0; parent = (parent - 1) / 2){Heapfiy(trips,nums,*size,parent);if(parent == 0){break;}}
}//删除
void HeapPop(int** trips, int* nums, int* size)
{Swap(&nums[0],&nums[--(*size)]);Heapfiy(trips,nums,*size,0);
}
4. 滑动窗口最大值
这道题给我们一个k大小的窗口,向右边的滑动,并且求出每一个窗口内的最大值。
下面是两种解法,在这道题目中,单调队列的时间更快。
(1)单调队列
第一种办法我们可以维护一个单调队列出来,此队列为递增序列。
- 利用两个指针来锁定窗口的大小,left 和 right,
- 如果right - left != k,就说明窗口还没形成,right一直往后走
- 否则left++。
接下来再维护一个单调队列就好了。 - 队列要保持一个单调递减的队列,这样队首永远都是属于一个最大值。
- 当我们进行出队列的时候,需要判断当前的left是否是队首的元素,如果是,那么队首也随之而然的消除。
- 当每次需要更新窗口大小的时候,就是收获结果的时候,我下面是直接在数组中进行修改的。
int* maxSlidingWindow(int* nums, int numsSize, int k, int* returnSize)
{int* deQueue = (int*)malloc(sizeof(int) * numsSize);int front = 0, rear = 0;int left = 0;for (int i = 0; i < numsSize; i++){if(i - left == k){//调整窗口大小 && 更新值nums[left] = nums[deQueue[front]];if(left == deQueue[front]){front++;}left++;}//维护单调队列while(front != rear && nums[deQueue[rear - 1]] < nums[i]){rear--;}//入队列deQueue[rear++] = i;}nums[left++] = nums[deQueue[front]];*returnSize = left;return nums;
}
(2)优先队列
根据上面的单调队列我们可以发现,只需要对于每个窗口获取最大值就好,所以我们同样也可以用优先队列来存储最大值利用大顶堆。
- 优先队列中存储的依旧是下标,思路其实和单调队列的思路是一样的。
- 只是换了一种获取最大值的数据结构而已。
- 单调队列:获取答案后出队列只需要将等于left对首出队列即可。
- 优先队列:获取答案后出队列需要将 小于等于left的所有节点出队列
- 所以优先队列需要一个循环出队列,而单调队列则需要一个if就ok了。
听起来好像优先队列更麻烦,其实不然,因为单调队列还需要维护一下,而优先队列直接插入就好了。
int* maxSlidingWindow(int* nums, int numsSize, int k, int* returnSize)
{int* ans = (int*)malloc(sizeof(int) * numsSize);int* heap = (int*)malloc(sizeof(int) * numsSize);int size = 0, left = 0, pos = 0;for (int i = 0; i < numsSize; i++){if(i - left == k){//修改窗口大小 && 获取ansans[pos++] = nums[heap[0]];//最大的数不在窗口内,出队列while(size != 0 && heap[0] <= left){HeapPop(nums,heap,&size);}left++;}//入队列HeapPush(nums,heap,&size,i);}ans[pos++] = nums[heap[0]];*returnSize = pos;return ans;
}
而对于堆函数的实现,和上一题拼车一样,存储的都是下标,修改修改就能用了。
//heap 中存放下标
void Heapfiy(int* nums, int* heap, int size, int i)
{int maxIndex = i, lc = 2 * i + 1, rc = 2 * i + 2;if (lc < size && nums[heap[lc]] > nums[heap[maxIndex]]){maxIndex = lc;}if (rc < size && nums[heap[rc]] > nums[heap[maxIndex]]){maxIndex = rc;}if(maxIndex != i){Swap(&heap[i], &heap[maxIndex]);Heapfiy(nums,heap,size,maxIndex);}
}//
void HeapPush(int* nums, int* heap, int* size, int i)
{heap[(*size)++] = i;for (int parent = (*size - 2) / 2; parent >= 0; parent = (parent - 1) / 2){Heapfiy(nums,heap,*size,parent);if(parent == 0){break;}}
}void HeapPop(int* nums,int* heap, int* size)
{Swap(&heap[0],&heap[--(*size)]);Heapfiy(nums,heap,*size,0);
}