引言
在算法世界中,哈希表(Hash Table) 是一个非常强大又常用的工具。它不仅能实现快速查找,还能用于数据归类、去重、统计等任务。
今天,我们就通过两道经典的 LeetCode 题目:
- LeetCode 128. 最长连续序列(Longest Consecutive Sequence)
- LeetCode 49. 字母异位词分组(Group Anagrams)
来一起看看 哈希表的两种典型应用场景。
哈希表
什么是哈希表?
哈希表(Hash Table)是一种用“键”来快速查找数据的数据结构。你可以把它想象成一个“超级快的字典”:你给它一个“关键词”(key),它就能立刻告诉你对应的“内容”(value)。
比如:
- 你问:“‘apple’对应的中文是什么?”
- 哈希表立刻回答:“‘苹果’。
哈希表的基本用法
哈希表最常用的两种形式是:
- 字典(map) :存储键值对,比如
{ "name": "小明" } - 集合(set) :存储唯一值,比如
{1, 2, 3}
在不同的编程语言中,哈希表可能有不同的名称:
- 在 Python 中,它叫
dict(字典) - 在 C++ 中,它叫
unordered_map - 在 Java 中,它叫
HashMap
无论叫什么名字,它们的核心功能都差不多:快速查找、插入和删除。
哈希表的三大特点
-
查找特别快
不管里面有多少数据,查找一个值是否存在,或者查找某个 key 对应的 value,几乎都是“瞬间完成”的。 -
插入和删除也快
添加或删除一个数据也非常高效。 -
可以用来去重、归类、统计
- 用集合(set)去掉重复的数字
- 用字典(map)统计每个单词出现的次数
- 把相同特征的数据归类在一起(比如字母异位词分组
常见操作及方法
1. 插入数据
插入数据就是给哈希表添加一个新的键值对。
unordered_map<string, string> myMap;
myMap["apple"] = "苹果";
2. 查找数据
查找数据是通过键来获取对应的值。
if (myMap.find("apple") != myMap.end()) {
cout << myMap["apple"] << endl; // 输出: 苹果
}
3. 删除数据
删除数据是根据键来移除相应的键值对。
myMap.erase("apple");
4. 遍历哈希表
遍历哈希表可以查看所有的键值对。
for (auto& pair : myMap) {
cout << pair.first << ": " << pair.second << endl;
}
5. 检查键是否存在
检查某个键是否存在于哈希表中。
if (myMap.find("apple") != myMap.end()) {
cout << "存在" << endl;
}
6. 获取所有键或值
有时候我们需要获取哈希表中的所有键或所有值。
vector<string> keys;
vector<string> values;
for (auto& pair : myMap) {
keys.push_back(pair.first); // 键
values.push_back(pair.second); // 值
}
小结一句话:
哈希表就像一本查得特别快的字典,能让你在一堆数据中快速找到你要的东西,或者把东西按规则归类。掌握几个基本操作——插入、查找、删除、遍历,就能应对大多数日常需求了。
题目解析
一、找最长连续序列:哈希集合 + 智能判断起点
🧩 问题描述
给定一个未排序的整数数组 nums,找出最长的连续数字序列的长度。例如:
输入:[100, 4, 200, 1, 3, 2]
输出:4
解释:最长连续序列是 [1, 2, 3, 4]
传统的做法可能是先排序,再遍历找出最长连续序列。但排序的时间复杂度是 O(n log n),不是我们想要的。我们希望找到一个线性时间 O(n) 的解法。
这时候,哈希集合就派上用场了。
我们首先将所有数字放入一个 unordered_set 中,这样查找一个数字是否存在的操作只需要 O(1) 的时间。然后我们遍历集合中的每一个数字,只有当当前数字的前一个数字不存在时,才开始向上查找连续序列。
比如,如果当前数字是 3,而 2 不在集合中,那 3 就可能是一个连续序列的起点。
我们从这个起点开始,不断查找 current_num + 1 是否存在,直到找不到为止,同时记录当前序列的长度。
在整个过程中,我们只对每个数字处理一次,因此时间复杂度是 O(n),空间复杂度也是 O(n)。
这个思路的关键在于:我们只从真正的起点开始查找,避免了重复遍历。而哈希集合的快速查找能力,让我们可以轻松判断一个数字是否存在。
核心代码(C++):
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
int length = 0;
unordered_set<int> numSet;
for(int i : nums){
numSet.insert(i);// 用set集合存储
}
for(int num : numSet){
if(numSet.find(num - 1) == numSet.end()){
int currentNum = num;
int currentLength = 1;
while(numSet.find(currentNum + 1) != numSet.end()){
currentNum++;
currentLength++;
}
//更新最长长度
length = max(length,currentLength);
}
}
return length;
}
};
哈希的妙用
在这个问题中,我们用哈希集合来:
- 快速查找一个数字是否存在
- 避免重复查找连续序列
- 实现 O(n) 时间复杂度
二、字母异位词分组:哈希映射 + 智能归类
🧩 问题描述
给定一个字符串数组,将字母异位词(即字母相同但顺序不同的字符串)归类到一起。例如:
输入:["eat", "tea", "tan", "ate", "nat", "bat"]
输出:[["bat"],["nat","tan"],["ate","eat","tea"]]
这道题的核心在于,如何判断两个字符串是否是字母异位词。一个很聪明的办法是:将每个字符串排序后作为 key。
因为字母异位词排序后会得到完全相同的字符串。
于是,我们可以使用一个 unordered_map<string, vector<string>>,其中 key 是排序后的字符串,value 是原始字符串的列表。我们对每个字符串进行排序,生成 key,然后将原始字符串插入到对应的 value 中。
最后,我们只需要遍历整个哈希表,把所有的 value 提取出来,就能得到最终结果。
这个思路的巧妙之处在于,我们用排序后的字符串作为统一的“指纹” ,然后利用哈希表自动归类的功能,把所有相同特征的字符串归为一组。
核心代码(C++):
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> map;
for(string& s : strs){
string key = s;
sort(key.begin(),key.end());// 按ASCII码从小到大排序作为key
map[key].push_back(s);// 相同key会归类到一起
}
vector<vector<string>> result;
for(auto& str : map){
result.push_back(str.second);// 把归类好的value给push到result
}
return result;
}
};
哈希的妙用
在这个问题中,我们用哈希映射来:
- 自动归类具有相同字母的字符串
- 利用排序后的字符串作为统一的 key
总结:哈希的两种经典用法
| 问题 | 哈希结构 | 用途 | 核心技巧 |
|---|---|---|---|
| 最长连续序列 | unordered_set | 快速查找 | 判断是否是连续序列起点 |
| 字母异位词分组 | unordered_map<string, vector<string>> | 智能归类 | 用排序字符串作为 key |
通过这两道题,我们可以看到,哈希思想在算法中的应用非常广泛。它不仅帮助我们快速查找,还能帮我们高效归类、去重、统计,甚至解决一些看似复杂的问题。
无论你是刚开始学习算法的新手,还是正在准备面试的进阶者,掌握哈希思想,都将成为你解决问题的重要武器。