目录
目录
哈希
关于哈希,以下是记忆模板:
unordered_map<Key类型, Value类型> mp;
// 存
mp[key] = value;
// 统计
mp[key]++;
// 查
if (mp.find(key) != mp.end()) {
// 存在
}
// 遍历
for (auto& pair : mp) {
pair.first; // key
pair.second; // value
}
还有一个unordered_set:一个“只存唯一值”的无序集合,而且查找非常快。用法:
#include <unordered_set>
using namespace std;
unordered_set<int> st;
#插入
st.insert(5);
#判断是否存在
if (st.count(5)){}
#删除
st.erase(5);
#集合大小
st.size();
1.两数之和
数组中找两个数,使它们满足某个和
优先使用哈希表:
遍历当前数 x.
算 need = target - x
先查 need 是否出现过
没出现就记录 x -> 下标
unordered_map<int, int> mp;
for (...) {
int need = target - nums[i];
if (mp.find(need) != mp.end())
return {mp[need], i};
mp[nums[i]] = i;
}
2.字母异位词分组
把具有相同特征的元素分组
这题的“特征”就是:排序后的字符串
字母异位词 → 排序后一定相同。
用排序后的字符串作为 unordered_map 的 key。
mp[key].push_back(原字符串)。
最后把 map 中每个 vector 放入答案。
unordered_map<特征, vector<原元素>> mp;
for (原元素) {
特征 = 计算特征;
mp[特征].push_back(原元素);
}
3.最长连续序列
找连续数字,但原数组本身是乱序的。
核心就一句:找不到前驱,才开始往后找。
要求 O(n) → unordered_set。
num - 1 不存在 → num 才是连续序列起点。
从起点不断检查 num + 1。
每段只扫描一次,所以整体 O(n)。
unordered_set<int> st(nums.begin(), nums.end());
for (int num : st) {
// 只有前一个数字不存在,才是起点
if (st.find(num - 1) == st.end()) {
int current = num;
int length = 1;
while (st.find(current + 1) != st.end()) {
current++;
length++;
}
}
}
双指针
1.移动零
看到「原地移动 + 保持相对顺序」→ 想双指针。
slow 指向下一个非零元素的位置,遍历时遇到非零就 nums[slow++] = nums[i]。
最后 [slow, n) 全部补 0。
时间 O(n),空间 O(1)。
int slow = 0;
for (int i = 0; i < nums.size(); i++) {
if (满足保留条件) {
nums[slow] = nums[i];
slow++;
}
}
2.盛最多水的容器
两端选两个位置 + 求最大面积 + 宽度和高度互相制约
优先想到双指针:
int left = 0, right = n - 1;
while (left < right) {
更新答案;
if (左边更矮)
left++;
else
right--;
}
3.三数之和
三数之和 = 排序 + 固定一个数 + 双指针。
和小了 left++,和大了 right—。
这题核心难点不是双指针,而是 三处去重:i 去重、left 去重、right 去重。
时间复杂度 O(n²)。
sort(nums.begin(), nums.end());
for (int i = 0; i < n - 2; i++) {
// i 去重
int left = i + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum < target)
left++;
else if (sum > target)
right--;
else {
记录答案;
左右去重;
}
}
}
4.接雨水
接雨水 = 看每个位置左右两边的最高挡板。
单点水量:min(左最高, 右最高) - 当前高度。
双指针从两端走,谁矮处理谁,同时维护 leftMax / rightMax。
时间 O(n),空间 O(1)。
while (left < right) {
if (height[left] < height[right]) {
leftMax = max(leftMax, height[left]);
ans += leftMax - height[left];
left++;
}
else {
rightMax = max(rightMax, height[right]);
ans += rightMax - height[right];
right--;
}
}
滑动窗口
1.无重复字符的最长子串
最长/最短连续子串 + 满足某个条件,优先考虑 滑动窗口。
无重复最长子串:滑动窗口 + set。
right 扩张,遇到重复就 while 移动 left。
当前长度是 right - left + 1。
时间 O(n),空间 O(n)。
unordered_set<char> window;
int left = 0;
for (int right = 0; right < s.size(); right++) {
while (当前窗口不合法) {
删除 s[left];
left++;
}
加入 s[right];
更新答案;
}
2.找到字符串中所有字母异位词
在字符串中找所有长度固定、字符频率满足条件的子串,优先想到:固定长度滑动窗口 + 频率数组
异位词 = 字符次数完全相同。
用固定长度滑动窗口,长度保持为 p.size()。
右边进一个字符,窗口太长就左边删一个。
两个 26 字符频率数组相同,就记录 left。
时间 O(n),空间 O(1)。
for (int right = 0; right < s.size(); right++) {
加入 s[right];
if (窗口太长) {
删除 s[left];
left++;
}
if (窗口满足条件) {
记录 left;
}
}
子串
1.和为 K 的子数组
连续子数组 + 子数组和等于某个值 → 前缀和 + 哈希表。
unordered_map<int, int> mp;
mp[0] = 1;
int sum = 0, ans = 0;
for (...) {
sum += ...;
ans += mp[sum - k];
mp[sum]++;
}
2.滑动窗口最大值
连续长度
k的窗口,每移动一步求最大/最小值 → 单调队列 + 存下标
先讲一下deque双端队列。普通 queue 只能:
- 从后面加入
push - 从前面删除
pop
但 deque 更灵活,前面和后面都可以插入、删除。
常见操作:
deque<int> q;
q.push_back(2); // [2] 从后面加入2
q.push_back(3); // [2, 3] 从后面加入3
q.push_front(1); // [1, 2, 3] 从前面加入1
cout << q.front(); // 1 查看第一个元素
cout << q.back(); // 3 查看最后一个元素
q.empty() == true // q 是空的
q.empty() == false // q 里面有元素滑动窗口最大值 → 单调递减队列。
队列存下标:队头过期就删,队尾比新元素小就删。
新元素入队后,窗口形成时 nums[q.front()] 就是最大值。
时间 O(n),空间 O(k)。
while (!q.empty() && nums[q.back()] <= nums[i]){
q.pop_back();
}
3.最小覆盖子串
过
普通数组
1.最大子数组和
最大连续子数组和 → Kadane。
curSum 表示“以当前位置结尾”的最大和。
每次二选一:接前面,或者从当前重新开始。
核心:curSum = max(nums[i], curSum + nums[i])。
curSum = max(nums[i], curSum + nums[i]);
maxSum = max(maxSum, curSum);
2.合并区间
一堆区间,要合并重叠区间
合并区间 → 先按左端点排序。
当前左端点 > 上一个右端点:不重叠,直接加入。
否则重叠:lastEnd = max(lastEnd, curEnd)。
整体 O(n log n)。
sort(intervals.begin(), intervals.end());
for (...) {
if (当前左端点 > 上一个右端点)
加入新区间;
else
更新右端点为 max(...);
}
3.轮转数组
数组右轮转 k 位 → 三次反转。
先 k %= n,再整体反转、前 k 个反转、剩余部分反转。
本质是把 [A|B] 变成 [B|A]。
时间 O(n),空间 O(1)。
k %= n;
reverse(nums.begin(), nums.end());
reverse(nums.begin(), nums.begin() + k);
reverse(nums.begin() + k, nums.end());
4.除了自身以外数组的乘积
除自身乘积 → 前缀乘积 × 后缀乘积。
第一遍从左到右,把“左边乘积”存进 answer。
第二遍从右到左,再乘“右边乘积”。
关键:先使用 left/right,再乘 nums[i],避免把自己算进去。
int left = 1;
for (int i = 0; i < n; ++i) {
answer[i] = left;
left *= nums[i];
}
int right = 1;
for (int i = n - 1; i >= 0; --i) {
answer[i] *= right;
right *= nums[i];
}
5.缺失的第一个正数
先略过
矩阵
1.矩阵置零
矩阵置零本质是先记录哪些行、哪些列要清零。
row[i] 记录第 i 行要不要清零。
col[j] 记录第 j 列要不要清零。
进行第一次遍历,然后标记后再进行第二次遍历置0。
vector<bool> row(m,false);
vector<bool> col(n,false);
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(matrix[i][j]==0){
row[i]=true;
col[j]=true;
}
}
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(row[i]||col[j]){
matrix[i][j]=0;
}
}
}
2.螺旋矩阵
看到“顺时针遍历矩阵” → 四边界模拟。
顺序固定:上 → 右 → 下 → 左。
每走完一边就收缩边界;下、左遍历前额外检查边界,防止重复。
while (top <= bottom && left <= right) {
上边 → top++;
右边 → right--;
if (...) 下边 → bottom--;
if (...) 左边 → left++;
}
3.旋转图像
旋转矩阵 + 要求原地 → 想“转置 + 翻转”。
顺时针 90°:先主对角线转置,再每行左右翻转。
转置只枚举 j = i + 1 开始的右上部分。
时间 O(n²),空间 O(1)。
// 转置
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
swap(matrix[i][j], matrix[j][i]);
// 每行反转
for (int i=0;i<n;i++)
reverse(row.begin(), row.end());