第6天:哈希表进阶 学习目标
掌握双指针+哈希表的组合技巧
理解前缀和+哈希表的应用
学会设计类问题:LRU、LFU缓存
掌握字符串匹配和模式识别
双指针+哈希表 组合特点
双指针 :维护窗口或搜索范围
哈希表 :记录状态或计数
优势 :结合两种算法的优点
应用 :子数组、子字符串问题
解题模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int twoPointersWithHash (vector<int >& nums) { unordered_map<int , int > count; int left = 0 , right = 0 ; int result = 0 ; while (right < nums.size ()) { count[nums[right]]++; while (condition) { count[nums[left]]--; left++; } result = max (result, right - left + 1 ); right++; } return result; }
今日重点题目 1. 三数之和 (Medium) 题目链接: 15. 三数之和
题目描述: 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
解题思路: 排序+双指针,固定一个数,用双指针在剩余数组中找两数之和。
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Solution {public : vector<vector<int >> threeSum (vector<int >& nums) { vector<vector<int >> result; int n = nums.size (); if (n < 3 ) return result; sort (nums.begin (), nums.end ()); for (int i = 0 ; i < n - 2 ; i++) { if (i > 0 && nums[i] == nums[i-1 ]) continue ; int left = i + 1 , right = n - 1 ; while (left < right) { int sum = nums[i] + nums[left] + nums[right]; if (sum == 0 ) { result.push_back ({nums[i], nums[left], nums[right]}); while (left < right && nums[left] == nums[left+1 ]) left++; while (left < right && nums[right] == nums[right-1 ]) right--; left++; right--; } else if (sum < 0 ) { left++; } else { right--; } } } return result; } };
时间复杂度: O(n²)空间复杂度: O(1)
2. 和为K的子数组 (Medium) 题目链接: 560. 和为K的子数组
题目描述: 给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的连续子数组的个数。
解题思路: 前缀和+哈希表,记录前缀和的出现次数,对于当前前缀和,查找是否存在前缀和等于当前前缀和-k。
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution {public : int subarraySum (vector<int >& nums, int k) { unordered_map<int , int > prefixSumCount; prefixSumCount[0 ] = 1 ; int prefixSum = 0 , count = 0 ; for (int num : nums) { prefixSum += num; if (prefixSumCount.find (prefixSum - k) != prefixSumCount.end ()) { count += prefixSumCount[prefixSum - k]; } prefixSumCount[prefixSum]++; } return count; } };
时间复杂度: O(n)空间复杂度: O(n)
3. LRU缓存 (Medium) 题目链接: 146. LRU缓存
题目描述: 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最近最少使用的关键字。
解题思路: 双向链表+哈希表,双向链表维护访问顺序,哈希表快速查找节点。
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 class LRUCache {private : struct Node { int key, value; Node* prev, *next; Node (int k = 0 , int v = 0 ) : key (k), value (v), prev (nullptr ), next (nullptr ) {} }; int capacity; Node* head, *tail; unordered_map<int , Node*> cache; void addNode (Node* node) { node->prev = head; node->next = head->next; head->next->prev = node; head->next = node; } void removeNode (Node* node) { node->prev->next = node->next; node->next->prev = node->prev; } void moveToHead (Node* node) { removeNode (node); addNode (node); } Node* popTail () { Node* last = tail->prev; removeNode (last); return last; } public : LRUCache (int capacity) : capacity (capacity) { head = new Node (); tail = new Node (); head->next = tail; tail->prev = head; } int get (int key) { if (cache.find (key) != cache.end ()) { Node* node = cache[key]; moveToHead (node); return node->value; } return -1 ; } void put (int key, int value) { if (cache.find (key) != cache.end ()) { Node* node = cache[key]; node->value = value; moveToHead (node); } else { Node* newNode = new Node (key, value); if (cache.size () >= capacity) { Node* tail = popTail (); cache.erase (tail->key); delete tail; } cache[key] = newNode; addNode (newNode); } } };
时间复杂度: O(1)空间复杂度: O(capacity)
4. 最小覆盖子串 (Hard) 题目链接: 76. 最小覆盖子串
题目描述: 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
解题思路: 滑动窗口+哈希表,维护一个包含t中所有字符的窗口,记录最小窗口。
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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.length ()) { char c = s[right]; right++; if (need.count (c)) { window[c]++; if (window[c] == need[c]) { valid++; } } while (valid == need.size ()) { 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); } };
时间复杂度: O(|s| + |t|)空间复杂度: O(|s| + |t|)
前缀和+哈希表 前缀和概念 前缀和是指数组中从开始位置到当前位置的所有元素的和。
前缀和公式
prefixSum[i] = prefixSum[i-1] + nums[i]
子数组和:sum(i, j) = prefixSum[j] - prefixSum[i-1]
前缀和+哈希表模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int prefixSumWithHash (vector<int >& nums, int target) { unordered_map<int , int > prefixSumCount; prefixSumCount[0 ] = 1 ; int prefixSum = 0 , count = 0 ; for (int num : nums) { prefixSum += num; if (prefixSumCount.find (prefixSum - target) != prefixSumCount.end ()) { count += prefixSumCount[prefixSum - target]; } prefixSumCount[prefixSum]++; } return count; }
扩展练习 5. 四数之和 (Medium) 题目链接: 18. 四数之和
6. 连续的子数组和 (Medium) 题目链接: 523. 连续的子数组和
7. 和为S的两个数字 (Easy) 题目链接: 剑指 Offer 57. 和为s的两个数字
8. 无重复字符的最长子串 (Medium) 题目链接: 3. 无重复字符的最长子串
学习总结 双指针+哈希表要点
窗口维护 :保持窗口内元素满足条件
状态记录 :用哈希表记录窗口状态
边界处理 :正确处理指针移动
去重处理 :避免重复结果
前缀和+哈希表要点
前缀和计算 :正确计算前缀和
哈希表记录 :记录前缀和出现次数
目标查找 :查找目标前缀和
边界初始化 :正确初始化哈希表
设计类问题要点
数据结构选择 :选择合适的底层数据结构
操作实现 :正确实现各种操作
状态维护 :维护数据结构的正确状态
边界处理 :处理各种边界情况
常见错误
双指针移动条件错误
前缀和计算错误
哈希表键值类型错误
设计类问题状态维护错误
今日收获
掌握了双指针+哈希表的组合技巧
理解了前缀和+哈希表的应用
学会了设计类问题的解法
建立了更复杂的哈希表思维
明日预告 明天我们将进行综合练习,结合DP、Tree、Map三种算法解决复杂问题,这是本周的总结和提升。
返回周计划 | 上一题:哈希表基础 | 下一题:综合练习