力扣算法编程技巧
1. 常用算法模板
1.二分查找通用模板
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size();
int l = 1, r = n - 1; // 也就是 n-1,数值范围
while (l < r) { // 【模版核心:不加等号】
int mid = l + (r - l) / 2; // 先写标准的向下取整
int cnt = 0;
for (int x : nums) {
if (x <= mid) cnt++;
}
// 【判定逻辑】
if (cnt > mid) {
// [1, mid] 太挤了,重复数就在这里面
// mid 也有可能是答案,所以不能 -1,只能 r = mid
r = mid;
} else {
// [1, mid] 很空,重复数肯定在右边 [mid+1, n]
// mid 肯定不是,扔掉,所以 l = mid + 1
l = mid + 1;
}
}
// 循环结束时 l == r,就是答案
return l;
}
};
这里直接不加等号,结束时,l和r在一个位置,l就是要找的答案的坐标。
然后考虑mid,如果mid可能是答案,则更新left或者right的时候可以保留,即让left或right=mid,否则,就扔掉。
为什么写 mid=l + (r - l) / 2,而不是(l+r)/2,主要考虑到,l+r可能会导致int溢出。
2.快排通用模板
void quick_sort(vector<int>& q,int l, int r)
{
//1.递归终止条件
if(l>=r) return;
//2. 初始化:i,j往两侧外扩一格,pivot选中间
//为什么要外扩,因为下面用的是 do-while,先右外向内跳一步
int i=l-1,j=r+1;
int pivot=q[(l+r) >> 1] //选中间作为基准,防止退化,尽量每次排序左右两边的数都是差不多的,>>是二进制右移操作,相当于除以2
//核心分区
while(i<j)
{
do i++;while(q[i]<pivot) // 找左边>=pivot的
do j--;while(q[j]>pivot) // 找右边<=pivot的
// 只有i<j才交换,防止交错后还在换
if (i<j) swap(q[i],q[j]);
}
//4. 递归
//关键点,用j分界
quick_sort(q,l,j);
quick_sort(q,j+1,r);
}
明白了,你是希望每个工具的标题用 Markdown 的 ## 或 ### 直接在代码块外面显示,而不是在代码里加注释。我帮你改成这种格式,using namespace std; 保留,自定义降序也加上了。
3. SWAP - 交换两个变量
#include <iostream>
#include <utility> // swap
using namespace std;
int main() {
int a = 5, b = 10;
swap(a, b);
cout << "a = " << a << ", b = " << b << "\n";
return 0;
}
4. SORT - 排序数组或容器(升序/降序)
#include <iostream>
#include <algorithm> // sort
#include <vector>
using namespace std;
int main() {
vector<int> v = {5, 2, 8, 1, 3};
// 升序排序
sort(v.begin(), v.end());
cout << "升序: ";
for(int x : v) cout << x << " ";
cout << "\n";
// 自定义降序排序
sort(v.begin(), v.end(), [](int a, int b){ return a > b; });
cout << "降序: ";
for(int x : v) cout << x << " ";
cout << "\n";
return 0;
}
5. REVERSE - 翻转数组或容器
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5};
reverse(v.begin(), v.end());
for(int x : v) cout << x << " ";
cout << "\n";
return 0;
}
6. MAX / MIN - 求最大值或最小值
#include <iostream>
#include <algorithm> // max, min
using namespace std;
int main() {
int a = 10, b = 20;
cout << "max = " << max(a, b) << ", min = " << min(a, b) << "\n";
return 0;
}
7. ACCUMULATE - 求和
#include <iostream>
#include <numeric> // accumulate
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5};
int sum = accumulate(v.begin(), v.end(), 0); // 初始值 0
cout << "sum = " << sum << "\n";
return 0;
}
8. COUNT - 统计元素出现次数
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 2, 3, 2, 4};
int cnt = count(v.begin(), v.end(), 2);
cout << "2 appears " << cnt << " times\n";
return 0;
}
9. FIND - 查找元素
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5};
auto it = find(v.begin(), v.end(), 3);
if(it != v.end()) cout << "Found 3 at index " << (it - v.begin()) << "\n";
else cout << "3 not found\n";
return 0;
}
10. LOWER_BOUND / UPPER_BOUND - 二分查找范围
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 2, 3, 4, 5};
sort(v.begin(), v.end()); // 需要先排序
auto lb = lower_bound(v.begin(), v.end(), 2); // 第一个 >= 2
auto ub = upper_bound(v.begin(), v.end(), 2); // 第一个 > 2
cout << "lower_bound = " << (lb - v.begin()) << ", upper_bound = " << (ub - v.begin()) << "\n";
return 0;
}
11. INT_MIN / INT_MAX - 整型最小值与最大值
#include <iostream>
#include <climits> // INT_MIN, INT_MAX
using namespace std;
int main() {
int minVal = INT_MIN;
int maxVal = INT_MAX;
cout << "INT_MIN = " << minVal << "\n";
cout << "INT_MAX = " << maxVal << "\n";
return 0;
}
2. 线性扫描艺术:滑动窗口技术(Sliding Window)
2.1 理论基础:从暴力递归到线性扫描
滑动窗口(Sliding Window)技术本质上是对暴力枚举法的极致优化。在处理数组或字符串的子序列问题时,暴力解法往往需要两层嵌套循环来遍历所有可能的子区间,导致时间复杂度高达 $O(N^2)$ 甚至 $O(N^3)$。滑动窗口通过维护一个动态的“视窗”,利用数据在时间序列上的连续性,将嵌套循环转化为单次遍历,从而将复杂度降维至 $O(N)$ 4。
其核心洞察在于“增量计算”:当窗口从位置 $[i, j]$ 滑动到 $[i+1, j+1]$ 时,中间的重叠部分 $[i+1, j]$ 的状态无需重新计算,只需扣除移出元素 $i$ 的影响并增加移入元素 $j+1$ 的贡献即可。这种思想不仅适用于求和,更广泛应用于计数、极值查找及哈希状态维护。
2.2 固定窗口模式(Fixed Window)
固定窗口是滑动窗口的最基础形式,通常用于解决“寻找长度为 $k$ 的最佳子数组”类问题。
2.2.1 算法逻辑与C++模板
在固定窗口中,窗口的大小 $k$ 是恒定的。我们首先初始化第一个长度为 $k$ 的窗口,随后同步移动左右边界。
通用C++模板:
C++
/*
* 固定窗口模板
* 场景:求解长度为 k 的子数组的最大和/平均值/特定属性
* 时间复杂度:O(N)
* 空间复杂度:O(1)
*/
#include <vector>
#include <numeric>
#include <algorithm>
#include <iostream>
using namespace std;
int fixedSlidingWindow(const vector<int>& nums, int k) {
// 边界条件防御
if (nums.size() < k) return -1;
// 1. 初始化阶段:计算第一个窗口的状态
long long current_window_val = 0;
for (int i = 0; i < k; ++i) {
current_window_val += nums[i];
}
long long max_val = current_window_val;
// 2. 滑动阶段:从第 k 个元素开始,每次右移一位
// i 代表新进入窗口的元素索引 (right edge)
// i - k 代表即将移出窗口的元素索引 (left edge)
for (size_t i = k; i < nums.size(); ++i) {
// 增量更新:减去左边离开的,加上右边进入的
current_window_val += nums[i] - nums[i - k];
// 维护最优解
if (current_window_val > max_val) {
max_val = current_window_val;
}
}
return static_cast<int>(max_val);
}
2.2.2 案例解析:找到字符串中所有字母异位词
问题背景:给定两个字符串 s 和 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
技术难点:需要在滑动过程中实时判断两个子串的字符频次是否一致。
深度实现:
C++
/*
* LeetCode 438. Find All Anagrams in a String
* 方法:固定窗口 + 数组哈希
*/
vector<int> findAnagrams(string s, string p) {
int s_len = s.length(), p_len = p.length();
if (s_len < p_len) return {};
vector<int> result;
// 使用大小为26的数组代替哈希表,利用C++数组的高效访问
vector<int> p_count(26, 0);
vector<int> window_count(26, 0);
// 1. 预处理目标串 p 和第一个窗口
for (int i = 0; i < p_len; ++i) {
p_count[p[i] - 'a']++;
window_count[s[i] - 'a']++;
}
// 检查第一个窗口
if (p_count == window_count) {
result.push_back(0);
}
// 2. 开始滑动
for (int i = p_len; i < s_len; ++i) {
// 移入右侧字符 s[i]
window_count[s[i] - 'a']++;
// 移出左侧字符 s[i - p_len]
window_count[s[i - p_len] - 'a']--;
// 比较两个频率数组(C++ vector 重载了 == 运算符,比较是 O(26) 即 O(1))
if (p_count == window_count) {
result.push_back(i - p_len + 1);
}
}
return result;
}
分析:此例展示了固定窗口与状态压缩(频率数组)的结合。由于字符集有限(26个字母),vector的比较操作被视为常数时间,整体复杂度严格保持在 $O(N)$。
2.3 可变窗口模式(Variable Window)
可变窗口是更高级的形态,通常涉及两个指针 left 和 right。right 主动扩张以寻找可行解,left 被动收缩以寻找最优解(最短)或恢复合法性(最长) 5。
2.3.1 伸缩策略逻辑
可变窗口通常遵循以下“虫取法”(Caterpillar Method)逻辑:
- 扩张(Expand):增加
right指针,将新元素纳入窗口,更新窗口状态。 - 判断(Check):检查窗口状态是否满足特定条件(如无重复字符、总和大于Target等)。
- 收缩(Shrink):
- 若求最长子数组:当窗口不满足条件时,通过移动
left收缩,直到恢复满足条件。 - 若求最短子数组:当窗口满足条件时,尝试移动
left收缩以寻找更短的可行解,直到不再满足条件。
- 若求最长子数组:当窗口不满足条件时,通过移动
通用C++模板:
C++
/*
* 可变窗口通用模板
* 适用于:最长/最短/计数类子数组问题
*/
int variableSlidingWindow(vector<int>& nums) {
int n = nums.size();
int left = 0, right = 0;
int ans = 0; // 根据问题初始化(0或INT_MAX)
// 定义维护窗口状态的数据结构(如Sum, Hash, Count)
// long long current_sum = 0;
while (right < n) {
// A. 移入元素:更新状态
int val_in = nums[right];
// current_sum += val_in;
// B. 收缩窗口逻辑
// Case 1: 寻找最长合法子窗口 -> 当窗口非法时收缩
// while (窗口状态非法) {
// int val_out = nums[left];
// current_sum -= val_out;
// left++;
// }
// ans = max(ans, right - left + 1);
// Case 2: 寻找最短合法子窗口 -> 当窗口合法时尝试收缩
// while (窗口状态合法) {
// ans = min(ans, right - left + 1);
// int val_out = nums[left];
// current_sum -= val_out;
// left++;
// }
right++; // 继续扩张
}
return ans;
}
2.3.2 案例解析:无重复字符的最长子串
问题:LeetCode 3. Longest Substring Without Repeating Characters
策略:寻找最长合法窗口。当发现重复字符时,窗口非法,需收缩左边界直到重复字符被排除。
深度实现:
C++
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// 使用数组模拟哈希表优化性能(ASCII 128)
// index 记录字符上一次出现的位置,避免逐步收缩,实现“跳跃式”收缩
vector<int> last_seen(128, -1);
int n = s.length();
int left = 0;
int max_len = 0;
for (int right = 0; right < n; ++right) {
char current_char = s[right];
// 如果当前字符之前出现过,并且出现在当前窗口内(>= left)
if (last_seen[current_char] >= left) {
// 直接将 left 跳到重复字符的下一位
left = last_seen[current_char] + 1;
}
// 更新当前字符的最新位置
last_seen[current_char] = right;
// 更新结果
max_len = max(max_len, right - left + 1);
}
return max_len;
}
};
洞察:此实现展示了 滑动窗口的一个高级优化——跳跃式窗口。标准模板中的 while 循环逐步收缩虽然也是 $O(N)$,但在特定场景下(如知道重复字符的具体索引),我们可以直接将 left 指针跳跃到重复位置的右侧,从而大幅减少指令数,提升常数级性能。
3. 指针的舞蹈:双指针策略(Two Pointers)
3.1 双指针的多维形态
双指针技术虽然简单,却是处理有序结构和链表问题的核心手段。它通过两个指针协同移动,将搜索空间从二维矩阵(所有可能的配对)压缩至一维路径 7。
主要形态包括:
- 对撞指针(Collision Pointers):左右夹逼,常用于有序数组求和(2Sum, 3Sum)或反转操作。
- 快慢指针(Fast & Slow Pointers):一快一慢,用于链表找环(Floyd判圈法)、寻找中点或数组原地去重。
- 分离双指针:处理两个独立序列,如归并排序的合并步骤。