Q53- code1438- 绝对差不超过限制的最长连续子数组
实现思路
1 方法1:双单调队列 + 滑动窗口
S1:理解问题本质
原问题:找最长连续子数组,任意两元素绝对差 ≤ limit 等价于:找最长连续子数组,最大值 - 最小值 ≤ limit
- 如果 max - min ≤ limit,那么任意两元素差都 ≤ limit
S2:寻找规律
- 如果 [i...j] 满足条件,那么 [i...j-1] 也满足
- 如果 [i...j] 不满足条件,那么 [i...j+1] 也不满足
S3:单调性
- 对于固定的 left,存在一个最大的 right,使得 [left...right] 满足条件
- 当 left 增加时,这个最大的 right 不会减少(单调性)
- 当 原left为最大值/最小值时, 最大差值会变小
- 当 原left不是最大值/最小值时,最大差值会不变
即:
- 如果 [left...right] 不满足条件(差值 > limit)
- 那么 [left+1...right] 的差值 ≤ 原差值
- 所以 [left+1...right] 可能满足条件
所以 当窗口不满足条件时,left++ 后, 窗口可能重新满足条件,所以 right 不需要回退!
S4.1:滑动窗口使用场景
- 特征:具有"窗口越长越难满足条件"的单调性质
- 当窗口不满足条件时,需要收缩左边界而不是回退右边界,因为继续扩展右边界只会让情况更糟
S4.2:单调队列的作用
- 需要维护:窗口内的最大值和最小值
- 频繁操作:窗口滑动时需要快速获取最值
- 单调队列优势:O(1)时间获取最值,O(1)时间维护
参考文档
代码实现
1 方法1: 双单调队列 + 滑动窗口
- 时间复杂度:O(n)
- 空间复杂度:O(n)
ts
function longestSubarray(nums: number[], limit: number): number {
// 两个空数组,存储索引
let minQ: number[] = [], maxQ: number[] = [];
// ans记录最长长度,left是窗口左边界
let left = 0, ans = 0;
nums.forEach((num, right) => {
// S1 保证 新元素总是从右边进入窗口的
// 维护最小值队列(单调递增)
// 如果 当前元素 ≤ 队列最后一个元素对应的值
// 则移除队列最后一个元素(保持单调递增)
while (minQ.length && num <= nums[minQ.at(-1)]) minQ.pop();
minQ.push(right);
// 维护最大值队列(单调递减)
// 如果 当前元素 ≥ 队列最后一个元素对应的值
// 则移除队列最后一个元素(保持单调递减)
while (maxQ.length && num >= nums[maxQ.at(-1)]) maxQ.pop();
maxQ.push(right);
// S2 收缩窗口
// 如果当前窗口的最大值-最小值 > limit
// 需要收缩窗口
while (nums[maxQ[0]] - nums[minQ[0]] > limit) {
left++;
// 如果最小值队列的队首索引 < left(已过期)
// 则移除队首
while (minQ[0] < left) minQ.shift();
// 如果最大值队列的队首索引 < left(已过期)
// 则移除队首
while (maxQ[0] < left) maxQ.shift();
}
// 更新最长长度
// 当前窗口长度 = right - left + 1
ans = Math.max(ans, right - left + 1);
});
return ans;
}
2 方法2:滑动窗口 + Map:注意 这种方法实际提交会超时
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
ts
function longestSubarray(nums: number[], limit: number): number {
let left = 0, ans = 0;
// Map<元素值, 出现次数>
const record = new Map();
// 固定右边界扩展策略,逐步扩大窗口,当窗口不满足条件时再收缩左边界
for (let right = 0; right < nums.length; right++) {
const x = nums[right];
record.set(x, (record.get(x) ?? 0) + 1);
// 获取窗口内的最值
const min = Math.min(...record.keys());
const max = Math.max(...record.keys());
// 如果窗口内的最值差大于limit,则收缩左边界
if (max - min > limit) {
// 获取将要移出窗口的元素, 更新它的出现次数
const lVal = nums[left];
const freq = record.get(lVal) - 1;
freq === 0 ? record.delete(lVal) : record.set(lVal, freq);
// 收缩左边界
left++;
}
// 更新最长长度
ans = Math.max(ans, right - left + 1);
}
return ans;
}
Q54- code895- 最大频率栈
实现思路
1 方法1:buckets + Map
1.1 分析题目要求
- 需要记录频率
- 频率相同时要取最近的
- 最难的是 "频率相同时取最近的" 这个要求
- 这暗示了我们需要某种方式保存元素的 "时序信息"
1.2 常见数据结构分析
- 堆?可以,但处理相同频率时需要额外信息
- 栈?天然保持时序,但不好处理频率
- Map?可以记录频率,但不保持时序
1.3 关键突破点
- 如果能把 "相同频率" 的元素放在一起
- 又能保持它们的入栈顺序
- 那不就自然解决了问题吗?
1.4 灵感来源
- 这类题目的一个常见技巧是 "分组"
- 比如桶排序就是按数值分组
- 这里我们可以按频率分组!
这种解法的巧妙之处在于:
- 把频率作为分组依据,自然解决了"找最高频率"的问题
- 每个频率组用数组存储,自然保持了入栈顺序
参考文档
代码实现
1 方法1: buckets + Map
- 时间复杂度:O(1)
- 空间复杂度:O(n)
ts
class FreqStack {
// idx是 频率freq,val是 出现的元素栈
private buckets: number[][] = [];
// key是num, val是freq
private freqs = new Map<number, number>();
push(val: number): void {
const freq = (this.freqs.get(val) ?? 0) + 1;
this.freqs.set(val, freq);
// 如果当前频率超过现有栈数,创建新栈
(this.buckets[freq] ??= []).push(val);
}
pop(): number {
const val = this.buckets.at(-1).pop();
// 如果最高频率栈为空,移除该栈
if (!this.buckets.at(-1).length) this.buckets.pop();
// 更新频率
const freq = this.freqs.get(val) - 1;
freq ? this.freqs.set(val, freq) : this.freqs.delete(val);
return val;
}
}