哈希粗略理解就是实现key到value的映射。显然,这里面有很多种实现方式, 比如哈希函数+链表、hash map等。
给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。
在构造过程中,请注意区分大小写。比如 “Aa” 不能当做一个回文字符串。
注意:
假设字符串的长度不会超过 1010。
示例 1:
输入:
"abccccdd"
输出:
7
解释:
我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
分析:
算法思路:
class Solution {
public:
int longestPalindrome(string s) {
int flag = 0;
int max_length = 0;
map<char, int> hash;
// 字符哈希计数
for (auto&c:s) hash[c]++;
// 统计回文长度
for (auto&e:hash) {
if (e.second % 2 == 0) {
max_length += e.second;
}
else {
max_length += e.second - 1;
flag = 1;
}
}
return max_length + flag;
}
};
不使用map容器而是使用ASCII码表(0-127)来进行字符哈希。这种方法的运行速度更快一些。
class Solution {
public:
int longestPalindrome(string s) {
int flag = 0;
int max_length = 0;
int char_map[128] = {
0};
// 字符哈希计数
for (auto&c:s) {
char_map[c]++;
}
// 统计回文长度
for (int i=0; i<128; i++) {
if ( char_map[i]% 2 == 0) {
max_length += char_map[i];
}
else {
max_length += char_map[i] - 1;
flag = 1;
}
}
return max_length + flag;
}
};
枚举例子说明所有不匹配可能情况:
(1) “abba”, “dog cat cat fish”
(2) “aaaa”, “dog cat cat dog”
(3) “abba”, “dog dog dog dog”
(3) “abba”, “dog”
总结不匹配的情况:
“abb*”, “dog cat cat ?”
其中,*是否出现过,通过used[128]数组来标记,就像是在图的深搜里标记节点有无被访问过那样。
class Solution {
public:
bool wordPattern(string pattern, string str) {
unordered_map<string, char> hash_map;
int used[128] = {
0};
string word = "";
// 使得对str可以根据空格来拆分单词;
str.push_back(' ');
int pos = 0;
for (int i=0; i<str.size(); i++) {
// 遇到空格拆分单词
if (str[i] == ' ') {
// 如果word没有出现在哈希表中
if (hash_map.count(word) == 0) {
// word未出现过,但是pattern对应字符出现过
if (used[pattern[pos]]) return false;
hash_map[word] = pattern[pos];
used[pattern[pos]] = 1;
}
else {
// word出现过,但是pattern对应字符不一致
if (hash_map[word] != pattern[pos]) return false;
}
pos++;
word = "";
}
else {
word += str[i];
}
}
// 两者数量不匹配
if (pos != pattern.size()) return false;
return true;
}
};
给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
示例:
输入: ["eat", "tea", "tan", "ate", "nat", "bat"]
输出:
[
["ate","eat","tea"],
["nat","tan"],
["bat"]
]
方法1:以排序后的单词作为key,设计哈希表
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> anagram; // 也可以是map>
vector<vector<string>> ans;
for (auto&word: strs) {
string str = word;
sort(str.begin(), str.end());
anagram[str].push_back(word);
}
for (auto&e: anagram) {
ans.push_back(e.second);
}
return ans;
}
};
方法2:以单词的字母表向量作为key,设计哈希表
class Solution {
public:
void to_vector(vector<int>& vec, string& str) {
for (int i=0; i<26; i++) {
vec.push_back(0);
}
for (int i=0; i<str.size(); i++) {
vec[str[i]-'a']++;
}
}
vector<vector<string>> groupAnagrams(vector<string>& strs) {
map<vector<int>, vector<string>> anagram;
vector<vector<string>> ans;
for (auto&word: strs) {
vector<int> vec;
to_vector(vec, word);
anagram[vec].push_back(word);
}
for (auto&e: anagram) {
ans.push_back(e.second);
}
return ans;
}
};
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
方法1:滑动窗口+哈希表(哈希表记录窗口字符数量)
关键是如何维护窗口内字符无重复:
(1) 窗口移动原理:窗口右端不停向右移动,直到窗口内有重复字符出现才停止(假停止,其实一直在向右移动),这个时候移动窗口左端,直到窗口内恰好无重复字符才停止。然后继续移动窗口右端,重复上述步骤。其中,第i字符是否在窗口内重复,是通过哈希表 char_map[i] 是否等于1来验证。
(2) 构建ASCII字符哈希表 char_map[128] = {0},来记录窗口内字符有哪些,以及重复情况:0表示没有出现,1表示出现一次,2及2以上表示重复。长度最长的窗口就是要求的结果。
(3) 关键是窗口左端left的更新方式:当char_map[i]>1时,说明此时第i个字符在窗口内试重复的,不停地收缩窗口左端:char_map[s[left]]–;left++;直到char_map[i] == 1;不重复子串的长度,通过字符串word来记录。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// left是滑动窗口左端,i是滑动窗口右端
int left = 0;
string word = "";
int char_map[128] = {
0};
int ans = 0;
for (int i=0; i<s.size(); i++) {
char_map[s[i]]++;
if (char_map[s[i]] == 1) {
word += s[i];
if (ans < word.size()) {
ans = word.size();
}
}
else {
// 将重复的字符从哈希表中删去
while (char_map[s[i]]>1) {
char_map[s[left]]--;
left++;
}
word = "";
for (int j=left; j<=i; j++) {
word += s[j];
}
}
}
return ans;
}
};
方法2:滑动窗口+哈希表(记录字符的位置)
滑动窗口的运行方式和方法1的相同,不同的是窗口内无重复内容的维护方式。
如何维护窗口内无重复字符:
(1) 与方法1不同的是,方法2的哈希表记录的不是窗口内字符出现的数量,而是字符出现的位置。
(2) 窗口移动的原理都是一样的,关键是窗口左端left的更新方式,通过举例思考窗口左端的更新方式。
(3) 窗口初始化,left = 0, i = 0。哈希表 char_map 记录窗口内字符出现的位置。很容易想到,哈希表中的值char_map[s[i]]在窗口的左右端内,且互不相等。(定理1)
完全没有重复的字符的字符串abcd,其哈希表中记录的是每个字符对应的位置。left=0; 右端i=3; a的哈希值是0;c的哈希值是2;无重复子串长度为(i+1)-left=4;
结尾再加一个a变成abcda,left=0,右端i=4,没有更新a的哈希值前,a的哈希值是 0 ≥ l e f t 0\geq left 0≥left,根据定理1,可推出a在窗口内出现过,left要右移一位,left=1;更新a的哈希值为i,即4;无重复子串长度为(i+1)-left=4;
结尾再加一个a变成abcdac,left=1,右端i=5, 没有更新c的哈希值前,c的哈希值是 2 ≥ l e f t 2\geq left 2≥left,根据定理1,可推出c在窗口内出现过,left要移动第一个c的后面一位,left=3;更新c的哈希值为i,即5;无重复子串长度为(i+1)-left=3;
(4) 通过(3)总结left的更新方式:当窗口右端的元素s[i]的哈希值(位置)char_map[s[i]]在更新之前满足定理1,也就是说s[i]在窗口内出现过。因为窗口内右端的元素永远是最大的,所以弱化定理1,为非窗口右端元素的哈希表值char_map[s[i]]一定大于等于left,且互不相等。(定理1)
此外,每当遇到一个重复元素,说明一个窗口已经结束,正要开启一个新窗口,所以,在更新新窗口左端数据之前,计算旧窗口的长度,并更新结果值res;
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int res = 0, left = 0, i = 0;
vector<int> char_map(128, -1);
for (;i < s.size(); i++) {
// s[i]在当前窗口出现过,即s[i]重复
// 更新窗口左端res和left
if(char_map[s[i]] >= left) {
res = max(res, i - left);
left = char_map[s[i]] + 1;
}
// 窗口右端一直向右移动
char_map[s[i]] = i;
}
// 最后一个窗口的长度更新到res中
// 因为i是全局变量,所以这里i等于s.size();
res = max(res, i - left);
return res;
}
};
或者通过while循环来替代for循环。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if (s.size() == 0) return 0;
int i = 0, left = 0;
vector<int> char_map(128, -1);
int res = 1;
while (i < s.size()) {
// s[i]在当前窗口出现过,即s[i]重复
// 更新窗口左端res和left
if (char_map[s[i]] >= left) {
res = max(res, i - left);
left = char_map[s[i]] + 1;
}
// 窗口右端一直向右移动
char_map[s[i]] = i;
i++;
}
// 最后一个窗口的长度更新到res中
// 因为i是全局变量,所以这里i等于s.size();
res = max(res, i - left);
return res;
}
};
或者使用unordered_set 建立哈希表
滑动窗口的运行方式相同,不同的是对窗口内元素的维护采用的是unordered_set:当 char_map.find(s[i]) != char_map.end()时,说明哈希表char_map里存在重复元素。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size() == 0) return 0;
unordered_set<char> char_map;
int res = 0;
int left = 0;
int i = 0;
for(; i < s.size(); i++) {
// 窗口内含有重复元素
// 更新窗口左端res和left
while (char_map.find(s[i]) != char_map.end()){
res = max(res,i-left);
char_map.erase(s[left]);
left ++;
}
// 窗口右端一直向右移动
char_map.insert(s[i]);
}
// 最后一个窗口的长度更新到res中
// 因为i是全局变量,所以这里i等于s.size();
res = max(res,i-left);
return res;
}
};
https://leetcode-cn.com/problems/longest-palindrome/ ↩︎
https://leetcode-cn.com/problems/word-pattern/ ↩︎
https://leetcode-cn.com/problems/group-anagrams/ ↩︎
https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/ ↩︎