系列文章目录
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
提示:这里可以添加本文要记录的大概内容:
一、哈希表基础知识
在力扣习题当中,哈希表是非常常见的知识点。 哈希表通过哈希函数将键映射到索引位置,因此在理想情况下,查找和插入操作的时间复杂度为O(1)。这使得哈希表非常适合于需要快速检索的场景。 因此,在某些场景中有重要的作用。本文中主要介绍哈希表的两种常见形式:unordered_map和unordered_set,这也是做题中最常用的两种类型。
1.1 unordered_map
unordered_map是一个将key和value关联起来的容器,它可以高效的根据单个key值查找对应的value。key应该是唯一的,key和value的数据类型可以不相同。
与 map 不同,unordered_map 不保证元素的顺序,而是通过哈希函数将键映射到桶(buckets),因此插入、删除和查找操作的平均时间复杂度都是常数时间 O(1)。适用于检索单个元素,效率很高。
下面是unordered_map的C++基本用法。其中,最重要的是find函数。他用于查找key所在的元素。如果找到则返回指向对应键值对的迭代器,否则返回 end() 迭代器,表示未找到该键。
//STL中map的基本用法
#include <unordered_map>
void basic()
{
std::unordered_map<std::string, int> myMap;
// 添加键值对
myMap["Alice"] = 25;
myMap["Bob"] = 30;
myMap["Charlie"] = 35;
//删除键值对
myMap.erase("Bob");
// 检查键是否存在
if (myMap.find("David") != myMap.end()) {
std::cout << "David's age is: " << myMap["David"] << std::endl;
}
else {
std::cout << "David is not in the map." << std::endl;
}
// 遍历哈方法1
for (auto it = myMap.begin(); it != myMap.end(); it++) {
std::cout << it->first << "'s age is: " << it->second << std::endl;
}
// 遍历方法2
for (const auto& pair : myMap) {
std::cout << pair.first << "'s age is: " << pair.second << std::endl;
}
}
在构造哈希表的时候,最容易出现问题的地方在于分不清键(key)和值(value),从而导致代码逻辑混乱。对于不同类型的键值,比较好区分,但是当有嵌套哈希表或者键值表示的意义接近时候,就很容易混淆。
首先我们明确一下,对于初始化的定义,第一个类型string是键,第二个类型int是值。
std::unordered_map<std::string, int> myMap;
我们插入元素的操作通常类似于数组形式,如下:
myMap["Alice"] = 25;
此时,在[]里面的Alice是键,而等于号右边的25是值。同时,在find函数中,我们给定的参数是键,函数会在哈希表中寻找该键。
这样说可能有点抽象,咱们来一个使用哈希表的常见案例。
对于一个字符串,我们想要统计每个字母出现的次数。
这就很好理解了,我们可以把26个字母作为哈希表的键,而他们出现的次数作为相对应的值。对每个字符遍历,如果字母已经存在,则增加其计数,否则将其计数设置为 1。最后,通过遍历 unordered_map,可以输出每个字母的出现次数。代码如下:
void countCharacters(const std::string& str) {
std::unordered_map<char, int> charCount;
// 遍历字符串,统计每个字母出现的次数
for (char c : str) {
if (std::isalpha(c)) { // 只统计字母
charCount[c]++;
}
}
// 输出结果
for (const auto& pair : charCount) {
std::cout << "'" << pair.first << "' 出现了 " << pair.second << " 次" << std::endl;
}
}
1.2 unordered_set
第二个比较常用的数据结构是std::unordered_set ,它是一个集合容器,用于存储唯一的元素,没有重复的值。它类似于数学上的集合。下面代码介绍他的基本用法。
void unorder_set_basic() {
unordered_set<int> myHashSet;
// 插入元素
myHashSet.insert(10);
myHashSet.insert(5);
myHashSet.insert(8);
myHashSet.insert(12);
// 查找元素并输出
int searchValue = 8;
std::unordered_set<int>::iterator it = myHashSet.find(searchValue);
if (it != myHashSet.end()) {
std::cout << "元素 " << searchValue << " 存在于集合中。" << std::endl;
}
else {
std::cout << "元素 " << searchValue << " 不存在于集合中。" << std::endl;
}
// 删除元素
myHashSet.erase(5);
// 遍历集合并输出
std::cout << "集合中的元素:";
for (const int& value : myHashSet) {
std::cout << value << " ";
}
std::cout << std::endl;
// 检查集合是否为空
if (myHashSet.empty()) {
std::cout << "集合为空。" << std::endl;
}
else {
std::cout << "集合不为空,大小为:" << myHashSet.size() << std::endl;
}
// 使用 count 检查元素是否存在
int elementToCheck = 3;
if (myHashSet.count(elementToCheck) == 1) {
std::cout << elementToCheck << " 存在于集合中。" << std::endl;
}
else {
std::cout << elementToCheck << " 不存在于集合中。" << std::endl;
}
}
这个数据结构适用的场所在于,当题目中存在重复的元素,而我们想要不重复的结果时候,就可以用到unordered_set了。下面举一个例子。
给定一个整数数组,判断是否存在重复的元素。
这个题目可以很简单的用unordered_set 解决,而且他展示了unordered_set 的核心功能:输入有重复元素的数组,返回无重复元素的数组。代码如下所示。
bool containsDuplicate(std::vector<int>& nums) {
std::unordered_set<int> numSet;
for (int num : nums) {
if (numSet.count(num) > 0) {
return true; // 已存在重复元素
}
numSet.insert(num);
}
return false; // 未找到重复元素
}
但通常题目没有这么简单,一般情况下,unordered_set 会作为解决题目的一个步骤。咱们先看两个简单的题目,体会一下map和set的区别。
两个数组的交集1
求两个数组的交集,但是要求输出结果中的每个元素一定是 唯一的。这时我们就会想到使用unordered_set来解决问题了。首先利用set统计其中一个数组的元素,然后遍历剩余一个数组,使用count函数判断nums2中是否出现nums1中相同的元素,为了保证返回的元素是唯一的,每当检测出一个元素,就使用erase函数消除这个元素。代码如下。
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int>set(nums1.begin(),nums1.end());
vector<int>ans;
for (auto num : nums2) {
if (set.count(num) > 0) {
ans.push_back(num);
set.erase(num);
}
}
return ans;
}
但是需要注意的是,unordered_set中的 count 函数只能返回 0 或 1,并不能检测某个元素出现的次数!咱们看下一题,就可以很快明白了。
两个数组的交集2
这道题目和上一题唯一的区别在于,题目中描述 返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致。这个才是我们直观上的题目描述思路。举个栗子,nums1 = [1,2,2,3],nums2 = [1,2,2]。按照上一个题目的要求,答案应该返回[1,2]。而这个题应该返回[1,2,2]。
这样的话,我们应该统计其中一个数组的数字出现次数,这时候使用unordered_set就不太合适了,改进的办法是使用unordered_map,代码和上面的非常相似,便可以轻松秒杀了!
vector<int> intersection1(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int,int>hashset;
vector<int>ans;
for (auto num : nums1) {
hashset[num]++;
}
for (auto num : nums2) {
if (hashset[num] > 0) {
ans.push_back(num);
hashset[num]--;
}
}
return ans;
}
二、相关习题
2.1 字母异位数相关习题
字母异位词简直是为哈希表量身定制的,一般这类题目都可以用哈希表来解决,让我们看几个例题。
1.有效的字母异位词
判断两个字符串是否为字母异位数,也就是说,这两个字符串的字母组成相同,但是顺序可能不一样,这就要发挥unordered的数据结构的魔力了!先构建一个哈希表,储存第一个字符串的元素,然后遍历第二个字符串,如果第二个字符串的元素包含在哈希表里,那么每遍历一个,哈希表的该元素就会减一;如果第二个字符串的元素不包含在哈希表里,说明第二个字符串中有第一个字符串没有的字母,直接返回false即可。最后,为了避免第二个字符串的元素小于第一个字符串的情况,直接开局判断字符串长度,不相等则返回false。代码如下。
bool isAnagram(string s, string t) {
unordered_map<char, int>map;
if(s.length() != t.length()) return false;
for (auto a : s) map[a]++;
for (auto a : t) {
if (map[a] > 0) map[a]--;
else return false;
}
/*
for (auto it = map.begin();it!=map.end();it++)
{
if (it->second > 0) return false;
}
*/
return true;
}
但是上面的代码还是有些问题,直接使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效。数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,长度为26即可,来记录字符串s里字符出现的次数。代码与上面的类似。
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++) {
// 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
record[s[i] - 'a']++;
}
for (int i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (record[i] != 0) {
return false;
}
}
return true;
}
所以在使用哈希表解决问题的时候,要先考虑能否用数组解决问题,这样可以对空间进行一些优化。
2.字母异位词分组
众所周知,正常情况下,一个哈希表只能找到一个字母异位数,那如果我要找两个呢,要两个哈希表。好吧,那我要找很多个,而且个数位置的情况下,应该如何处理呢?我们可以看出,只使用一个简单的<char,int>哈希表的话,不能判断一个字符串组中的异位数,这个题目就是这样的一个问题。
对于本题,我们想要让好几个字符串都对应一个相同的字符串。例如strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”],我们想要让“eat",“tea”,"ate"都对应一个相同的字符串,他们排序后都对应字符串(aet),反之,如果排序后对应的不是这个字符串,那说明不是一个字母异位词。那么应该如何构造哈希表的对应关系呢,首先我们知道哈希表的键(key)是固定的,而值是可以多个,所以我们就可以构造出下面的哈希表。
unordered_map<string, vector<string>>map;
之后我们只需把每个将要遍历的字符串赋值给key,之后排序,即可找到对应关系,然后把相同字母异位数都对应到相同的映射中。
map[key].emplace_back(str);
这样的话,每个哈希表储存的值vector>就是输出结果的每个元素。完整代码如下,简直是太精妙了!!
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>>ans;
unordered_map<string, vector<string>>map;
for (auto str : strs) {
string key = str;
sort(key.begin(), key.end());
map[key].emplace_back(str);
}
for (auto it = map.begin(); it != map.end(); ++it)
{
ans.emplace_back(it->second);
}
return ans;
}
这是一个滑动窗口与哈希结合的题目,看起来就有些难。自从我做了上面一题,我发现把字符串排序比较是否相同太方便了,结果迅速写出了相关代码,然后就超时了,,,。
这道题的解法思路也非常巧妙。我们可以使用两个数组代表哈希表,分别统计在p的大小区间中,两个字符串出现的字母次数,然后先排除一些特殊情况。接下来到关键时刻了,创建一个p长度的滑动窗口,需要依次遍历s的字串了。这个时候,每次向前移动一步,滑窗就会少一个字母和多一个字母,那么我们是不是只需要把这个少的字母在哈希表中减去,同时加上多的字母的映射,然后判断两者是否相同即可。
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
if (sLen < pLen) {
return vector<int>();
}
vector<int> ans;
vector<int> sCount(26);
vector<int> pCount(26);
for (int i = 0; i < pLen; ++i) {
++sCount[s[i] - 'a'];
++pCount[p[i] - 'a'];
}
if (sCount == pCount) {
ans.emplace_back(0);
}
for (int i = 0; i < sLen - pLen; ++i) {
--sCount[s[i] - 'a'];
++sCount[s[i + pLen] - 'a'];
if (sCount == pCount) {
ans.emplace_back(i + 1);
}
}
return ans;
这道题目也是哈希表和滑窗结合的问题。和上面的题目非常相似,但是更简单。
int lengthOfLongestSubstring(string s) {
unordered_map<char, int>map;
int i =0, j = 0;
int maxlen = 0;
while (j<s.size())
{
if (map[s[j]] == 0) { map[s[j]]++; j++; }
else { map[s[i]]--; i++; }
maxlen = max(maxlen, j - i);
}
return maxlen;
}
2.2 unordered_set相关题目
1.快乐数
这道题目相对来说还是比较简单的,只需要注意一点,当出现 无限循环 但始终变不到 1的情况时候,我们可以创建一个set统计是否出现重复的结果。如果重复了,说明已经是无限循环了,返回false即可。同时我们需要编写一个对数字的每位进行处理求平方和的函数。代码如下:
int getSum(int n) {
int ans = 0;
while (n)
{
int a = n % 10;
ans += a * a;
n /= 10;
}
return ans;
}
bool isHappy(int n) {
unordered_set<int>set;
int ans = 0;
while (1)
{
ans = getSum(n);
if (ans == 1)return true;
else if (set.count(ans) > 0) return false;
else set.insert(ans);
n = ans;
}
}
看到本题的第一眼,我想到的是使用一个有序表set记录数组中的数字,然后就可以遍历有序表,记录每次连续的序列,统计最大的次数,直接就拿下了!但是有序表的查找速度是O(logn),还需要一层遍历,而题目中要求使用O(n)时间解决,看来不太符合要求。那我们想要更快一点,就使用无序表unordered_set!这样的话会出现新的问题了,数组是无序的,无序表也是无序的,而我们要找的连续序列确是有序的!这简直是强人所难!
为了解决这个问题,我们可以先将数组的元素存到无序表中,然后判断有无与当前元素相邻的元素即可。这样的话,还有一个问题,我们并不知道刚开始检测的元素是最大最小还是在中间,所以他的相邻元素有两个,分别是a+1和a-1。这个时候就需要一个巧妙的方法解决问题了。
我们继续遍历nums,对每个元素先进行一个判断
if (!set.count(num - 1))
也就是说,判断这个元素的相邻前一个元素是否在数组中,为什么要这样呢?因为如果这个元素的前一个元素不存在,说明以这个元素为开头的最长序列是最长的!!,如果如果这个元素的前一个元素存在,那么肯定是按照前一个元素为开头的序列更长。这也是解决本题的关键。
当我们判断这个元素可以作为起始元素后,就可以判断他的后一个相邻元素是否存在了。存在一个,加一下长度,最后进行比较,取能达到最长连续序列的长度即可!代码如下:
int longestConsecutive(vector<int>& nums) {
unordered_set<int>set;
for (auto num : nums) set.insert(num);
int curlen = 1, maxlen = 0;
for (auto& num : nums) {
if (set.count(num - 1) == 0) {
int cur = num;
curlen = 1;
while (set.count(cur + 1))
{
cur++;
curlen++;
}
}
maxlen = max(curlen, maxlen);
}
return maxlen;
}