【LeetCode】字符串精选9题

目录

基础题:

1. 最长公共前缀(简单)

滑动窗口:

1. 无重复字符的最长子串(中等)

2. 找到字符串中所有字母异位词(中等)

3. 串联所有单词的子串(困难)

4. 最小覆盖子串(困难)

回文串:

1. 验证回文串(简单)

2. 验证回文串 II(简单)

3. 回文子串(中等)

4. 最长回文子串(中等)


基础题:

1. 最长公共前缀(简单)

方法一:横向扫描

依次遍历字符串数组中的每个字符串,对于每个遍历到的字符串,更新最长公共前缀,当遍历完所有的字符串以后,即可得到字符串数组中的最长公共前缀。

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        string ans = strs[0];
        for (int i = 1; i < strs.size(); i++)
        {
            ans = findCommon(ans, strs[i]);
        }
        return ans;
    }

private:
    string findCommon(string& s1, string& s2)
    {
        int i = 0;
        while (i < min(s1.size(), s2.size()) && s1[i] == s2[i])
        {
            i++;
        }
        return s1.substr(0, i);
    }
};

方法二:纵向扫描

纵向扫描时,从前往后遍历所有字符串的每一列,比较相同列上的字符是否相同,如果相同则继续对下一列进行比较,如果不相同则当前列不再属于公共前缀,当前列之前的部分为最长公共前缀。

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        for (int j = 0; j < strs[0].size(); j++) // j表示列
        {
            char ch = strs[0][j];
            for (int i = 0; i < strs.size(); i++) // i表示行
            {
                if (j == strs[i].size() || strs[i][j] != ch)
                    return strs[0].substr(0, j);
            }
        }
        return strs[0];
    }
};

滑动窗口:

1. 无重复字符的最长子串(中等)

用滑动窗口定位子串,用哈希表记录子串中每个字符出现的次数。

进窗口后判断窗口中有没有重复字符,如果已经有重复字符了,要出窗口,直到没有重复字符。这时就找到了没有重复字符的子串,更新结果。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {        
        int n = s.size();
        int ans = 0;
        int left = 0;
        int right = 0;
        vector<int> hash(256, 0); // 下标表示ASCII码值
        while (right < n)
        {
            // 进窗口
            hash[s[right]]++;
            // 判断窗口中有没有重复字符
            while (hash[s[right]] > 1 && left <= right)
            {
                // 出窗口
                hash[s[left]]--;
                left++;
            }
            // 更新结果
            ans = max(ans, right - left + 1);
            // 更新右边界
            right++;
        }
        return ans;
    }
};

2. 找到字符串中所有字母异位词(中等)

字母异位词:

  • 长度相等
  • 每个字母出现的次数相等(可以用哈希表记录字符串中每个字母出现的次数)

暴力解法

用固定的p.size()大小的窗口定位s的子串,逐个扫描子串中的字母,并把字母在哈希表中对应的值-1,如果子串是p的排列,哈希表中所有的值为0。

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int nS = s.size();
        int nP = p.size();
        if (nS < nP)
            return {};

        vector<int> ans;
        vector<int> hash(26, 0); // hash[0]存储'a'出现的次数,hash[1]存储'b'出现的次数……
        for (auto& ch : p)
        {
            hash[ch - 'a']++;
        }

        int left = 0;
        int right = nP - 1;
        while (right < nS)
        {
            vector<int> tmp(hash);
            for (int i = left; i <= right; i++)
            {
                tmp[s[i] - 'a']--;
            }
            if (areAllZero(tmp))
            {
                ans.push_back(left);
            }
            left++;
            right++;
        }
        return ans;
    }

private:
    bool areAllZero(vector<int>& v)
    {
        for (auto& i : v)
        {
            if (i)
                return false;
        }
        return true;
    }
};

优化

用hash1记录字符串p中每个字母出现的次数,hash2记录字符串s的窗口中每个字母出现的次数。

用count记录窗口中有效字母的个数。窗口中的某一字母,它在窗口中出现的次数不能比它在p中出现的次数多,这就是有效字母。

进窗口的同时要维护count,然后判断窗口的大小是否> p.size(),如果已经> p.size()了,要出窗口并维护count,直到窗口的大小== p.size()。这时只能说明窗口的大小是合法的,不能说明窗口定位的子串一定是p的异位词。所以,接下来要判断有效字母的个数是否== p.size(),如果相等,说明找到了字母异位词,更新结果。

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int nS = s.size();
        int nP = p.size();
        if (nS < nP)
            return {};

        vector<int> ans;
        vector<int> hash1(26, 0); // 记录p中的字母出现的次数
        vector<int> hash2(26, 0); // 记录窗口中的字母出现的次数
        for (auto& ch : p)
        {
            hash1[ch - 'a']++;
        }

        int left = 0;
        int right = 0;
        int count = 0;
        while (right < nS)
        {
            // 进窗口+维护count
            char in = s[right];
            if (++hash2[in - 'a'] <= hash1[in - 'a']) // 判断有效字母
            {
                count++;
            }
            // 判断窗口大小是否>p.size()
            if (right - left + 1 > nP)
            {
                // 出窗口+维护count
                char out = s[left++];
                if (hash2[out - 'a']-- <= hash1[out - 'a']) // 判断有效字母
                {
                    count--;
                }
            }
            // 有效字母的个数==p.size(),说明找到了字母异位词,更新结果
            if (count == nP)
            {
                ans.push_back(left);
            }
            // 更新右边界
            right++;
        }
        return ans;
    }
};

3. 串联所有单词的子串(困难)

和“找到字符串中所有字母异位词”类似。区别在于:

  • 哈希表的key是字符串
  • 指针移动的步长是单词的长度len
  • 滑动窗口执行的次数是单词的长度len,见下图

暴力解法

用固定的words.size() * len大小的窗口定位s的子串,逐个扫描子串中的单词,并把单词在哈希表中对应的值-1,如果子串是words中的单词的排列,哈希表中所有的值为0。

暴力解法会超时,但是代码本身没有问题。

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        int nS = s.size(); // 字符串s的长度
        int nW = words.size(); // 数组words中单词的个数
        int len = words[0].size(); // 一个单词的长度
        if (nS < nW * len)
            return {};

        vector<int> ans;
        unordered_map<string, int> hash; // 记录words中单词出现的次数
        for (auto& s : words)
        {
            hash[s]++;
        }

        // 滑动窗口执行len次
        for (int i = 0; i < len; i++)
        {
            int left = i;
            int right = i + nW * len - 1;
            while (right < nS)
            {
                unordered_map<string, int> tmp(hash);
                for (int i = left; i <= right - len + 1; i += len)
                {
                    tmp[s.substr(i, len)]--;
                }
                if (areAllZero(tmp))
                {
                    ans.push_back(left);
                }
                left += len;
                right += len;
            }
        }
        return ans;
    }

private:
    bool areAllZero(unordered_map<string, int> ump)
    {
        for (auto& e : ump)
        {
            if (e.second)
                return false;
        }
        return true;
    }
};

优化

用hash1记录数组words中每个单词出现的次数,hash2记录字符串s的窗口中每个单词出现的次数。

用count记录窗口中有效单词的个数。窗口中的某一单词,它在窗口中出现的次数不能比它在words中出现的次数多,这就是有效单词。

进窗口的同时要维护count,然后判断窗口的大小是否> words.size() * len,如果已经> words.size() * len了,要出窗口并维护count,直到窗口的大小== words.size() * len。这时只能说明窗口的大小是合法的,不能说明窗口定位的子串一定是串联子串。所以,接下来要判断有效单词的个数是否== words.size(),如果相等,说明找到了串联子串,更新结果。

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        int nS = s.size(); // 字符串s的长度
        int nW = words.size(); // 数组words中单词的个数
        int len = words[0].size(); // 一个单词的长度
        if (nS < nW * len)
            return {};

        vector<int> ans;
        unordered_map<string, int> hash1; // 记录words中单词出现的次数
        for (auto& s : words)
        {
            hash1[s]++;
        }

        // 滑动窗口执行len次
        for (int i = 0; i < len; i++)
        {
            unordered_map<string, int> hash2; // 记录窗口中单词出现的次数
            int left = i;
            int right = i;
            int count = 0;
            while (right + len - 1 < nS)
            {
                // 进窗口+维护count
                string in = s.substr(right, len);
                hash2[in]++;
                if (hash1.count(in) && hash2[in] <= hash1[in]) // 判断有效单词
                {
                    count++;
                }
                // 判断窗口大小是否大于words.size()*len
                if (right + len - left > nW * len)
                {
                    // 出窗口+维护count
                    string out = s.substr(left, len);
                    if (hash1.count(out) && hash2[out] <= hash1[out]) // 判断有效单词
                    {
                        count--;
                    }
                    hash2[out]--;
                    left += len;
                }
                // 有效单词的个数==words.size(),说明找到了串联子串,更新结果
                if (count == nW)
                {
                    ans.push_back(left);
                }
                // 更新右边界
                right += len;
            }
        }
        return ans;
    }
};

4. 最小覆盖子串(困难)

暴力解法

用哈希表记录字符串t每个字符出现的次数。用滑动窗口定位s的子串,逐个扫描子串中的字符,并把字符在哈希表中对应的值-1。

如果哈希表中有值>0,说明子串没包含t的所有字符,则让右边界往右滑动,直到全部包含t的字符(哈希表中最终所有值都<=0)。

如果子串包含t的所有字符,则让左边界往右滑动,然后判断删除子串最左边的字符后是否仍然包含t的所有字符。

class Solution {
public:
    string minWindow(string s, string t) {
        int nS = s.size();
        int nT = t.size();
        if (nS < nT)
            return "";

        vector<int> hash(256, 0); // 下标表示ASCII码值
        for (auto& ch : t)
        {
            hash[ch]++;
        }

        int left = 0;
        int right = 0;
        int min = INT_MAX; // 最小子串长度
        int begin = -1; // 最小子串起始位置
        while (right < nS)
        {
            hash[s[right]]--;
            while (areAllNotMoreThanZero(hash)) // 子串包括t的所有字符
            {
                if (right - left + 1 < min)
                {
                    min = right - left + 1;
                    begin = left;
                }
                hash[s[left]]++;
                left++;
            }
            right++;
        }
        
        if (begin == -1)
            return "";
        else
            return s.substr(begin, min);
    }

private:
    bool areAllNotMoreThanZero(vector<int>& v)
    {
        for (auto& i : v)
        {
            if (i > 0)
                return false;
        }
        return true;
    }
};

优化

用hash1记录字符串t中每个字符出现的次数,hash2记录字符串s的窗口中每个字符出现的次数。

用kind记录t中的字符的种类。用count记录窗口中有效字符的种类。注意,是种类,和之前的题不一样!窗口中的某一字符,只有它在窗口中出现的次数 == 它在t中出现的次数时,才记录。

例如,

t = "ABCCC",s = "ECDBACBCBD"

假设,红色表示的是左边界,蓝色表示的是右边界。现在正在进窗口:

ECDBACBCBD,E在窗口中出现1次,在t中出现0次

ECDBACBCBD,C在窗口中出现1次,在t中出现3次

ECDBACBCBD,D在窗口中出现1次,在t中出现0次。

ECDBACBCBD,B在窗口中出现1次,在t中出现1次,count++

ECDBACBCBD,A在窗口中出现1次,在t中出现1次,count++

ECDBACBCBD,C在窗口中出现2次,在t中出现3次

ECDBACBCBD,B在窗口中出现2次,在t中出现1次

ECDBACBCBD,C在窗口中出现3次,在t中出现3次,count++

此时窗口中有效字符的种类 == 字符串t中字符的种类,显然"ECDBACBC"涵盖了"ABCCC"的所有字符。

进窗口的同时要维护count,然后判断count是否== kind,如果相等,说明找到了覆盖子串,更新结果,然后出窗口并维护count,直到count < kind。

class Solution {
public:
    string minWindow(string s, string t) {
        int nS = s.size();
        int nT = t.size();
        if (nS < nT)
            return "";

        vector<int> hash1(256, 0); // 记录t中的字符出现的次数
        vector<int> hash2(256, 0); // 记录窗口中的字符出现的次数
        int kind = 0; // 记录t中的字符的种类
        for (auto& ch : t)
        {
            if (hash1[ch]++ == 0)
            {
                kind++;
            }
        }

        int left = 0;
        int right = 0;
        int count = 0;
        int min = INT_MAX; // 最小子串长度
        int begin = -1; // 最小子串起始位置
        while (right < nS)
        {
            // 进窗口+维护count
            char in = s[right];
            if (++hash2[in] == hash1[in])
            {
                count++;
            }
            // 判断count是否== kind
            while (count == kind)
            {
                // 找到了覆盖子串,更新结果
                if (right - left + 1 < min)
                {
                    min = right - left + 1;
                    begin = left;
                }
                // 出窗口+维护count
                char out = s[left++];
                if (hash2[out]-- == hash1[out])
                {
                    count--;
                }
            }
            right++;
        }
        
        if (begin == -1)
            return "";
        else
            return s.substr(begin, min);
    }
};

回文串:

1. 验证回文串(简单)

从两端向里逐个比较,跳过非字母数字字符,如果出现了不同的字符,则不是回文串。

class Solution {
public:
    bool isPalindrome(string s) {
        int left = 0; // 首指针
        int right = s.size() - 1; // 尾指针
        while (left < right)
        {
            while (!isalnum(s[left]) && left < right)
            {
                left++;
            }
            while (!isalnum(s[right]) && left < right)
            {
                right--;
            }
            int chLeft = tolower(s[left]);
            int chRight = tolower(s[right]);
            if (chLeft != chRight)
            {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
};

2. 验证回文串 II(简单)

从两端向里逐个比较,如果字符不相同,判断删除这两个字符的其中一个能否形成回文。

class Solution {
public:
    bool validPalindrome(string s) {
        int left = 0; // 首指针
        int right = s.size() - 1; // 尾指针
        while (left < right)
        {
            if (s[left] != s[right])
            {
                break;
            }
            left++;
            right--;
        }
        return left == s.size() / 2 || isPalindrome(s, left, right - 1) || isPalindrome(s, left + 1, right);
    }

private:
    bool isPalindrome(string s, int start, int end)
    {
        while (start < end)
        {
            if (s[start] != s[end])
            {
                return false;
            }
            start++;
            end--;
        }
        return true;
    }
};

3. 回文子串(中等)

枚举所有可能的回文中心,向两边拓展,形成回文子字符串。

回文长度是奇数,回文中心是一个字符;回文长度是偶数,回文中心是两个字符。

假设字符串长度为4,枚举所有可能的回文中心:

"abcd"

 0123

编号i回文中心左起始位置left回文中心右起始位置right
000
101
211
312
422
523
633

可以看出left = i/2,right = i/2 + i % 2,长度为n的字符串,一共有2n - 1个可能的回文中心。

class Solution {
public:
    int countSubstrings(string s) {        
        int n = s.size();
        int ans = 0;
        for (int i = 0; i < 2 * n - 1; i++)
        {
            int left = i / 2;
            int right = i / 2 + i % 2;
            while (left >= 0 && right < n && s[left] == s[right]) 
            {
                left--;
                right++;
                ans++;
            }
        }
        return ans;
    }
};

4. 最长回文子串(中等)

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        int pos = 0; // 最长回文子串的起始位置
        int len = 0; // 最长回文子串的长度
        for (int i = 0; i < 2 * n - 1; i++)
        {
            int left = i / 2;
            int right = i / 2 + i % 2;
            while (left >= 0 && right < n && s[left] == s[right]) 
            {
                left--;
                right++;
            }
            if (right - left - 1 > len)
            {
                pos = left + 1;
                len = right - left - 1;
            }
        }
        return s.substr(pos, len);
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值