字符串有点繁琐,先用张自己整理的脑图来镇楼~ 图片里如果有错误,还请斧正!感谢~
第五讲 字符串
0x1. 字符和整数的转化
常用ASCII值
-
特殊值:
A - Z
是 $65$ ~ $92$,a - z
是 $97$ ~ $122$,0 - 9
是 $48$ ~ $57$。 -
强制转换:
cpp char c = 'a'; cout << (int)c << endl; // 同理 (char)97 将97转换成a
/* 输入一行字符,统计出其中数字字符的个数,以及字母字符的个数。*/
#include <cstdio>
#include <iostream>
using namespace std;
int main()
{
char c;
int nums = 0, chars = 0;
while (cin >> c)
{
if (c >= '0' && c <= '9') nums++;
else if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') chars++;
}
printf("nums: %d\nchars: %d\n", nums, chars);
return 0;
}
ASCII码:
32 space
49 1
32+48=81
81 'Q'
0x2. 字符数组
-
字符串就是字符数组加上结束符
'\0'
-
可以使用 字符串 来初始化 字符数组,但此时要注意,每个字符串结尾会暗含一个
'\0'
字符,因此字符数组的长度至少要比字符串的长度多 $1$
#include <cstdio>
#include <iostream>
using namespace std;
int main()
{
char a1[] = {'C', '+', '+'}; //不是字符串,列表初始化,没有空字符
char a2[] = {'C', '+', '+', '\0'}; //列表初始化,含有显式的空字符,是字符串
char a3[] = "C++"; //自动添加表示字符串结尾的空字符
char a4[6] = "Daniel"; //错误,没有空间可存放空字符!
return 0;
}
2.1 读入字符数组
注意:scanf
和 cin
都是读到 空格
或 回车
为止
输入
- 读入字符数组时,
scanf("%s", s);
无须加取址符号&
,因为变量s
本身就是个地址。 - 输入也可以用
cin >> s;
- 如果想读到从下标
1
开始的话,写成scanf("%s", s + 1)
输出
- 可以用
cout << s;
- 也可以写成
printf("%s\n", s);
- 如果想从下标
1
或2
开始,则写成cout << s + 1;
(从下标1
开始输出) 或printf("%s\n", s + 1);
// 字符数组 vs 字符
char str[31]; //字符数组
scanf("%s", str); //不要加取址符&
char c; //字符
scanf("\n%c", &c); //过滤掉回车
2.2. 读入一整行
(1) 字符数组 用 fgets
或 cin.getline
输入;输出也可以用 puts
- 这里的
stdin
是把标准读入当成文件来读入。 - 输出除了用
printf
也可以用puts
,等价于printf("%s\n", s);
它会自动补上换行符 fgets
输入会自动加上回车,所以用puts
(输出自带回车) 的话会多一个回车
#include <cstdio>
#include <iostream>
using namespace std;
int main()
{
char s[100];
fgets(s, 100, stdin); //stdin 把终端当文件读入
cin.getline(s, 100);
cout << s << endl;
printf("%s", s);
puts(s); //等价于 printf("%s\n", s); 包括换行符
return 0;
}
(2) 字符串 用 getline(cin, s);
输入
- 如果是整行
string
类型的字符串,则用getline()
来输入
string s;
getline(cin , s);
- 如果是单个
string
类型,用cin
读入,不能用scanf
读入
字符串用cin
读会去掉多余空格
#include <iostream>
using namespace std;
int main()
{
string s;
while (cin >> s) cout << s << ' ';
return 0;
}
/*
输入样例:
Hello world.This is c language.
输出样例:
Hello world.This is c language.
*/
2.3 字符数组的常用操作
-
下面几个函数需要引入头文件:
#include <cstring>
-
(1)
strlen(str)
,求字符串的长度,只计算字符串的元素,\0
不计入其中 - (2)
strcmp(a, b)
,比较两个字符串的大小,$a < b$ 返回 $-1$,$a == b$ 返回 $0$,$a > b$ 返回 $1$。这里的比较方式是字典序! - (3)
strcpy(a, b)
,将字符串 $b$ 复制给从 $a$ 开始的字符数组。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
int main()
{
char s1[100] = "hello world!", s2[100];
cout << strlen(s1) << endl;
strcpy(s2, s1); // 把后者复制给前者
cout << strcmp(s1, s2) << endl;
return 0;
}
-
(4) 遍历字符数组中的字符
注意:如果没先存
len
而写成i < strlen(a)
,那么就两重循环了。
#include <cstring>
#include <iostream>
using namespace std;
int main()
{
char a[100] = "hello world";
for (int i = 0, len = strlen(a); i < len; i++)
cout << a[i] << endl;
return 0;
}
经典题:给定一个只包含小写字母的字符串,请你找到第一个仅出现一次的字符。如果没有,输出“no”。
/*开一个数组,存储 a 到 z 的出现次数
'a' 'b' 'c' 'd' ... 'z'
0 1 2 3 ... 25
*/
#include <cstring>
#include <iostream>
using namespace std;
int cnt[26];
char str[100010];
int main()
{
cin >> str;
int len = strlen(str);
for (int i = 0; i < len; i++) cnt[str[i] - 'a'] ++;
for (int i = 0; i < len; i++)
if (cnt[str[i] - 'a'] == 1)
{
cout << str[i] << endl;
return 0;
}
puts("no");
return 0;
}
因为字符数组最后是 \0
,所以循环条件写成如下即可
for (int i = 0; str[i]; i++)
经典题:密码翻译,输入一个只包含小写字母的字符串,将其中的每个字母替换成它的后继字母,如果原字母是’z’,则替换成’a’。
for (char &c : s)
if (c >= 'a' && c <= 'z') c = (c - 'a' + 1) % 26 + 'a';
else if (c >= 'A' && c <= 'Z') c = (c - 'A' + 1) % 26 + 'A';
难题:字符串模拟题
stoi(str);
//将字符串转换成数字atoi(str.c_str());
//将字符数组转换成数字
class Solution {
public:
int strToInt(string str) {
int k = 0;
//去掉空格
while (k < str.size() && str[k] == ' ') k++;
bool is_neg = false;
long long res = 0;
//判断正负
if (str[k] == '-') k++, is_neg = true;
else if (str[k] == '+') k++;
//字符变数字
while (k < str.size() && str[k] >= '0' && str[k] <= '9')
{
res = res * 10 + str[k] - '0';
k++;
}
// 处理特例
if (is_neg) res *= -1;
if (res > INT_MAX) res = INT_MAX;
if (res < INT_MIN) res = INT_MIN;
return res;
// return stoi(str); //将字符串转换成数字
// return atoi(str.c_str()); //将字符数组转换成数字
}
};
0x3. 标准库类型 string
- 可变长的字符序列,可以动态分配,比字符数组更加好用。需要引入头文件:
#include <string>
- 可将两个字符串拼接在一起
3.1 定义和初始化
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1; //默认的空字符串
string s2 = s1; // s2是s1的副本
string s3 = "hiya"; //s3是该字符串字面值的副本
string s4(10, 'c'); //s4的内容是 cccccccccc
return 0;
}
3.2 string
上的操作
- 不能用
scanf("%s", &s1)
来读入string
,这是错的;只能用cin
来读入 -
不能用
printf
直接输出string
,需要写成:cpp printf(“%s”, s.c_str()); //或 puts(s1.c_str());
c_str()
返回的是字符数组的首地址 -
使用
getline
读取一整行 -
string
的empty
和size
操作。size()
是 $O(1)$ 复杂度,strlen()
是 $O(n)$ 的复杂度。
3.3 字面值和 string
对象相加
- 做加法运算时,字面值和字符都会被转化成
string
对象,因此直接相加就是将这些字面值串联起来
string s1 = "hello", s2 = "world";// 在s1和s2中都没有标点符号
string s3 = s1 + "," + s2 + '!';
- 当把
string
对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是string
string s4 = s1 + ",";// 正确:把一个string对象和有一个字面值相加
string s5 = "hello" + ","; // 错误:两个运算对象都不是string
string s6 = s1 + "," + "world"; // 正确,每个加法运算都有一个运算符是string
string s7 = "hello" + "," + s2; // 错误:不能把字面值直接相加,运算是从左到右进行的
29. 技巧:比较时,把字符串变量存为数字
设猎人为 0
狗熊为 1
枪 为 2
1 赢 0
2 赢 1
0 赢 2
x y
if(x == y) Tie
if (x = (y + 1) % 3) Player1 赢
else Player2 赢
30.substr(pos, count)
的用法
substr(pos, count)函数:
pos - 要包含的首个字符的位置
count - 子串的长度
返回含子串 [pos, pos+count) 的 string
举例:左旋转字符串:
输入:”abcdefg” , n=2
输出:”cdefgab”
class Solution {
public:
string leftRotateString(string str, int n) {
return str.substr(n) + str.substr(0, n);
}
};
#include <iostream>
using namespace std;
int main()
{
string a, b;
while (cin >> a >> b)
{
int p = 0;
for (int i = 1; i < a.size(); i++)
if (a[i] > a[p]) p = i;
/* substr(pos, count)函数
pos - 要包含的首个字符的位置
count - 子串的长度
返回含子串 [pos, pos+count) 的 string
*/
cout << a.substr(0, p + 1) + b + a.substr(p + 1) << endl;
}
return 0;
}
31. fgets()
与 cin.getline()
的区别
- 用
fgets()
输入字符数组时,还要记得去掉末尾回车 - 用
cin.getline()
输入字符数组时,不用做这步操作
用 fgets()
输入字符数组:
// 用 fgets() 输入字符数组
char a[100], b[100];
// 去掉末尾回车
fgets(a, 100, stdin);
fgets(b, 100, stdin);
int x = strlen(a), y = strlen(b);
if (a[x-1] == '\n') a[x - 1] = 0;
if (b[y-1] == '\n') b[y - 1] = 0;
用 cin.getline()
输入字符数组:
// 用 cin.getline() 输入字符数组
char a[100], b[100];
cin.getline(a, 100);
cin.getline(b, 100);
fgets
输入会自动带上回车,如果再用puts
输出(自动带上回车),就有两个回车了,多出一个回车
#include <cstdio>
void print(char str[])
{
// puts(str); 由于 fgets会在最后加上回车,所以这里再用puts就多出一个回车,换printf
printf("%s", str); //而且也不要写成 "%s\n"
}
int main()
{
char str[110];
fgets(str, 110, stdin);
print(str);
return 0;
}
32. 将大写字母变成小写字母 1) ASCII 码 2) tolower
函数
'A' + 32 == 'a'
,字符'A'
的 ASCII 码是 $65$,'a'
的 ASCII 码是 $97$,它们之间相差 $32$,所以s[i] += 32;
c = tolower(c)
string a, b;
getline(cin, a), getline(cin, b);
for (char& c : a) c = tolower(c);
for (char& c : b) c = tolower(c);
33. stringstream
的用法
sstream
头文件定义了三个类型来支持内存 IO,这些类型可以向 string 写入数据,从 string 读取数据,就像 string 是一个 IO流一样。
istringstream
从 string 读取数据ostringstream
向 string 写入数据- 头文件
stringstream
用于读写给定 string 的字符串流,既可从 string 读数据也可向 string 写数据
#include <iostream>
#include <sstream>
using namespace std;
int main()
{
string s, a, b;
getline(cin, s);
cin >> a >> b;
stringstream ssin(s); //ssin 类似 cin
string str;
while (ssin >> str)
if (str == a) cout << b << ' ';
else cout << str << ' ';
return 0;
}
34. 双指针算法
for (int i = 0; i < s.size(); i++)
{
int j = i;
while (j < s.size() && s[j] == s[i]) j++;
i = j - 1;
}
35. string 句子有句号先去掉句号
while (cin >> str)
{
if (str.back() == '.') str.pop_back();
...
}
36. 字符串数组 - 倒排单词
字符串数组有别于字符数组,定义字符串数组 string str[100];
可以先用 while (cin >> str[n])
读入,统计 n
倒排单词可以倒序排,输出 str[i] << ' '
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
string str[100];
int n = 0;
while (cin >> str[n]) n++;
for (int i = n - 1; i >= 0; i--) cout << str[i] << ' ';
cout << endl;
return 0;
}
37. substr()
的用法
for ()
{
当前循环移位得到 a': a = a.substr(1) + a[0];
判断 b 是否是 a' 的子串
for (起点...)
for (枚举对应位置)
如果不一样,break
如果都一样,说明是子串
}
#include <iostream>
using namespace std;
int main()
{
string a, b;
cin >> a >> b;
if (a.size() < b.size()) swap(a, b);
for (int i = 0; i < a.size(); i++)
{
a = a.substr(1) + a[0];
for (int j = 0; j + b.size() <= a.size(); j++)
{
int k = 0;
for (; k < b.size(); k++)
if (a[j + k] != b[k]) break;
if (k == b.size())
{
puts("true");
return 0;
}
}
}
puts("false");
return 0;
}
类似技巧的难题:AcWing 778. 字符串最大跨距
38. 字符串的最大周期
- 这道题是让我们求几次方 $n$ 最大值,可以把字符串当成周期串来处理
- $len$ 是字符串总长,假定最大周期是 $n$,最小单位子串的长度为 $m = len/n$
- 可以采用倒序
n = len
再n--
,找到最大周期 $n$ 就可以break
跳出 - 一个条件是
len % n == 0
- 设单位子串为
string r = str.substr(0, m);
- 如果将单位子串循环 $n$ 遍后得到的字符串等于 $str$,说明 $n$ 是我们要求的最小周期
- 输出 $n$,并
break
跳出即可
#include <iostream>
using namespace std;
int main()
{
string str;
while (cin >> str, str != ".")
{
int len = str.size();
for (int n = len; n; n--)
if (len % n == 0)
{
int m = len / n;
string s = str.substr(0, m);
string r;
for (int i = 0; i < n; i++) r += s;
if (r == str)
{
cout << n << endl;
break;
}
}
}
return 0;
}