12.22-12.28学习总结

18 阅读22分钟

哈希表的学习

计算区间和的方式

image-20251222142434520.png

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 的引用。
  • 优势:无需编写 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) 的保留类名。虽然在局部作用域可能不报错,但在大型项目中极易产生命名冲突。
  • 建议:使用 mpcountsgroupsanagramMap

✅ 最终优化版代码

 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 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

image-20251222142657710

找到顶峰,然后左右分别遍历,找到一个比之前遍历过的高的可能就会形成空间能接雨水,所接的雨水等于两者之间形成的空间减去两者之间有的障碍物。

 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 中,stringvector 有很多相似之处,但也有各自独有的骚操作。

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. 截取子串:

     // 从下标 1 开始,截取 3 个字符
     string sub = s.substr(1, 3); 
    
  2. 查找子串/字符:

     // 找到返回下标,找不到返回 string::npos
     int pos = s.find("abc"); 
     if (pos != string::npos) { ... }
    
  3. 拼接:

     string newS = s1 + s2; // 直接相加
    
  4. 转 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;
 }

};

 ​
 ​