C++ 字符串
- C风格字符串声明:
用char
数组存储,用空字符\0
表示字符串的结尾
char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
等同于
char site[] = "RUNOOB";
- C++ String类
#include <string>
string str = "runoob";
- 和许多 STL 容器相同,
string
能动态分配空间,这使得我们可以直接使用std::cin
来输入; - string 重载了加法运算符,可以直接拼接两个字符串或一个字符串和一个字符;
- string 重载了比较运算符,同样是按字典序比较的,所以我们可以直接调用
std::sort
对若干字符串进行排序。
1. 转char数组
str.data()
str .c_str()
保证末尾有空字符
2. 获取长度
printf("s 的长度为 %lu", s.size());
printf("s 的长度为 %lu", s.length());
printf("s 的长度为 %lu", strlen(s.c_str()));
这三个函数(以及下面将要提到的
find
函数)的返回值类型都是size_t
(unsigned long)。因此,这些返回值不支持直接与负数比较或运算,建议在需要时进行强制转换。
3. 寻找第一次出现位置
find(str,pos)
函数可以用来查找字符串中一个字符/字符串在 pos
(含)之后第一次出现的位置(若不传参给 pos 则默认为 0)。
如果没有出现,则返回 string::npos(被定义为 -1,但类型仍为 size_t/unsigned long)。
4. 截取子串
substr(pos, len)
函数的参数返回从 pos 位置开始截取最多 len 个字符组成的字符串(如果从 pos 开始的后缀长度不足 len 则截取这个后缀)。
5. 插入/删除字符(串)
insert(index,count,ch)
和insert(index,str)
分别表示在 index 处连续插入 count 次字符串 ch 和插入字符串 str。erase(index,count)
函数将字符串 index 位置开始(含)的 count 个字符删除(若不传参给 count 则表示删去 count 位置及以后的所有字符)。
6. 替换字符(串)
replace(pos,count,str)
表示将从 pos 位置开始 count 个字符的子串替换为 str
replace(first,last,str)
表示将以 first 开始(含)、last 结束(不含)的子串替换为 str,其中 first 和 last 均为迭代器。
7. STL 对 string 类操作
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
int main()
{
string s("afgcbed");
string::iterator p = find(s.begin(), s.end(), 'c'); // 迭代器p
if (p!= s.end())
cout << p - s.begin() << endl; //输出 3
sort(s.begin(), s.end()); // 排序
cout << s << endl; //输出 abcdefg
next_permutation(s.begin(), s.end()); // 全排列函数
cout << s << endl; //输出 abcdegf
return 0;
}
344. 反转字符串 ●
class Solution {
public:
void reverseString(vector<char>& s) {
int n = s.size();
for (int i = 0; i < n/2; ++i){
// char temp = s[i];
// s[i] = s[n-1-i];
// s[n-1-i] = temp;
swap(s[i], s[n-1-i]);
}
}
};
- 时间复杂度:O(N),其中 N 为字符数组的长度。一共执行了 N/2 次的交换。
- 空间复杂度:O(1)。只使用了常数空间来存放若干变量。
541. 反转字符串 II ●
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
模拟
class Solution {
public:
string reverseStr(string s, int k) {
int len = s.length();
for(int i = 0; i < len; i += 2*k){ // 每次后移2k个字符
reverse(s.begin()+i, s.begin()+min(len,i+k)); // 反转前k个字符
}
// int j = 0;
// while(len > 0){
// if(len < k ){ // 反转前len个字符(len<k)
// // for(int i = 0; i < len/2; ++i){
// // swap(s[i+j*2*k],s[len-1-i+j*2*k]);
// // }
// }
// else{ // 反转前k个字符
// // for(int i = 0; i < k/2; ++i){
// // swap(s[i+j*2*k],s[k-1-i+j*2*k]);
// // }
// }
// ++j;
// len -= 2*k;
// }
return s;
}
};
剑指 Offer 05. 替换空格
1、新字符串+遍历
class Solution {
public:
string replaceSpace(string s) {
string ans;
for(char ch : s){
if(ch == ' '){
ans.append("%20"); // 插入字符串
}
else{
ans.push_back(ch); // 插入字符
}
}
return ans;
}
};
- 时间复杂度:O(n)。遍历字符串 s 一遍。
- 空间复杂度:O(n)。
2、扩容+双指针
- 特点:
1、不用申请新数组。
2、从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。
class Solution {
public:
string replaceSpace(string s) {
int n = 0;
int len0 = s.length(); // 原长度len0
for(char ch : s){
if(ch==' ') ++n; // 空格数量n
}
int len1 = s.length()+ 2 * n;
s.resize(len1); // 字符串扩容
int left = len0 - 1, right = len1 - 1;
while(left >= 0){ // 从后往前遍历
if(s[left] != ' '){
s[right--] = s[left--];
}
else{
s[right--] = '0';
s[right--] = '2';
s[right--] = '%';
left--;
}
}
return s;
}
};
- 时间复杂度 :O(N),遍历统计、遍历修改皆使用 O(N) 时间。
- 空间复杂度 :O(1),由于是原地扩展 s 长度,因此使用 O(1) 额外空间。
151.翻转字符串里的单词 ●●
输入:s = " hello world "
输出:“world hello”
1. 一次遍历,非原地解法
- 多次
insert
操作消耗大
class Solution {
public:
string reverseWords(string s) {
string ans;
char pre; // 前一个字符
int flag = 0; // 是否为起始空格
int i = 0; // 插入的位置
for(char ch : s){ // 从前往后遍历
if(ch != ' '){
if(pre == ' '){
ans.insert(0,1, ' '); // 插入一个空格
i = 0; // 新单词,插入位置清0
}
ans.insert(i,1, ch); // 第i个位置插入一个字符
++i;
flag = 1;
}
if(flag) pre = ch; // 非起始空格
}
return ans;
}
};
s.substr(pos, num)
截取子串 O(n)
- 最外面的while loop用来遍历s
- 第二个while loop用来遍历空格
- 第三个while loop用来遍历我们的字符
- startIndex 是宏观的变量,然后每次遍历时要计算字符长度 num 以便截取
- 注意截取时是 startIndex + 1, 因为此时 startIndex 已经不指向字符了
- 最后 return ans 时注意最后有个多余的空格
class Solution {
public:
string reverseWords(string s) {
string ans;
int n = s.length();
int startIndex = n-1; // 从后往前遍历
while(startIndex >= 0){
while(startIndex >= 0 && s[startIndex] == ' ') --startIndex;
int num = 0;
while(startIndex >= 0 && s[startIndex] != ' '){
--startIndex;
++num;
}
if(num) ans += s.substr(startIndex+1, num) + " ";
}
return ans.substr(0, ans.length()-1);
}
};
2. 双指针,原地修改
- 移除多余空格(双指针)
- 将整个字符串反转
- 将每个单词反转
- 时间复杂度:O(n)
- 空间复杂度:O(1)
class Solution {
public:
void eraseSpace(string& s){ // 双指针,删除不规格的空格
int slow = 0;
int fast = 0;
int n = s.length();
while(fast < n && s[fast] == ' ') ++fast; // 去除起始空格
while(fast < n){
if(fast > 0 && s[fast - 1] == ' ' && s[fast] != ' ' && slow != 0){
s[slow++] = ' '; // 新单词前加一个空格,slow!=0 确保第一个位置不为空格
}
while(fast < n && s[fast] != ' '){ // 移动指针,赋值
s[slow++] = s[fast++];
}
++fast;
}
s.resize(slow); // 改变长度
}
void reverse(string& s, int start, int end){ // 字符串指定位置的字符进行反转
for(int l = start, r = end; l < r; ++l, --r){
swap(s[l], s[r]);
}
}
string reverseWords(string s) {
eraseSpace(s); // 先清除不规格空格
int n = s.length();
reverse(s, 0, n-1); // 反转整个字符串
int start = 0;
int end = 0;
while(end < n){ // 逐个反转每个单词
while(s[end] != ' ' && end < n) ++end;
reverse(s, start, end-1);
start = end+1;
++end;
}
return s;
}
};
剑指Offer58-II.左旋转字符串 ●
输入: s = “abcdefg”, k = 2
输出: “cdefgab”
1. 申请O(n)额外空间
class Solution {
public:
string reverseLeftWords(string s, int n) {
return s.substr(n, s.length()-n) + s.substr(0,n); // 截取子串
}
};
2. O(1)空间,原地反转修改
class Solution {
public:
string reverseLeftWords(string s, int n) {
reverse(s.begin(),s.begin()+n); // 反转前n个字符
reverse(s.begin()+n,s.end()); // 反转后半段字符
reverse(s.begin(),s.end()); // 全部反转
return s;
}
};
28. 实现 strStr() ●
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
输入:haystack = “hello”, needle = “ll”
输出:2
1. 朴素的模式匹配
逐一匹配,匹配失败时退回已匹配好的个数重新查找匹配,会出现重复遍历的现象,最坏的时间复杂度为: O ( ( n − m + 1 ) ∗ m ) O((n-m+1)*m) O((n−m+1)∗m),m为子串长度。
class Solution {
public:
int strStr(string haystack, string needle) {
int m = haystack.length(), n = needle.length();
if(m == 0) return 0; // 空字符返回0
for(int i = 0; i <= m-n; ++i){ // haystack[i] 作为开头
bool flag = true; // 匹配标志位
for(int j = 0; j < n; ++j){
if(haystack[i+j] != needle[j]){ // 逐一匹配字符
flag = false; // 匹配失败,跳出
break;
}
}
if(flag) return i; // 判断当前匹配结果
}
return -1;
}
};
2. ★ KMP算法 ★
KMP主要应用在字符串匹配上。
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息不回溯文本串,而是考虑改变匹配串重新匹配的位置,避免重复遍历。
- 时间复杂度: O ( n + m ) O(n+m) O(n+m),遍历文本串 O ( n ) O(n) O(n) + 构造模式串前缀表 O ( m ) O(m) O(m)
- 空间复杂度: O ( m ) O(m) O(m),用于保存字符串 needle 的前缀表。
● 前缀表及其作用:
next 数组(前缀表):
记录当模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
如上图所示,下标 5 之前这部分的模式串(也就是字符串aabaa
)的最长相等的前缀和后缀字符串是子字符串 aa
,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
● 前缀表的计算
下标 i 之前(包括i)的字符串中,最长的相同前后缀的长度。
前缀是指所有/以第一个字符开头/,但不包含最后一个字符的连续子串。
后缀是指/以最后一个字符结尾/,但不包含第一个字符的连续子串。
- 利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置:
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。(因为匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了)
重点是构造模式串的前缀表:
- 初始化
next[0] = 0;
- 处理前后缀不相同的情况
while(s[i] != s[j] && j > 0) { j = next[j-1]; }
- 处理前后缀相同的情况
if(s[i] == s[j]){ ++j; } next[i] = j;
注意回退时,j
指向前一个位置对应的回退位置,即 j = next[j-1];
class Solution {
public:
void getNext(string s, int * next){
int j = 0; // j为前缀末尾位置,同时代表最长相同前后缀长度
next[0] = 0; // next数组
for(int i = 1; i < s.length(); ++i){ // 从下标 1 开始遍历字符串
while(s[i] != s[j] && j > 0){ // 当前缀末端j>0,且前后缀末端不相等时
j = next[j-1]; // 回退,前缀末端的位置指向前一个位置的next数组指向的值(即前一位对应的回退位置)
}
if(s[i] == s[j]){ // 前后缀末尾相等
++j; // 最长相同前后缀长度 = 下标 + 1, 指代下一次匹配的前缀末尾下标
}
next[i] = j; // 更新next[i] = 前缀的长度
}
}
int strStr(string haystack, string needle) {
int n = needle.length();
if(n == 0) return 0;
int next[n];
getNext(needle, next); // 构造模式串的前缀表
int h = 0; // h 表示当前待匹配的模式串字符位置
for(int k = 0; k < haystack.length(); ++k){ // 遍历文本串
while(h > 0 && haystack[k] != needle[h]){
h = next[h-1]; // 字符不匹配时,回退到前一位对应的回退位置
}
if(haystack[k] == needle[h]){
++h; // 匹配时 个数加一
}
if(h == n) return k-n+1; // 返回首字母下标
}
return -1;
}
};
459. 重复的子字符串 ●
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
输入: s = “abcabcabcabc”
输出: true
1. 枚举遍历
如果一个长度为 n 的字符串 s 可以由它的一个长度为 n′ 的子串 s′ 重复多次构成,那么:
- n 一定是 n’ 的倍数;
- s′ 一定是 s 的前缀;
- 对于任意的 i ∈ [ n′ ,n ),有
s[i] =s[i−n′]
。
我们可以从小到大枚举 n′,并对字符串 s 进行遍历,进行上述的判断。
注意到一个小优化是,因为子串至少需要重复一次,所以 n′ 不会大于 n 的一半,我们只需要在 [1, n/2] 的范围内枚举 n′ 即可。
class Solution {
public:
bool repeatedSubstringPattern(string s) {
int n = s.length();
for(int i = 1; 2 * i <= n; ++i){ // 遍历查找前缀 s’
if( n % i == 0){ // 长度为倍数时,可能为前缀
bool match = true;
for(int j = i; j < n; ++j){ // 遍历后缀,判断是否重复
if(s[j] != s[j-i]){
match = false; // 不重复,退出循环
break;
}
}
if(match) return true; // 重复标识符
}
}
return false;
}
};
- 时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n 是字符串 s 的长度。枚举 i 的时间复杂度为 O(n),遍历 s 的时间复杂度为O(n),相乘即为总时间复杂度。
- 空间复杂度: O ( 1 ) O(1) O(1)。
2. KMP 算法
class Solution {
public:
bool repeatedSubstringPattern(string s) {
int n = s.length();
int next[n];
next[0] = 0;
int j = 0;
for(int i = 1; i < n; ++i){ // 遍历,得到字符串s对应的前缀表next数组
while(j > 0 && s[j] != s[i]){
j = next[j-1];
}
if(s[j] == s[i]){
++j;
}
next[i] = j;
}
// 用规律判断是否符合条件
return (n % ( n - next[n-1]) == 0 && next[n-1] != 0);
}
};
n-next[n-1]
为第一个循环子串的长度;next[n-1] != 0
判断是否具有相同的前后缀。