LCR032-LCR035哈希表的应用

剑指offer(专项突破版)5.3 哈希表的应用

LCR032.有效的字母异位词

分析

遍历第一个字符串,使用数组作为哈希表,字母出现则+1;然后遍历第二个字符串,字母出现则-1;最后判断数组中的值是否全为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
class Solution {
public:
bool isAnagram(string s, string t) {
// 两个字符串长度不相等直接返回false
int m = s.length();
int n = t.length();
if(m != n || s == t){
return false;
}
// 哈希表存储
vector<int> hash(26);
// 遍历第一个字符串
for(int i = 0; i < m; i++){
hash[s[i]-97]++;
}
// 遍历第二个字符串
for(int i = 0; i < n; i++){
hash[t[i]-97]--;
}
// 检查
for(int i = 0; i < 26; i++){
if(hash[i]){
return false;
}
}
return true;
}
};
  • 时间复杂度:O(m+n)
  • 空间复杂度:O(1)

LCR032结果

进阶

如果输入字符串包含unicode字符怎么办?

一个ASCII码字符的长度为8位,所以ASCII码字符集只能包含256个不同的字符,中文及很多语言的字符集都远远超过这个数字。为了包含更多的字符,需要其他编码的字符集,目前使用最多的是Unicode编码。一个Unicode的字符的长度为16位,这样就能表示65536个字符。

使用真正的哈希表unordered_map<char,int>记录。

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
class Solution {
public:
bool isAnagram(string s, string t) {
// 两个字符串长度不相等直接返回false
int m = s.length();
int n = t.length();
if(m != n || s == t){
return false;
}
// 哈希表存储
unordered_map<char,int> hash;
// 遍历第一个字符串
for(int i = 0; i < m; i++){
hash[s[i]]++;
}
// 遍历第二个字符串
for(int i = 0; i < n; i++){
hash[t[i]]--;
}
// 检查
unordered_map<char, int>::iterator iter;
for(iter = hash.begin(); iter != hash.end(); iter++){
if(iter->second){
return false;
}
}
return true;
}
};

LCR033.字母异位词分组

方法1:将单词的字母排序

把一组变位词映射到同一个单词。由于互为变位词的单词的字母出现的次数分别相同,因此如果把单词中的字母排序就会得到相同的字符串。例如,把”eat”、”tea”和”ate”的字母按照字母表顺序排序都得到字符串”aet”。

因此,可以定义一个哈希表,哈希表的键是把单词字母排序得到的字符串,而值为一组变位词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
// 定义结果
vector<vector<string>> res;
// 哈希表存储排序后的字符串和字符串列表
unordered_map<string, vector<string>> hash;
// 遍历
for(int i = 0; i < strs.size(); i++){
string key = strs[i];
sort(key.begin(), key.end());
hash[key].push_back(strs[i]);
}
// 最终结果
unordered_map<string, vector<string>>::iterator iter;
for(iter = hash.begin(); iter != hash.end(); iter++){
res.push_back(iter->second);
}
return res;
}
};
  • 如果每个单词平均有m个字母,排序一个单词需要O(mlogm)的时间。假设总共有n个单词,该算法总的时间复杂度是O(nmlogm)。
  • 使用了一个哈希表来存储排序后的字符串和对应的字符串列表,哈希表的空间复杂度为 O(n)。最终返回的结果 res 也需要 O(n) 的空间来存储结果。因此,总的空间复杂度为 O(n)。

LCR033排序方法结果

方法2:计数

由于互为字母异位词的两个字符串包含的字母相同,因此两个字符串中的相同字母出现的次数一定是相同的,故可以将每个字母出现的次数使用字符串表示,作为哈希表的键。

由于字符串只包含小写字母,因此对于每个字符串,可以使用长度为 262626 的数组记录每个字母出现的次数。需要注意的是,在使用数组作为哈希表的键时,不同语言的支持程度不同,因此不同语言的实现方式也不同。

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
class Solution {
public:
// 定义一个长为26的字符串来记录每个字符串每个字母出现的次数
string getString(string s){
string str(26,'0');
for(char c:s){
str[c-'a']++;
}
return str;
}
vector<vector<string>> groupAnagrams(vector<string>& strs) {
// 定义结果
vector<vector<string>> res;
// 哈希表存储排序后的字符串和字符串列表
unordered_map<string, vector<string>> hash;
// 遍历
for(int i = 0; i < strs.size(); i++){
string key = getString(strs[i]);
hash[key].push_back(strs[i]);
}
// 最终结果
unordered_map<string, vector<string>>::iterator iter;
for(iter = hash.begin(); iter != hash.end(); iter++){
res.push_back(iter->second);
}
return res;
}
};
  • 时间复杂度对于每个字符串,生成字符串表示需要遍历字符串一次,时间复杂度为 O(m)。因此,在遍历输入字符串数组时,对每个字符串执行这样的操作,所以总的时间复杂度是 O(mn),其中 n 是字符串数组的长度,m是字符串的平均长度。
  • 空间复杂度:和第一种方法相同,O(n)。

LCR033计数方法结果

LCR034.验证外星语词典

分析

直接的想法是把字母顺序存到哈希表里,键为字母,值为其顺序。然后检查每一个字符串,是否满足这个顺序。怎么检查,逐个字母判断。

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
class Solution {
private:
int hash[26]; // 哈希表

public:
bool isSorted(string s, string t){
int m = s.length();
int n = t.length();
int i = 0;
while(i < min(m,n) && s[i] == t[i]){
i++;
}
if(i == m){
return true;
}
if(i == n){
return false;
}
if(hash[s[i]-'a'] <= hash[t[i]-'a']){
return true;
}
else{
return false;
}
}
bool isAlienSorted(vector<string>& words, string order) {
// 处理字母顺序
for(int i = 0; i < 26; i++){
hash[order[i]-'a'] = i;
}
// 遍历字符串列表
for(int i = 0; i < words.size()-1; i++){
bool res = isSorted(words[i], words[i+1]);
if(!res){
return false;
}
}
return true;
}
};
  • 时间复杂度:需要遍历words中的每一个word,假设有n个,每个word平均长度为m,即O(mn)。
  • 空间复杂度:O(1)。

LCR034结果

LCR035.最小时间差

分析

一天有24小时,即1440分钟。如果用一个长度为1440的数组表示一天的时间,那么数组下标为0的位置对应时间00:00,下标为1的位置对应时间00:01,以此类推,下标为1439的位置对应23:59。数组中的每个元素是true或false的标识,表示对应的时间是否存在于输入的时间数组中。

有了这个辅助数组,就只需要从头到尾扫描一遍,相邻的两个为true的值表示对应的两个时间在输入时间数组中是相邻的。例如,输入时间数组[“23:50”,”23:59”,”00:00”],数组中只有下标为0、1430和1439这3个位置的值为true,其他位置的值都是false。

由于数组的下标对应的是时间,因此两个时间之间的时间差就是它们在数组中对应的下标之差。23:50和23:59之间相隔9分钟,它们在数组中的下标之差也是9。

其实,这个数组模拟了一个键为时间、值为true或false的哈希表。可以用数组模拟哈希表的原因是一天的分钟数是已知的,而且数组的长度为1440,也不算太长。有了这个数组,就可以和用哈希表一样,在O(1)的时间知道每个时间是否出现在输入的时间数组中。

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
class Solution {
public:
int getMinutes(string s){
return ((s[0] - '0') * 10 + (s[1] - '0')) * 60 + (s[3] - '0') * 10 + (s[4] - '0');
}

int findMinDifference(vector<string>& timePoints) {
// 辅助数组
bool time[1440] = {false};

// 记录
for(int i = 0; i < timePoints.size(); i++){
int index = getMinutes(timePoints[i]);
if(time[index]){
return 0;
}
time[index] = true;
}

// 遍历
int res = INT_MAX; // 使用INT_MAX作为初始值
int pre = -1; // 初始化前一个时间点
int firstTime = -1; // 初始化第一个为真的时间点
for(int i = 0; i < 1440; i++){
if(time[i]){
if(pre != -1){
res = min(res, i - pre);
}
if(firstTime == -1) firstTime = i; // 记录第一个为真的时间点
pre = i;
}
}

// 处理首尾时间点之间的差值
res = min(res, 1440 - pre + firstTime); // 更新结果

return res;
}
};
  • 时间复杂度:在记录时间点的循环中,我们遍历了输入数组 timePoints,其时间复杂度为 O(n),其中 n 是时间点的数量。在遍历辅助数组 time 来计算最小时间差时,我们遍历了一次长度为 1440 的数组,其时间复杂度为 O(1440) = O(1)。因此,总的时间复杂度为 O(n)。
  • 空间复杂度:使用了一个大小为 1440 的辅助数组 time 来记录时间点的出现情况,因此空间复杂度为 O(1)。

LCR035结果

总结

本章介绍了哈希表。哈希表的时间效率很高,添加、删除和查找操作的时间复杂度都是O(1)。

为了设计一个哈希表,首先需要一个数组,把每个键的哈希值映射到数组的一个位置。为了解决冲突,可以把映射到同一位置的多个键用链表存储。同时,为了避免链表太长,当哈希表中元素的数目与数组的长度的比值超过一定的阈值时,则增加数组的长度并根据新的长度重新映射每个键的位置。

如果结合哈希表和其他数据结构的特点,则还可以设计出很多更加高级、更加复杂的数据结构,如最近最少使用缓存。

在解决算法面试题时,哈希表是经常被使用的工具,用来记录字符串中字母出现的次数、字符串中字符出现的位置等信息。

如果哈希表的键的数目是固定的,并且数目不太大,那么也可以用数组来模拟哈希表,数组的下标对应哈希表的键,而数组的值与哈希表的值对应。


LCR032-LCR035哈希表的应用
http://example.com/2024/04/26/posts/LCR032/
作者
Xuan Yang
发布于
2024年4月26日
许可协议