必须要了解的编程基础--哈希与字符串

哈希粗略理解就是实现key到value的映射。显然,这里面有很多种实现方式, 比如哈希函数+链表、hash map等。

1. 简单题目

1.1 LeetCode 409. 最长回文串1

给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。
在构造过程中,请注意区分大小写。比如 “Aa” 不能当做一个回文字符串。
注意:
假设字符串的长度不会超过 1010。

示例 1:

输入:
"abccccdd"

输出:
7

解释:
我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。

分析:

  1. 本题属于构造回文字符串。
  2. 回文字符串和字符数目的奇偶性有关。若全是偶数,则字符总数就是最长回文串的长度。若是有字符的数量为奇数,则可以考虑回文串中心位为奇数个数的字符,而其余位为偶数数目的字符(奇数也可以通过减1变成偶数个数)。
  3. 因此,需要对字符的个数进行计数,这便利用到了字符哈希的知识。

算法思路:

  1. 字符哈希的方法统计所有字符的数量;
  2. 设最长回文串偶数字符长度为max_length = 0;
  3. 设是否有中心字符标记flag = 0;
  4. 遍历每一个字符,字符数为count,若count为偶数,则max_length += count; 若count为奇数,max_length += count - 1; flag = 1;
  5. 最终返回最长回文子串长度 max_length + flag;
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.2 LeetCode 290. 单词规律2

枚举例子说明所有不匹配可能情况:

(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 ?”

  1. ?未出现过,*出现过; (1)、(2)
  2. ? 出现过,但是*和之前的不一致
  3. pattern字符数和str的单词数量不一致

其中,*是否出现过,通过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;
    }
};

简单题目小节

  1. 对于1.1和1.2中的规律的归纳,需要静下心来,通过枚举不同的示例来归纳,这个阶段思维要发散一些。
  2. 对于word是存在哈希表中,除了可以通过unordered_map的 word_map.count(word)==1之外,还可以使用map的 word_map.find(word) != word_map.end()
  3. 个人觉得1.2不太像简单级的。

2. 中等题目

2.1 LeetCode 49. 字母异位词分组3

给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。

示例:

输入: ["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;
    }
};

2.2 LeetCode 3.无重复字符的最长子串4

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 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 0left,根据定理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 2left,根据定理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;
    }
};

2.4 中等题目小节

  1. 除了2.2中有滑动窗口这个知识点之外,其余探讨的都是如何构建哈希表的问题;
  2. 2.1中考察的是如何将多个异位单词映射到同一个哈希值,采用了两种方式:将排序后的单词当做key 和 将单词映射到以字母表为基的向量空间。
  3. 2.2中考察的是如何将不重复的子串映射到哈希表中,然后通过哈希表中值的规律来区别不同不重复子串。建立哈希表的方法也有两种:对窗口内建立每个字符到其出现的数量的哈希映射 和 在窗口内建立每个字符及其出现位置的哈希映射;

  1. https://leetcode-cn.com/problems/longest-palindrome/ ↩︎

  2. https://leetcode-cn.com/problems/word-pattern/ ↩︎

  3. https://leetcode-cn.com/problems/group-anagrams/ ↩︎

  4. https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/ ↩︎

你可能感兴趣的:(编程基础,字符串,哈希表,leetcode)