哈希表的学习
计算区间和的方式
C++ STL 容器详解:Map 与 Set
这是关于 C++ 中 Map (映射) 和 Set (集合) 的使用指南。在算法竞赛和工程中,我们主要关注它们的底层实现区别:哈希表 (Unordered) vs 红黑树 (Ordered) 。
1. Map (映射)
Map 用于存储 键值对 (Key-Value Pairs)。
核心操作速查
| 操作 | 代码示例 | 说明 |
|---|---|---|
| 引入库 | #include <unordered_map> | 如果需要排序则用 <map> |
| 定义 | unordered_map<string, int> mp; | Key是string,Value是int |
| 插入/更新 | mp["apple"] = 5; | 像数组一样使用下标 |
| 查找(简单) | if (mp.count("apple")) | 返回 1 (存在) 或 0 (不存在) |
| 查找(详细) | auto it = mp.find("apple"); | 需要判断 it != mp.end() |
| 取值 | int n = mp["apple"]; | 注意:如果key不存在,会自动创建并初始化为0 |
| 删除 | mp.erase("apple"); | 删除指定的键值对 |
代码示例
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
// 1. 定义哈希 Map
unordered_map<string, int> scores;
// 2. 添加数据
scores["Alice"] = 95;
scores["Bob"] = 88;
// 3. 修改数据
scores["Alice"] = 99; // 覆盖旧值
// 4. 查找数据
string name = "Bob";
if (scores.count(name)) {
cout << name << " 的分数是: " << scores[name] << endl;
}
// 5. 遍历 (C++11 写法)
for (auto item : scores) {
// item.first 是 Key, item.second 是 Value
cout << item.first << ": " << item.second << endl;
}
return 0;
}
2. Set (集合)
Set 用于存储 不重复 的元素,主要用于去重和快速存在性检测。它可以理解为只有 Key 没有 Value 的 Map。
核心操作速查
| 操作 | 代码示例 | 说明 |
|---|---|---|
| 引入库 | #include <unordered_set> | 如果需要排序则用 <set> |
| 定义 | unordered_set<int> st; | 存储 int 类型的集合 |
| 插入 | st.insert(10); | 如果 10 已存在,则不进行任何操作 |
| 查找 | if (st.count(10)) | 判断元素是否存在 (1或0) |
| 删除 | st.erase(10); | 移除元素 |
| 大小 | st.size(); | 获取集合中元素个数 |
代码示例
#include <iostream>
#include <unordered_set>
using namespace std;
int main() {
// 1. 定义哈希 Set
unordered_set<int> mySet;
// 2. 插入数据 (自动去重)
mySet.insert(1);
mySet.insert(2);
mySet.insert(1); // 无效操作,因为 1 已经存在了
// 3. 判断是否存在
if (mySet.count(2)) {
cout << "2 在集合里" << endl;
} else {
cout << "2 不在集合里" << endl;
}
// 4. 删除
mySet.erase(2);
// 5. 遍历
cout << "当前集合内容: ";
for (int num : mySet) {
cout << num << " ";
}
cout << endl;
return 0;
}
1.两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。你可以按任意顺序返回答案。
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> mp; // key = 数字, value = 下标
for (int i = 0; i < nums.size(); i++) {
int need = target - nums[i]; // 计算所需匹配的数
if (mp.count(need)) { // 如果匹配数已存在
return {mp[need], i}; // 返回匹配下标
}
mp[nums[i]] = i; // 把当前数字存入 map
}
return {}; // 如果没有解,返回空数组
}
};
unordered_map<int, int> mp 创建一个哈希表(键和值对应的关系) mp.count(need)可以遍历整个mp来判断need是否在里面,如果在的话可以用mp[need]返回他的值
49.字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
LeetCode 49. 字母异位词分组 - 代码深度解析
🧠 代码核心解析 (4个关键点)
1. sort`?
-
目的:寻找“指纹” (Canonical Form)。
-
原理:异位词的特点是字母组成相同,但顺序不同。
"eat"排序后"aet"`"tea"排序后 "aet"`
-
结论:排序后的字符串是唯一的 Key。只要 Key 相同,就说明它们是异位词,应该放进同一个袋子 (Vector) 里。
2. 语法特性:map[key] 的“隐式创建”
代码:
map[key].push_back(strs[i]);
-
机制:这是 C++
map/unordered_map的特性。- 如果 Key 不存在:系统会自动插入这个 Key,并调用 Value 类型 (这里是
vector) 的默认构造函数,生成一个空 vector。 - 如果 Key 已存在:直接返回对应 vector 的引用。
- 如果 Key 不存在:系统会自动插入这个 Key,并调用 Value 类型 (这里是
-
优势:无需编写
if(map.count(key))判断,一行代码完成“查找 + 创建 + 插入”。
3. 性能关键:auto &p (引用)
代码:
for(auto &p : map)
- ⚠️ 必须加
&:表示直接操作 map 里的原始数据。 - 如果不加
&:for(auto p : map)会触发深拷贝。它会将 map 里存的所有 vector 都复制一份给变量p。 - 后果:当字符串数量很大时,拷贝操作会导致超时 (TLE) 或内存暴涨。这是刷题时的常见陷阱。
4. 命名规范:避免使用 map 做变量名
- 坏习惯:
unordered_map<...> map; - 风险:
map是 C++ 标准库 (std::map) 的保留类名。虽然在局部作用域可能不报错,但在大型项目中极易产生命名冲突。 - 建议:使用
mp、counts、groups或anagramMap。
✅ 最终优化版代码
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
// 1. 定义哈希表
// Key: 排序后的字符串 (唯一标识)
// Value: 所有对应的异位词列表
unordered_map<string, vector<string>> mp; // 建议变量名改为 mp
// 2. 遍历输入数组
for(int i = 0; i < strs.size(); i++){
string key = strs[i];
// 3. 核心步骤:排序
// "eat", "tea", "ate" -> 排序后都变成 "aet"
sort(key.begin(), key.end());
// 4. 插入数据
// 利用 map 特性:key 不存在时会自动创建一个空 vector
// 然后直接把原字符串 (strs[i]) push 进去
mp[key].push_back(strs[i]);
}
// 5. 收集结果
vector<vector<string>> res;
// ⚠️ 注意:一定要用 & (引用),避免拷贝整个 vector,防止超时!
for(auto &p : mp){
// p.first 是 key ("aet")
// p.second 是 value (["eat", "tea", "ate"]) -> 我们要这个
res.push_back(p.second);
}
return res;
}
};
# 计数法
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<string>> map;
for(string str:strs) {
int counts[26] = {0};
for(char c:str) {
counts[c-'a']++;
}
string key = "";
for(int i = 0;i<26;++i) {
if(counts[i]!=0) {
key.push_back(i+'a');
key.push_back(counts[i]);
}
}
map[key].push_back(str);
}
vector<vector<string>> res;
for(auto& p:map) {
res.push_back(p.second);
}
return res;
}
};
128.最长连续序列
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> num_set;
// 1. 预处理:将所有数字放入哈希集合,用于去重和 O(1) 查找
for (const int& num : nums) {
num_set.insert(num);
}
int longestStreak = 0;
for (const int& num : num_set) {
// 2. 核心优化:只从序列的“起点”开始尝试
if (!num_set.count(num - 1)) {
int currentNum = num;
int currentStreak = 1;
// 3. 向后计数
while (num_set.count(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = max(longestStreak, currentStreak);
}
}
return longestStreak;
}
};
引用 vs 值拷贝:到底谁更省空间?
1. 结论速览
-
对于大对象 (
string,vector,class) :- ✅ 你说得对! 引用 (
const string&) 远比 值拷贝 (string) 省空间且速度快。
- ✅ 你说得对! 引用 (
-
对于小对象 (
int,char,bool) :- ❌ 这里反过来了! 直接拷贝值 (
int) 其实比引用 (const int&) 更省空间(或者差不多),而且速度往往更快。
- ❌ 这里反过来了! 直接拷贝值 (
2. 深度原理解析
为什么 int 会反直觉?我们需要看底层的数据大小。
假设我们在 64位操作系统 下:
情况 A:如果是 string (大对象)
假设这个字符串是 "Hello World... (1000字)"。
string s(值拷贝) :需要申请一块新的内存,把这 1000 个字全部复制过来。占用 几KB 甚至 几MB 的空间。const string& s(引用) :底层只是一个指针,只存“原数据的地址”。固定只占 8 字节。- 结果:引用完胜!
情况 B:如果是 int (小类型)
-
int num(值拷贝) :int标准大小是 4 字节。拷贝时,CPU 只需要把这 4 个字节拿过来放在寄存器里,非常快。 -
const int& num(引用) :- 引用的底层通常是通过指针实现的。
- 在 64 位系统下,一个指针(地址)的大小是 8 字节。
-
结果:
- 空间上:值拷贝 (4字节) < 引用 (8字节)。值拷贝反而更小!
- 速度上:CPU 处理立即数 (Value) 比处理地址 (Reference -> Value) 少了一次“寻址”的过程,所以值拷贝极快。
3. 生活中的类比
为了方便记忆,我们可以这样理解:
| 场景 | 原始数据 | 值拷贝 (Copy) | 引用 (Reference) | 哪个好? |
|---|---|---|---|---|
| int (硬币) | 你有一枚硬币 | 我也拿一枚一模一样的硬币 | 我写一张纸条,上面写着你硬币的位置 | 直接拿硬币方便 (值拷贝) |
| string (房子) | 你有一栋房子 | 我在旁边盖一栋一模一样的房子 | 我写一张纸条,上面写着你房子的地址 | 给地址方便 (引用) |
💡 最佳实践建议
-
遇到
int,double,bool,char:- 放心写
int num,不用加&,这是最标准的写法。
- 放心写
-
遇到
string,vector,自定义类:- 一定要写
const string& s,既省空间又防修改。
- 一定要写
滑动窗口
3.无重复字符的最长字串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
找到非重复的,可以建立一个哈希表,set方式一个值只能出现一次的,找到后收缩左边界,直到找不到重复的。
class Solution {
public:
int lengthOfLongestSubstring(std::string s) {
std::unordered_set<char> window;
int left = 0;
int maxLength = 0;
for (int right = 0; right < s.length(); ++right) {
while (window.count(s[right]) ) {
window.erase(s[left]); // 移除左边界的字符
left++; // 左边界右移
}
window.insert(s[right]); // 将当前字符加入窗口
maxLength = std::max(maxLength, right - left + 1); // 更新最大长度
}
return maxLength; // 返回最长子串的长度
}
};
438.找到字符串中所有字母异味词
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
vector cntP(26, 0), cntS(26, 0);
for (char c : p) {
cntP[c - 'a']++;
}可以通过这样的定义来返回字符串p里面含有的字母。
这样每次比较只要改变一个也不需要用到sort
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> res;
int n = s.size(), m = p.size();
if (n < m) return res;
vector<int> cntP(26, 0), cntS(26, 0);
// 统计 p 的字符频次
for (char c : p) {
cntP[c - 'a']++;
}
// 初始化第一个窗口
for (int i = 0; i < m; i++) {
cntS[s[i] - 'a']++;
}
if (cntS == cntP) {
res.push_back(0);
}
// 滑动窗口
for (int i = m; i < n; i++) {
cntS[s[i] - 'a']++; // 右边进
cntS[s[i - m] - 'a']--; // 左边出
if (cntS == cntP) {
res.push_back(i - m + 1);
}
}
return res;
}
};
数组
53.最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
找连续的,可以想到之前的区间和,,现在等于一遍 遍历过去,找到指针所在位置与前面某个指针形成的窗口的最大值
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int ans = nums[0];
int preSum = 0;
int minPreSum = 0; // 记录前面出现的最小值
for (int x : nums) {
preSum += x; // 当前前缀和
ans = max(ans, preSum - minPreSum); // 当前减去最小值,就是最大的可能
minPreSum = min(minPreSum, preSum); // 更新最小值,留给后面用
}
return ans;
}
};
#分治法
class Solution {
public:
struct Status {
int lSum, rSum, mSum, iSum;
};
Status pushUp(Status l, Status r) {
int iSum = l.iSum + r.iSum;
int lSum = max(l.lSum, l.iSum + r.lSum);
int rSum = max(r.rSum, r.iSum + l.rSum);
int mSum = max(max(l.mSum, r.mSum), l.rSum + r.lSum);
return (Status) {lSum, rSum, mSum, iSum};
};
Status get(vector<int> &a, int l, int r) {
if (l == r) {
return (Status) {a[l], a[l], a[l], a[l]};
}
int m = (l + r) >> 1;
Status lSub = get(a, l, m);
Status rSub = get(a, m + 1, r);
return pushUp(lSub, rSub);
}
int maxSubArray(vector<int>& nums) {
return get(nums, 0, nums.size() - 1).mSum;
}
};
56.合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.empty()) return {};
vector<vector<int>> res;
sort(intervals.begin(),intervals.end());
res.push_back(intervals[0]);
for(int i =1; i<intervals.size();i++){
int start = intervals[i][0];
int end = intervals[i][1];
if(start-res.back()[1]<1){
res.back()[1]=max(end,res.back()[1]);
}else{
res.push_back(intervals[i]);
}
}
return res;
}
};
189.轮转数组
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
我的,没有原地算法
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
vector<int> temp(n);
double a = 0;
for(int i= 0;i<n;i++){
a = (i+k)%n;
temp[a]=nums[i];
}
nums = temp;
}
};
数组翻转:
class Solution {
public:
void reverse(vector<int>& nums, int start, int end) {
while (start < end) {
swap(nums[start], nums[end]);
start += 1;
end -= 1;
}
}
void rotate(vector<int>& nums, int k) {
k %= nums.size();
reverse(nums, 0, nums.size() - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.size() - 1);
}
};
238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。请不要使用除法,且在 O(n) 时间复杂度内完成此题
左乘区间右乘区间
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> L(n, 0), R(n, 0);
vector<int> answer(n);
int ans = 1;
L[0]=1;
R[n-1]=1;
for(int i =1; i < n ;i++){
L[i] = L[i - 1] * nums[i - 1];
}
for(int i = n-2; i>=0;i--){
R[i] = R[i + 1] * nums[i + 1];
}
for(int i = 0; i<n; i++){
answer[i] = L[i] * R[i];
}
return answer;
}
};
空间复杂度为1的方法
初始化 answer 数组,对于给定索引 i,answer[i] 代表的是 i 左侧所有数字的乘积。
构造方式与之前相同,只是我们试图节省空间,先把 answer 作为方法一的 L 数组。
这种方法的唯一变化就是我们没有构造 R 数组。而是用一个遍历来跟踪右边元素的乘积。并更新数组 answer[i]=answer[i]∗R。然后 R 更新为 R=R∗nums[i],其中变量 R 表示的就是索引右侧数字的乘积。
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int length = nums.size();
vector<int> answer(length);
// answer[i] 表示索引 i 左侧所有元素的乘积
// 因为索引为 '0' 的元素左侧没有元素, 所以 answer[0] = 1
answer[0] = 1;
for (int i = 1; i < length; i++) {
answer[i] = nums[i - 1] * answer[i - 1];
}
// R 为右侧所有元素的乘积
// 刚开始右边没有元素,所以 R = 1
int R = 1;
for (int i = length - 1; i >= 0; i--) {
// 对于索引 i,左边的乘积为 answer[i],右边的乘积为 R
answer[i] = answer[i] * R;
// R 需要包含右边所有的乘积,所以计算下一个结果时需要将当前值乘到 R 上
R *= nums[i];
}
return answer;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/product-of-array-except-self/solutions/272369/chu-zi-shen-yi-wai-shu-zu-de-cheng-ji-by-leetcode-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
41.缺失的第一个正数
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
sort(nums.begin(),nums.end());
int i = 0 ;
int ans = 1;
if(*max_element(nums.begin(),nums.end())<=0) return ans;
while(nums[i]<=0){
i++;
}
if(nums[i]==1){
ans = nums[i]+1;
i++;
while(i<nums.size()&&(nums[i]-nums[i-1])<=1){
ans = nums[i]+1;
i++;
}
return ans;
}else{
return ans;
}
}
};
双指针
双指针类型:
1.两个指针,方向相反(升序排列中找到两个数使得两数的和等于给定值)
2.两个指针,方向相同(实现两个有序数组的合并 )
3.两个指针,快慢指针(判断一个单链表是否成环)
4.两个指针,方向相同,但是起点不同(返回单链表的倒数的第n个节点)
用 left 和 right 两个指针表示当前子数组的左右边界,初始时都指向数组开头。 用一个哈希表记录窗口内元素的出现次数。 right 向右移动,将当前元素加入窗口: 如果当前元素在窗口中未重复,继续移动 right,更新最长长度。 如果当前元素重复,移动 left,将窗口左边界向右收缩,直到窗口内不再有重复元素。 重复上述过程,直到 right 遍历完数组。
双指针主要就是左右边界的管理问题
// 双指针(滑动窗口)通用模板
#include
#include
using namespace std;
int slidingWindowTemplate(vector& nums) {
int n = nums.size();
int left = 0; // 窗口左边界
int result = 0; // 存储最终结果
// 可以根据需求定义辅助数据结构(哈希表、计数器等)
// 例如:unordered_map cnt; // 统计窗口内元素出现次数
// int valid = 0; // 标记窗口是否满足条件
for (int right = 0; right < n; right++) {
// 步骤1:进窗口——将当前元素加入窗口,更新辅助数据结构
// 例如:cnt[nums[right]]++;
// if (cnt[nums[right]] == 1) valid++;
// 步骤2:判条件——判断窗口是否需要收缩(根据题目要求调整)
// 条件可能是:窗口内有重复元素、窗口内元素和超过阈值、窗口满足目标要求等
while (/* 窗口不满足条件/需要优化 */) {
// 步骤3:出窗口——将左边界元素移出窗口,更新辅助数据结构
// 例如:cnt[nums[left]]--;
// if (cnt[nums[left]] == 0) valid--;
left++; // 收缩左边界
}
// 步骤4:更结果——窗口此时满足条件,更新最优结果
// 例如:result = max(result, right - left + 1);
}
return result;
}
283.移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。
考虑的方法是将非零的保存下来
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n = nums.size();
int k = 0; // k 指向下一个非 0 元素应该放的位置
// 1. 第一步:把非 0 元素全部移到前面
for(int i = 0; i < n; i++){
if (nums[i] != 0){
nums[k] = nums[i];
k++;
}
}
// 2. 第二步:剩下的位置全部补 0
// 此时 k 已经在最后一个非0数的后面了,直接从 k 开始填到最后即可
while(k < n){
nums[k] = 0;
k++;
}
}
};
11.盛最多水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。
左右往中间夹,双指针方法,属于方向相反
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0, right = (int)height.size() - 1;
int ans = 0;
while (left < right) {
int water = min(height[left], height[right]) * (right - left);
ans = max(ans, water);
if (height[left] < height[right]) left++;
else right--;
}
return ans;
}
};
优化后
class Solution {
public:
int maxArea(vector<int>& height) {
// 1. IO 加速 (C++ 刷题必备)
ios::sync_with_stdio(false);
cin.tie(nullptr);
int left = 0;
int right = height.size() - 1;
int ans = 0;
while(left < right){
int h_left = height[left];
int h_right = height[right];
// 计算当前最小高度
int min_h = min(h_left, h_right);
// 更新最大面积
// 优化点:直接用 min_h,避免重复访问数组
ans = max(ans, min_h * (right - left));
// 2. 逻辑优化:跳过所有比当前短板更矮的柱子
// 因为宽度在变小,如果高度还不增加,面积绝不可能变大
while(left < right && height[left] <= min_h) {
left++;
}
while(left < right && height[right] <= min_h) {
right--;
}
}
return ans;
}
};
15.三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
核心 定一找二。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int n = (int)nums.size();
vector<vector<int>> ans;
if (n < 3) return ans;
sort(nums.begin(), nums.end());
int i = 0;
int left = 1, right = n - 1;
for (int i = 0; i < n - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
if (nums[i] > 0) break;
int left = i + 1;
int right = n - 1;
while(left<right){
int sum = nums[i]+nums[left]+nums[right];
if (sum >0){
right--;
}else if(sum < 0){
left++;
}else{
ans.push_back({nums[i],nums[left],nums[right]});
// 4) 去重:跳过重复的 left/right
int lv = nums[left];
int rv = nums[right];
while (left < right && nums[left] == lv) left++;
while (left < right && nums[right] == rv) right--;
}
}
}
return ans;
}
};
42.接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
找到顶峰,然后左右分别遍历,找到一个比之前遍历过的高的可能就会形成空间能接雨水,所接的雨水等于两者之间形成的空间减去两者之间有的障碍物。
class Solution {
public:
int trap(vector<int>& height) {
int n = (int)height.size();
if (n < 3) return 0;
vector<long long> pre(n + 1, 0);
for (int i = 0; i < n; i++) pre[i + 1] = pre[i] + height[i];
int peak = 0;
for (int i = 1; i < n; i++) {
if (height[i] > height[peak]) peak = i;
}
long long sum = 0;
int level = 0;
int midSum=0;
int left = 0;
for (int right = 1; right <= peak; right++) {
if (height[right] >= height[left]) {
int w = right - left - 1;
if (w > 0) {
level = height[left];
midSum = pre[right] - pre[left + 1];
sum += level * w - midSum;
}
left = right;
}
}
int b = n - 1;
for (int right = n - 2; right >= peak; right--) {
if (height[right] >= height[b]) {
int w = b - right - 1;
if (w > 0) {
level = height[b];
midSum = pre[b] - pre[right + 1];
sum += level * w - midSum;
}
b = right;
}
}
return (int)sum;
}
};
560.和为K的子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。子数组是数组中元素的连续非空序列。
因为是连续的,可以想到区间和的方式来返回值,这样来判断的更加遍历。
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp; // 记录前缀和出现的次数
mp[0] = 1; // 基础情况:前缀和为0出现1次
int count = 0, pre = 0;
for (int x : nums) {
pre += x; // 计算当前前缀和
if (mp.count(pre - k)) { // 如果存在一个旧的前缀和,使得 pre - old = k
count += mp[pre - k];
}
mp[pre]++; // 记录当前前缀和
}
return count;
}
};
239.滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值。
C++ 双端队列 (std::deque) 学习笔记
1. 什么是 Deque?
std::deque (Double-Ended Queue) 是一个双端队列,允许在两端进行插入和删除操作。
- 头文件:
#include <deque> - 区别: 普通
queue只能 FIFO (先进先出),deque两头都能进出。
2. 常用操作函数
| 操作 | 代码 | 描述 |
|---|---|---|
| 入队 | dq.push_back(val) | 在尾部添加元素 |
dq.push_front(val) | 在头部添加元素 | |
| 出队 | dq.pop_back() | 删除尾部元素 (本题核心) |
dq.pop_front() | 删除头部元素 (窗口滑动时使用) | |
| 访问 | dq.front() | 访问头部元素 |
dq.back() | 访问尾部元素 | |
| 状态 | dq.empty() | 判断是否为空 |
3. 为什么这题要用 Deque?
我们在寻找滑动窗口最大值时,维护了一个单调递减队列。 当新元素进来时,如果比队尾元素大,我们需要把队尾元素踢出去 (pop_back) 。 普通的 std::queue 没有 pop_back() 功能,所以必须使用 std::deque。
💡 口诀: "新来的比你强,你就得走" ——
pop_back"你太老了,该退休了" ——pop_front
#include <vector>
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
deque<int> dq;
int n = nums.size();
for(int i = 0;i < n;i++){
while(!dq.empty()&& i-dq.front()==k){
dq.pop_front();
}
while(!dq.empty() && nums[dq.back()]<nums[i]){
dq.pop_back();
}
dq.push_back(i);
if(i>=k-1){
res.push_back(nums[dq.front()]);
}
}
return res;
}
};
76.最小覆盖子串
给定两个字符串 s 和 t,长度分别是 m 和 n,返回 s 中的 最短窗口 子串,使得该子串包含 t 中的每一个字符(包括重复字符)。如果没有这样的子串,返回空字符串 ""。
要找s中的t,先统计t中含有的字符有哪些,定义一个哈希表,存储t的元素
遍历字符串s,每找到一个数与t相同,就存储进去,并且计算个数相等时会返回,表明一个条件以满足,当所有条件都满足的时候,就遍历当前窗口(left为左边界,right为又边界),直到缩小到无法满足找到所有t为止,就会退出,继续遍历字符串s
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0; // 记录有多少种字符已经达到了需要的数量
int start = 0, len = INT_MAX;
while (right < s.size()) {
char c = s[right];
right++; // 扩大窗口
if (need.count(c)) {
window[c]++;
// 只有当这个字符的数量【刚好等于】需要的数量时,才算凑齐了一种
if (window[c] == need[c]) {
valid++;
}
}
// 当 valid 等于 need.size()(注意是 map 的大小,即字符种类数)
while (valid == need.size()) {
// 更新结果(注意:当前窗口是 [left, right),长度是 right - left)
if (right - left < len) {
start = left;
len = right - left;
}
char d = s[left];
left++; // 缩小窗口
if (need.count(d)) {
// 如果这个字符原本是凑齐的,现在减掉一个就不够了
if (window[d] == need[d]) {
valid--;
}
window[d]--;
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
};
📚 C++ String vs Vector 常用操作速查表
在 C++ STL 中,string 和 vector 有很多相似之处,但也有各自独有的骚操作。
1. 基础属性与访问
| 操作 | String (字符串) | Vector (数组) | 备注 |
|---|---|---|---|
| 求长度 | s.length() 或 s.size() | v.size() | String 两者皆可,Vector 只能用 size |
| 判空 | s.empty() | v.empty() | 返回 true/false |
| 访问元素 | s[i] | v[i] | 下标访问 |
| 首尾元素 | s.front(), s.back() | v.front(), v.back() |
2. 增加与删除 (动态修改)
| 操作 | String (字符串) | Vector (数组) | 备注 |
|---|---|---|---|
| 尾部添加 | s += 'a'; 或 s.push_back('a') | v.push_back(1); | String 用 += 最方便 |
| 尾部删除 | s.pop_back() | v.pop_back() | 删掉最后一个元素 |
| 清空 | s.clear() | v.clear() | 变成空,size 为 0 |
| 中间插入 | s.insert(pos, "str") | v.insert(it, val) | 效率较低 O(N),慎用 |
| 中间删除 | s.erase(pos, len) | v.erase(it) | 效率较低 O(N) |
3. 核心独有操作 (重点区分) 🌟
🅰️ String 特有
-
截取子串:
// 从下标 1 开始,截取 3 个字符 string sub = s.substr(1, 3); -
查找子串/字符:
// 找到返回下标,找不到返回 string::npos int pos = s.find("abc"); if (pos != string::npos) { ... } -
拼接:
string newS = s1 + s2; // 直接相加 -
转 C 风格字符串:
s.c_str()(某些老接口需要)
🅱️ Vector 特有 (截取方法)
Vector 没有 substr,必须利用迭代器构造:
// 截取 nums[1] 到 nums[2] (左闭右开)
// 也就是复制 {nums[1], nums[2]}
vector<int> sub(nums.begin() + 1, nums.begin() + 3);
##53.最大子数组和
###给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
class Solution { public: int maxSubArray(vector& nums) { int ans = nums[0]; int preSum = 0; int minPreSum = 0; // 记录前面出现的最小值
for (int x : nums) {
preSum += x; // 当前前缀和
ans = max(ans, preSum - minPreSum); // 当前减去最小值,就是最大的可能
minPreSum = min(minPreSum, preSum); // 更新最小值,留给后面用
}
return ans;
}
};