数据结构-字符串篇

字符串理论基础总结:

什么是字符串,字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,我们以C/C++中的字符串举例。

在C语言中,把一个字符串存入一个数组时,也把结束符'\0'存入数组,并以此作为该字符是否结束的标志。

char a[5] = "asd";
for (int i = 0; a[i] != '\0'; i++) {
}

在C++中,提供一个 string 类,string 类会提供size接口,可以用来判断 string 类字符串是否结束,就不用'\0'来判断是否结束。

string a = "asd";
for (int i = 0; i < a.size(); i++) {
}

那么 vector<char> 和 string 又有什么区别呢?

其实在基本操作上没有区别,但是 string 提供更多的字符串处理的相关接口,例如 string 重载了 '+',而 vector 却没有。

所以想处理字符串,我们还是会定义一个 string 类型。

字符串的经典题目:

/*
    思路分析:双指针法
*/
class Solution {
public:
    void reverseString(vector<char>& s) {
        int left = 0;
        int right = s.size() - 1;
        while(left <= right) {
            swap(s[left],s[right]);
            left++;
            right--;
        }
    }
};

/*
    思路分析:当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章
            一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。
            其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
*/
class Solution {
public:
    string reverseStr(string s, int k) {
        for (int i = 0; i < s.size(); i += (2 * k)) {
            // 1. 每隔 2k 个字符的前 k 个字符进行反转
            // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
            if (i + k <= s.size()) {
                reverse(s.begin() + i, s.begin() + i + k );
            } else {
                // 3. 剩余字符少于 k 个,则将剩余字符全部反转。
                reverse(s.begin() + i, s.end());
            }
        }
        return s;
    }
};

/*
    思路分析:解决方案有很多,但要想做到极致,我们就不使用额外的辅助空间了。
             1.首先将数组扩充到每个数字字符替换成 'number' 之后的大小
             2.双指针法,从后向前替换字符
*/
int main() {
    std::string str;
    while(std::cin >> str) {
        int oldIndex = str.size() - 1;
        int numberCount = 0;
        for(char c : str) {
            if(c >= '0' && c <= '9') {
                numberCount++;
            }
        }
        str.resize(str.size() + numberCount * 5);
        int newIndex = str.size() - 1;
        while(oldIndex >= 0) {
            if(str[oldIndex] >= '0' && str[oldIndex] <= '9') {
                str[newIndex--] = 'r';
                str[newIndex--] = 'e';
                str[newIndex--] = 'b';
                str[newIndex--] = 'm';
                str[newIndex--] = 'u';
                str[newIndex--] = 'n';
            } else {
                str[newIndex--] = str[oldIndex];
            }
            oldIndex--;
        }
        
        std::cout << str << std::endl;
    } 
    
    return 0;
}

/*
    思路分析:1.整体反转字符串
             2.逐个反转每个单词
             3.移除多余空格
*/
class Solution {
public:
    string reverseWords(string s) {
        // 1.整体反转字符串
        reverse(s.begin(), s.end());

        // 2.逐个反转每个单词
        int start = 0, end = 0;
        int n = s.size();
        while(start < n) {
            // 找到单词的起点
            while(start < n && s[start] == ' ') start++;
            // 遍历结束
            if(start >= n) break;

            end = start;

            // 找到单词的终点
            while(end < n && s[end] != ' ') end++;

            // 反转单词
            reverse(s.begin() + start, s.begin() + end);
            
            start = end;
        }

        // 3.移除多余空格
        int fastIndex = 0, lowIndex = 0;
        for(; fastIndex < n; ++fastIndex) {
            if(s[fastIndex] != ' ') {
                // 单词之间添加空格
                if(lowIndex != 0) s[lowIndex++] = ' ';
                while(fastIndex < n && s[fastIndex] != ' ') {
                    s[lowIndex++] = s[fastIndex++];
                }
            }
        }
        // 调整字符串大小,移除末尾多余空格
        s.resize(lowIndex); 

        return  s; 
    }
};

/*
    思路分析:将字符串分为两个部分,先整体倒序,再局部倒序,负负得正。
*/
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
    int n;
    string s;
    cin >> n;
    cin >> s;
    int len = s.size(); //获取长度

    reverse(s.begin(), s.end()); // 整体反转
    reverse(s.begin(), s.begin() + n); // 先反转前一段,长度n
    reverse(s.begin() + n, s.end()); // 再反转后一段

    cout << s << endl;

} 

// 补充评论区答案中的巧妙思路:回文串思想 + string 内置函数 substr()
#include <iostream>
using namespace std;
int main()
{
    int n;
    string s;
    cin >> n >> s;
    int len = s.size();
    cout << (s + s).substr(len - n, len) << '\n';
    return 0;
}

/* 
    思路分析:1.暴力解法
             2.KMP算法:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
*/

// 暴力解法 -- 时间复杂度O(m * n)
class Solution {
public:
    int strStr(string haystack, string needle) {
        int lenHay = haystack.size();
        int lenNeed = needle.size();

        // 处理特殊情况
        if (needle.empty()) return 0;
        if (lenNeed > lenHay) return -1;

        for (int posHay = 0; posHay <= lenHay - lenNeed; ++posHay) {
            int posNeed = 0;
            while (posNeed < lenNeed && haystack[posHay + posNeed] == needle[posNeed]) {
                ++posNeed;
            }
            if (posNeed == lenNeed) {
                return posHay;
            }
        }
        return -1;
    }
};

// KMP算法 -- 时间复杂度O(m + n)
class Solution {
public:
    vector<int> getNext(const string& str) {
        vector<int> next(str.size());
        int j = 0;
        next[0] = 0;
        
        for (int i = 1; i < str.size(); ++i) {  // i 从 1 开始
            while (j > 0 && str[i] != str[j]) {
                j = next[j - 1];
            }
            if (str[i] == str[j]) {
                ++j;
            }
            next[i] = j;
        }
        return next;
    }

    int strStr(string haystack, string needle) {
        int lenHay = haystack.size();
        int lenNeed = needle.size();

        // 处理特殊情况
        if (needle.empty()) return 0;
        if (lenNeed > lenHay) return -1;

        vector<int> next = getNext(needle);
        for (int i = 0, j = 0; i < haystack.size(); ++i) {
            while (j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }
            if (haystack[i] == needle[j]) {
                ++j;
            }
            if (j == lenNeed) return i - j + 1;
        }

        return -1;
    }
};

/*
    思路分析:1.暴力解法:一个for循环获取 子串的终止位置, 然后判断子串是否能重复构成字符串,又嵌套一个for循环,所以是O(n^2)的时间复杂度。
             2.移动匹配:用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s。
             3.KMP算法:与实现strStr()不同的是,这道题的核心在于找到最小重复串,数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
*/
class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        int len = s.size();

        vector<int> next(s.size());
        next[0] = 0;
        for(int i = 1, k = 0; i < len; ++i) {
            while(k > 0 && s[i] != s[k]) {
                k = next[k - 1];
            }
            if(s[i] == s[k]) {
                k++;
            }
            next[i] = k;
        }

        int longest_prefix = next[len - 1];
        if(longest_prefix > 0 && len % (len - longest_prefix) == 0) 
            return true;

        return false;
    }
};

总结 

字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。

双指针法是字符串处理的常客。

KMP算法是字符串查找最重要的算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值