剑指offer(专项突破)---字符串

总目录:剑指offer(专项突破)---目录-CSDN博客


1.字符串的基本知识

C语言中: 

函数名功能描述
strcpy(s1, s2)将字符串s2复制到字符串s1中,包括结束符'\0',要求s1有足够空间容纳s2的内容。
strncpy(s1, s2, n)s2中最多n个字符复制到s1中。若s2的长度小于n,则s1中剩余部分用'\0'填充;若s2长度大于等于n,则s1不会以'\0'结尾(需手动添加)。
strcat(s1, s2)将字符串s2连接到字符串s1的末尾,s1要有足够空间容纳连接后的内容,会自动添加结束符'\0'
strncat(s1, s2, n)s2中最多n个字符连接到s1的末尾,然后添加'\0's1要预留足够空间。
strcmp(s1, s2)比较s1s2两个字符串的大小,按照字典序进行比较。若s1小于s2返回负整数;若s1等于s2返回 0;若s1大于s2返回正整数。
strncmp(s1, s2, n)比较s1s2中前n个字符的大小,按照字典序比较,返回值规则同strcmp
strlen(s)计算字符串s的长度,不包括结束符'\0',返回字符串中字符的个数。
strchr(s, c)在字符串s中查找字符c第一次出现的位置,若找到返回指向该字符的指针,若没找到返回nullptr
strrchr(s, c)在字符串s中查找字符c最后一次出现的位置,若找到返回指向该字符的指针,若没找到返回nullptr
strstr(s1, s2)在字符串s1中查找字符串s2第一次出现的位置,若找到返回指向s2s1中起始位置的指针,若没找到返回nullptr

C++中: 

函数名功能描述
size()返回字符串中字符的个数。
length()获取字符串的长度,即字符个数。
empty()判断字符串是否为空,为空返回true,否则返回false
clear()清空字符串内容,使其长度变为 0。
push_back(c)在字符串末尾添加一个字符c
pop_back()删除字符串末尾的一个字符。
compare(s2)比较当前字符串和s2的大小,按照字典序比较,返回值规则类似strcmp函数(小于返回负整数,等于返回 0,大于返回正整数)。
substr(pos, n)从索引pos位置开始提取连续的n个字符,若n省略,则提取从pos开始到末尾的所有字符,返回提取出来的子字符串。
find(s2, pos)从索引pos位置开始查找字符串s2第一次出现的位置,若找到返回位置索引,若没找到返回std::string::npos(一个特殊的表示未找到的值),若pos省略,则从开头查找。
rfind(s2, pos)从索引pos位置开始查找字符串s2最后一次出现的位置,返回值规则同find函数,若pos省略,则从末尾往前查找。
replace(pos, n, s2)将从索引pos开始的n个字符替换成字符串s2,若n省略,则替换从pos开始到末尾的所有字符。

2.双指针

第2章用两个指针来定位一个子数组,其中一个指针指向数组的第1个数字,另一个指针指向数组的最后一个数字,那么两个指针之间所包含的就是一个子数组。

如果将字符串看成一个由字符组成的数组,那么也可以用两个指针来定位一个子字符串,其中一个指针指向字符串的第1个字符,另一个指针指向字符串的最后一个字符,两个指针之间所包含的就是一个子字符串。

LCR 014. 字符串的排列 - 力扣(LeetCode)

题解:滑动窗口

数组模拟哈希表 cnt1 统计字符串 s1 中每个字符出现的次数,然后遍历字符串 s2,维护一个窗口大小为 m 的滑动窗口。

数组模拟哈希表 cnt2 统计窗口内每个字符出现的次数,当 cnt1=cnt2 时,说明窗口内的字符及其个数与字符串 s1 相同,返回 true 即可。

否则,遍历结束后,返回 false

时间复杂度 (m+n×∣Σ∣),空间复杂度 O(∣Σ∣)。其中 m 和 n 分别为字符串 s1 和 s2 的长度;而 ∣Σ∣ 为字符集的大小,本题中∣Σ∣=26。

class Solution 
{
public:
    bool checkInclusion(string s1, string s2) 
    {
        int m = s1.size(), n = s2.size();
        if (m > n)
            return false;

        vector<int> cnt1(26), cnt2(26);
        for (int i = 0; i < m; ++i) 
        {
            ++cnt1[s1[i] - 'a'];
            ++cnt2[s2[i] - 'a'];
        }

        if (cnt1 == cnt2)
            return true;

        for (int i = m; i < n; ++i) 
        {
            ++cnt2[s2[i] - 'a'];
            --cnt2[s2[i - m] - 'a'];
            if (cnt1 == cnt2)
                return true;
        }
        return false;
    }
};

优化:

每次加入和移除一个字符时,都需要比较两个哈希表,时间复杂度较高。我们可以维护一个变量 k,表示两个大小为 m 的字符串中,有多少种字符出现的个数不同。当 k=0 时,说明两个字符串中的字符个数相同。

时间复杂度 O(m+n+∣Σ∣),空间复杂度 O(∣Σ∣)。其中 m 和 n 分别为字符串 s1 和 s2 的长度;而 ∣Σ∣ 为字符集的大小,本题中 ∣Σ∣=26。

class Solution 
{
public:
    bool checkInclusion(string s1, string s2) 
    {
        int m = s1.size(), n = s2.size();
        if (m > n)
            return false;

        vector<int> cnt(26);
        for (int i = 0; i < m; ++i) 
        {
            --cnt[s1[i] - 'a'];
            ++cnt[s2[i] - 'a'];
        }

        int k = 0;
        for (int x : cnt)
            if (x != 0)
                ++ k;
        if (k == 0)
            return true;

        for (int i = m; i < n; ++i) 
        {
            int a = s2[i - m] - 'a';
            int b = s2[i] - 'a';
            if (cnt[a] == 0)
                ++ k;
            -- cnt[a];
            if (cnt[a] == 0)
                -- k;
            
            if (cnt[b] == 0)
                ++ k;
            ++ cnt[b];
            if (cnt[b] == 0)
                -- k;

            if (k == 0)
                return true;

        }
        return false;
    }
};

LCR 015. 找到字符串中所有字母异位词 - 力扣(LeetCode)

题解:滑动窗口

同LCR 014,优化方式也一样,添加一个差异计数器 

class Solution 
{
public:
    vector<int> findAnagrams(string s, string p) 
    {
        int m = s.size();
        int n = p.size();
        vector<int> ans;
        if (m < n)
            return ans;

        vector<int> cnt1(26), cnt2(26);
        for (int i = 0; i < n; ++i) 
        {
            ++cnt1[s[i] - 'a'];
            ++cnt2[p[i] - 'a'];
        }
        if (cnt1 == cnt2)
            ans.push_back(0);

        for (int i = n; i < m; ++i) 
        {
            ++cnt1[s[i] - 'a'];
            --cnt1[s[i - n] - 'a'];
            if (cnt1 == cnt2)
                ans.push_back(i - n + 1);
        }
        return ans;
    }
};

LCR 016. 无重复字符的最长子串 - 力扣(LeetCode)

题解:双指针 + 哈希表

遍历字符串 s,对于当前遍历到的字符 s[r],如果 s[r] 在 [l,r) 范围内有与 s[r] 相同的字符,我们就不断地向右移动指针 l,直到 ss[s[r]] 为 false,此时 [l,r) 中没有任何与 s[r] 相同的字符,我们就找到了以字符 s[r] 为结尾的最长子串。更新最长子串的长度,最终返回答案。

时间复杂度 O(n),空间复杂度 O(∣Σ∣),其中 n 为字符串 s 的长度,而 Σ 表示字符集,本题中字符集为所有 ASCII 码在 [0,128) 内的字符,即∣Σ∣=128。

class Solution 
{
public:
    int lengthOfLongestSubstring(string s) 
    {
        bool ss[128] = {false};
        int ans = 0;
        for (int l = 0, r = 0; r < s.size(); ++ r) 
        {
            while (ss[s[r]])
                ss[s[l++]] = false;

            ss[s[r]] = true;
            ans = max(ans, r - l + 1);
        }
        return ans;
    }
};

LCR 017. 最小覆盖子串 - 力扣(LeetCode)

题解:滑动窗口

  • 外层while循环 - 窗口扩张
    • while(r < s.size()):这个循环的条件是只要r指针还没遍历完整个字符串s,就持续向右移动r指针来扩展窗口,模拟窗口不断向右滑动去尝试包含t中所有字符的过程。
    • char i = s[r++];:每次获取r指针指向的字符,并将r指针后移一位来扩大窗口范围。
    • if(++curHash[i] <= baseHash[i]):将该字符在curHash数组中的出现次数加 1,然后判断加 1 后的次数是否小于等于其在baseHash数组中记录的t里该字符出现的次数。若成立,则将计数器count加 1。
  • 内层while循环 - 窗口收缩
    • while(count == t.size()):当count的值等于字符串t的长度时,意味着当前窗口已经包含了t中的所有字符,此时就进入内层循环来尝试收缩窗口,看能否找到更小的符合条件的窗口。
    • if(r-l < minLen):判断当前窗口的长度(r - l)是否小于已记录的最小窗口长度minLen,如果是,则更新k为当前窗口的起始位置l,更新minLen为当前窗口的长度,即找到了一个更短的包含t所有字符的窗口。
    • char o = s[l++];:从窗口的左边开始收缩,获取要移出窗口的字符,并将l指针后移一位。
    • if(curHash[o]-- <= baseHash[o]):将该字符在curHash数组中的出现次数减 1,然后判断减 1 后的次数是否小于等于其在baseHash数组中记录的t里该字符出现的次数。若成立,则将计数器count减 1。

两个指针 l 和 r 都是从最左端向最右端移动,且 l 的位置一定在r 的左边或重合。注意本题虽然在 while 循环里出现了一个 while 循环,但是因为内循环负责移动 l 指针,且 l 只会从左到右移动一次,因此总时间复杂度仍然是 O(n)。 

class Solution
{
public:
    string minWindow(string s, string t)
    {
        //先统计字符情况
        int baseHash[128] = { 0 }, curHash[128] = { 0 };
        for(auto& ch : t)
        {
            ++baseHash[ch];
        }

        int k = 0, minLen = s.size() + 1;//k:记录窗口起始位置 minLen:记录最小窗口长度
        int l = 0, r = 0, count = 0;

        //外层循环:窗口扩张
        while(r < s.size())
        {
            char i = s[r++];
            if(++curHash[i] <= baseHash[i])
            {
                ++count;
            }

            //内层循环:窗口收缩
            while(count == t.size())
            {
                if(r-l < minLen)
                {
                    k = l;
                    minLen = r-l;
                }

                char o = s[l++];
                if(curHash[o]-- <= baseHash[o])
                {
                    --count;
                }
            }
        }
        return minLen > s.size() ? "" : s.substr(k, minLen);
    }
};

3.回文字符串

LCR 018. 验证回文串 - 力扣(LeetCode)

题解:双指针

时间复杂度 O(n)。空间复杂度 O(1)。 

class Solution 
{
public:
    bool isPalindrome(string s) 
    {
        int l = 0, r = s.size() - 1;
        while (l < r) 
        {
            while (l < r && !isalnum(s[l]))
                ++ l;
            while (l < r && !isalnum(s[r]))
                -- r;

            if (tolower(s[l]) != tolower(s[r]))
                return false;

            ++ l;
            -- r;
        }
        return true;
    }
};

LCR 019. 验证回文串 II - 力扣(LeetCode)

题解:双指针 + 递归

时间复杂度 O(n)。空间复杂度 O(1)。 

class Solution 
{
public:
    bool validPalindrome(string s) 
    {
        auto check = [&](int i, int j) 
        {
            for (; i < j; ++i, --j)
                if (s[i] != s[j])
                    return false;
            
            return true;
        };
        
        for (int i = 0, j = s.size() - 1; i < j; ++i, --j) 
            if (s[i] != s[j])
                return check(i + 1, j) || check(i, j - 1);

        return true;
    }
};

LCR 020. 回文子串 - 力扣(LeetCode)

题解1:从中心向两侧扩展回文串

外层循环:遍历字符串每一位

内层循环:分别计算当前字符为中心点当前字符与下一位字符为中心点 

时间复杂度 O(n^2)。

class Solution 
{
public:
    int countSubstrings(string s) 
    {
        int ans = 0;
        auto f = [&](int i, int j) -> int 
        {
            int cnt = 0;
            for (; i >= 0 && j < s.size() && s[i] == s[j]; -- i, ++ j)
                ++cnt;

            return cnt;
        };

        for (int i = 0; i < s.size(); ++ i)
            ans += f(i, i) + f(i, i + 1);

        return ans;
    }
};

题解2:Manacher 算法 

在 Manacher 算法的计算过程中,用 p[i]−1 表示以第 i 位为中心的最大回文长度,以第 i 位为中心的回文串数量为 \left \lceil \frac{p[i] - 1}{2} \right \rceil

时间复杂度 O(n),空间复杂度 O(n)。

class Solution 
{
public:
    int countSubstrings(string s) 
    {
        int n = s.size();
        string t = "!#";
        for (const char &c: s) 
        {
            t += c;
            t += '#';
        }
        n = t.size();
        t += '$';

        auto f = vector <int> (n);
        int mid = 0, rMax = 0, ans = 0;
        for (int i = 1; i < n; ++i) 
        {
            // 初始化 f[i]
            f[i] = (i <= rMax) ? min(rMax - i + 1, f[2 * mid - i]) : 1;
            // 中心拓展
            while (t[i + f[i]] == t[i - f[i]]) 
                ++f[i];
            if (i + f[i] - 1 > rMax) 
            {
                mid = i;
                rMax = i + f[i] - 1;
            }
            // 当前贡献为 (f[i] - 1) / 2 上取整
            ans += (f[i] / 2);
        }

        return ans;
    }
};

4.小结

变位词和回文是很有意思的文字游戏。如果两个字符串包含的字符及每个字符出现的次数都相同,只是字符出现的顺序不同,那么它们就是一组变位词。通常可以用一个哈希表来统计每个字符出现的次数,有了哈希表就很容易判断两个字符串是不是一组变位词。

回文是一类特殊的字符串。不管是从前往后还是从后往前读取其每一个字符,得到的内容都是一样的。通常可以用两个指针来判断一个字符串是不是回文,要么两个指针从字符串的两端开始向中间移动,要么两个指针从中间开始向两端移动。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

禊月初三

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值