哈希表通关八题:从两数之和到LRU缓存,手撕高频面试题(Python + C++)
哈希表是算法面试中使用频率最高的数据结构之一。本文整理了8道经典题目,每道题都包含:题目描述、解题思路、图解(文本示意)、Python代码、C++代码、时间复杂度与空间复杂度分析。掌握这些,哈希表类题目基本通关。
📌 题目清单
| 题号 | 题目 | 核心考点 |
|---|---|---|
| 1 | 两数之和 | 哈希表 O(n) |
| 217 | 存在重复元素 | 集合去重 |
| 349 | 两个数组的交集 | 哈希集合 |
| 242 | 有效的字母异位词 | 字符计数 / 排序 |
| 387 | 字符串中的第一个唯一字符 | 哈希计数 |
| 49 | 字母异位词分组 | 哈希映射(键为排序后字符串) |
| 146 | LRU 缓存 | 哈希表 + 双向链表(高频) |
| 3 | 无重复字符的最长子串 | 滑动窗口 + 哈希表 |
1. 两数之和(LeetCode 1)
题目描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 的两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,且同一个元素不能使用两遍。
示例 :
输入:nums = [2,7,11,15], target = 9
输出:[0,1] 解释:nums[0] + nums[1] = 2 + 7 = 9
解题思路
- 遍历数组,对于每个元素
nums[i],检查target - nums[i]是否已经在哈希表中。 - 如果在,直接返回当前下标和哈希表中存储的下标。
- 如果不在,将当前元素的值和下标存入哈希表。
图解
nums = [2,7,11,15], target = 9
i=0: num=2, need=7, map{} → 存入 {2:0}
i=1: num=7, need=2, map中有2 → 返回 [0,1]
Python代码
python
def twoSum(nums, target):
seen = {}
for i, num in enumerate(nums):
need = target - num
if need in seen:
return [seen[need], i]
seen[num] = i
return []
C++代码
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> seen;
for (int i = 0; i < nums.size(); ++i) {
int need = target - nums[i];
if (seen.count(need)) {
return {seen[need], i};
}
seen[nums[i]] = i;
}
return {};
}
};
复杂度分析
- 时间复杂度:O(n),只需遍历一次数组,哈希表查找 O(1)。
- 空间复杂度:O(n),哈希表最多存储 n 个元素。
2. 存在重复元素(LeetCode 217)
题目描述
给定一个整数数组,判断是否存在重复元素。如果任一值在数组中出现至少两次,返回 true;否则返回 false。
示例 :
输入:[1,2,3,1] → 输出:true
输入:[1,2,3,4] → 输出:false
解题思路
使用哈希集合,遍历数组时检查当前元素是否已在集合中,若在则返回 true,否则加入集合。
图解
nums = [1,2,3,1]
集合: {} → 加入1 → {1} → 加入2 → {1,2} → 加入3 → {1,2,3} → 遇到1,已存在 → 返回 true
Python代码
python
def containsDuplicate(nums):
seen = set()
for num in nums:
if num in seen:
return True
seen.add(num)
return False
C++代码
cpp
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
unordered_set<int> seen;
for (int num : nums) {
if (seen.count(num)) return true;
seen.insert(num);
}
return false;
}
};
复杂度分析
- 时间复杂度:O(n),遍历一次,集合查找插入 O(1) 平均。
- 空间复杂度:O(n),集合最多存储 n 个元素。
3. 两个数组的交集(LeetCode 349)
题目描述
给定两个数组 nums1 和 nums2,返回它们的交集。输出结果中的每个元素一定是唯一的,可以不考虑输出结果的顺序。
示例 :
输入:nums1 = [1,2,2,1], nums2 = [2,2] → 输出:[2]
解题思路
- 将
nums1转换为哈希集合。 - 遍历
nums2,如果元素在集合中,则加入结果集(自动去重),并从集合中删除以避免重复添加。
图解
nums1 = [1,2,2,1] → set1 = {1,2}
nums2 = [2,2] → 遍历: 2在set1中 → 加入结果,删除2 → set1={1}
结果: [2]
Python代码
python
def intersection(nums1, nums2):
set1 = set(nums1)
result = set()
for num in nums2:
if num in set1:
result.add(num)
return list(result)
C++代码
cpp
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> set1(nums1.begin(), nums1.end());
unordered_set<int> res;
for (int num : nums2) {
if (set1.count(num)) {
res.insert(num);
}
}
return vector<int>(res.begin(), res.end());
}
};
复杂度分析
- 时间复杂度:O(m + n),其中 m、n 分别为两个数组的长度。
- 空间复杂度:O(min(m, n)),结果集和哈希集合的大小。
4. 有效的字母异位词(LeetCode 242)
题目描述
给定两个字符串 s 和 t,编写一个函数判断 t 是否是 s 的字母异位词(即字符种类和数量相同,顺序不同)。
示例 :
输入:s = "anagram", t = "nagaram" → 输出:true
输入:s = "rat", t = "car" → 输出:false
解题思路
- 如果长度不同,直接返回 false。
- 使用长度为26的数组(或哈希表)统计
s中每个字符的出现次数。 - 遍历
t,遇到字符则对应计数减1,如果减后小于0则返回 false。 - 最后所有计数应为0。
图解
s = "anagram", t = "nagaram"
计数数组: a:3, n:1, g:1, r:1, m:1
遍历t:
n → a:3,n:0,g:1,r:1,m:1
a → a:2,n:0,g:1,r:1,m:1
g → a:2,n:0,g:0,r:1,m:1
a → a:1,n:0,g:0,r:1,m:1
r → a:1,n:0,g:0,r:0,m:1
a → a:0,n:0,g:0,r:0,m:1
m → a:0,n:0,g:0,r:0,m:0 → true
Python代码
python
def isAnagram(s, t):
if len(s) != len(t):
return False
count = [0] * 26
for ch in s:
count[ord(ch) - ord('a')] += 1
for ch in t:
idx = ord(ch) - ord('a')
count[idx] -= 1
if count[idx] < 0:
return False
return True
C++代码
cpp
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.size() != t.size()) return false;
vector<int> count(26, 0);
for (char c : s) count[c - 'a']++;
for (char c : t) {
int idx = c - 'a';
if (--count[idx] < 0) return false;
}
return true;
}
};
复杂度分析
- 时间复杂度:O(n),n 为字符串长度。
- 空间复杂度:O(1),固定大小的计数数组。
5. 字符串中的第一个唯一字符(LeetCode 387)
题目描述
给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。
示例 :
输入:s = "leetcode" → 输出:0
输入:s = "loveleetcode" → 输出:2
解题思路
- 第一遍遍历,使用哈希表统计每个字符出现的次数。
- 第二遍遍历,找到第一个出现次数为1的字符,返回其索引。
图解
s = "loveleetcode"
统计: l:1, o:1, v:1, e:3, t:1, c:1, d:1
遍历: 索引0 'l' 次数1 → 返回0
Python代码
python
def firstUniqChar(s):
count = {}
for ch in s:
count[ch] = count.get(ch, 0) + 1
for i, ch in enumerate(s):
if count[ch] == 1:
return i
return -1
C++代码
cpp
class Solution {
public:
int firstUniqChar(string s) {
unordered_map<char, int> count;
for (char c : s) count[c]++;
for (int i = 0; i < s.size(); ++i) {
if (count[s[i]] == 1) return i;
}
return -1;
}
};
复杂度分析
- 时间复杂度:O(n),遍历两次字符串。
- 空间复杂度:O(1) 或 O(Σ),由于字符集有限(26个字母),可视为 O(1)。
6. 字母异位词分组(LeetCode 49)
题目描述
给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同但排列不同的字符串。
示例 :
输入:strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出:[["bat"],["nat","tan"],["ate","eat","tea"]]
解题思路
- 使用哈希表,键为排序后的字符串,值为该组异位词的列表。
- 遍历每个字符串,排序后作为键,将原字符串加入对应的列表。
图解
strs = ["eat","tea","tan","ate","nat","bat"]
"eat" -> 排序"aet" -> map["aet"] = ["eat"]
"tea" -> 排序"aet" -> map["aet"] = ["eat","tea"]
"tan" -> 排序"ant" -> map["ant"] = ["tan"]
"ate" -> 排序"aet" -> map["aet"] = ["eat","tea","ate"]
"nat" -> 排序"ant" -> map["ant"] = ["tan","nat"]
"bat" -> 排序"abt" -> map["abt"] = ["bat"]
最终输出所有value
Python代码
python
def groupAnagrams(strs):
from collections import defaultdict
groups = defaultdict(list)
for s in strs:
key = ''.join(sorted(s))
groups[key].append(s)
return list(groups.values())
C++代码
cpp
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> groups;
for (string& s : strs) {
string key = s;
sort(key.begin(), key.end());
groups[key].push_back(s);
}
vector<vector<string>> res;
for (auto& p : groups) {
res.push_back(p.second);
}
return res;
}
};
复杂度分析
- 时间复杂度:O(n × k log k),n 为字符串个数,k 为字符串最大长度;排序每个字符串 O(k log k)。
- 空间复杂度:O(n × k),存储所有字符串。
7. LRU 缓存(LeetCode 146)
题目描述
设计和实现一个 LRU (最近最少使用) 缓存机制。支持 get(key) 和 put(key, value) 操作,平均时间复杂度 O(1)。
要求:
get(key):如果 key 存在,返回 value,否则返回 -1,并将该 key 标记为最近使用。put(key, value):如果 key 存在,修改 value 并标记为最近使用;否则插入新键值对,若缓存达到容量,则删除最久未使用的键。
解题思路
- 哈希表:存储 key 到节点的映射,实现 O(1) 查找。
- 双向链表:维护访问顺序,最近使用的节点放在头部,最久未使用的放在尾部。
get:从哈希表找到节点,将其移动到链表头部。put:若 key 存在,更新值并移动到头部;若不存在,插入新节点到头部,如果超过容量则删除尾部节点并从哈希表中删除。
图解
容量=2,操作序列: put(1,1), put(2,2), get(1), put(3,3)
init: 空
put(1,1): head<->1<->tail
put(2,2): head<->2<->1<->tail
get(1): 将1移到头部: head<->1<->2<->tail
put(3,3): 容量满,删除尾部2: head<->3<->1<->tail
Python代码
python
class DLinkedNode:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.cache = {}
self.capacity = capacity
self.size = 0
self.head = DLinkedNode()
self.tail = DLinkedNode()
self.head.next = self.tail
self.tail.prev = self.head
def _add_node(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def _remove_node(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _move_to_head(self, node):
self._remove_node(node)
self._add_node(node)
def _pop_tail(self):
node = self.tail.prev
self._remove_node(node)
return node
def get(self, key: int) -> int:
if key in self.cache:
node = self.cache[key]
self._move_to_head(node)
return node.value
return -1
def put(self, key: int, value: int) -> None:
if key in self.cache:
node = self.cache[key]
node.value = value
self._move_to_head(node)
else:
node = DLinkedNode(key, value)
self.cache[key] = node
self._add_node(node)
self.size += 1
if self.size > self.capacity:
tail = self._pop_tail()
del self.cache[tail.key]
self.size -= 1
C++代码
cpp
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int k, int v): key(k), value(v), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int capacity;
int size;
void addToHead(DLinkedNode* node) {
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(DLinkedNode* node) {
removeNode(node);
addToHead(node);
}
DLinkedNode* removeTail() {
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
public:
LRUCache(int capacity) {
this->capacity = capacity;
this->size = 0;
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (!cache.count(key)) return -1;
DLinkedNode* node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
if (cache.count(key)) {
DLinkedNode* node = cache[key];
node->value = value;
moveToHead(node);
} else {
DLinkedNode* node = new DLinkedNode(key, value);
cache[key] = node;
addToHead(node);
++size;
if (size > capacity) {
DLinkedNode* tailNode = removeTail();
cache.erase(tailNode->key);
delete tailNode;
--size;
}
}
}
};
复杂度分析
- 时间复杂度:get 和 put 均为 O(1)。
- 空间复杂度:O(capacity),缓存大小。
8. 无重复字符的最长子串(LeetCode 3)
题目描述
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 :
输入:"abcabcbb" → 输出:3("abc")
输入:"bbbbb" → 输出:1
输入:"pwwkew" → 输出:3("wke")
解题思路:滑动窗口 + 哈希表
- 使用两个指针
left和right表示窗口边界,哈希表记录字符最近出现的位置。 - 遍历字符串,
right右移:- 如果当前字符已在哈希表中且其索引 >= left,说明窗口内有重复,将
left移动到重复字符的下一个位置。 - 更新哈希表中字符的索引为当前
right。 - 计算当前窗口长度
right - left + 1,更新最大长度。
- 如果当前字符已在哈希表中且其索引 >= left,说明窗口内有重复,将
图解
s = "abcabcbb"
left=0, right=0: 'a' 未出现 -> map{a:0}, maxLen=1
right=1: 'b' -> map{a:0,b:1}, maxLen=2
right=2: 'c' -> map{a:0,b:1,c:2}, maxLen=3
right=3: 'a' 已存在且索引0>=left -> left=1, 更新a:3, 窗口[1,3]="bca" 长度3
right=4: 'b' 已存在索引1>=left -> left=2, 更新b:4, 窗口[2,4]="cab" 长度3
right=5: 'c' 索引2>=left -> left=3, 更新c:5, 窗口[3,5]="abc" 长度3
right=6: 'b' 索引4>=left -> left=5, 更新b:6, 窗口[5,6]="cb" 长度2
right=7: 'b' 索引6>=left -> left=7, 窗口[7,7]="b" 长度1
最大长度=3
Python代码
python
def lengthOfLongestSubstring(s: str) -> int:
char_index = {}
left = 0
max_len = 0
for right, ch in enumerate(s):
if ch in char_index and char_index[ch] >= left:
left = char_index[ch] + 1
char_index[ch] = right
max_len = max(max_len, right - left + 1)
return max_len
C++代码
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> charIndex;
int left = 0, maxLen = 0;
for (int right = 0; right < s.size(); ++right) {
char c = s[right];
if (charIndex.count(c) && charIndex[c] >= left) {
left = charIndex[c] + 1;
}
charIndex[c] = right;
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
};
复杂度分析
- 时间复杂度:O(n),每个字符最多被 left 和 right 指针各遍历一次。
- 空间复杂度:O(min(n, Σ)),哈希表存储字符,字符集大小有限(如 ASCII 128),可视为 O(1)。
🎯 总结
| 题目 | 核心技巧 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 1. 两数之和 | 哈希表记录补数 | O(n) | O(n) |
| 217. 存在重复元素 | 集合去重 | O(n) | O(n) |
| 349. 两个数组的交集 | 哈希集合 | O(m+n) | O(min(m,n)) |
| 242. 有效的字母异位词 | 字符计数数组 | O(n) | O(1) |
| 387. 第一个唯一字符 | 两次遍历+计数 | O(n) | O(1) |
| 49. 字母异位词分组 | 排序后作为哈希键 | O(n×k log k) | O(n×k) |
| 146. LRU缓存 | 哈希表+双向链表 | O(1) per op | O(capacity) |
| 3. 无重复最长子串 | 滑动窗口+哈希表 | O(n) | O(min(n,Σ)) |
哈希表的核心价值在于 O(1) 的查找和插入,常用于:判重、计数、映射、缓存等场景。熟练掌握这几道题,哈希表相关题目基本都能应对。