读这一页之前,先知道读完要能做什么
- 写出来读一整行(
getline)和读一个词(cin >>),并知道什么时候用哪个 - 写出来截子串
s.substr(pos, len)和找子串s.find(找不到是string::npos) - 说清
s == t比内容、s < t比字典序 —— 所以sort对字符串直接能用
这是什么
一句话:string 是「装一串字符的变量」 —— 下标从 0 数,s[i] 拿到一个 char,s.size() 是字符个数。
★★ 读入有三种,最常见的两个是这样的:
cin >> s —— 读一个词:空格、换行都断在那儿
getline(cin, s) —— 读一整行:含空格,换行才断
★ 字符串可以直接比:s == t 比的是内容(不是地址),s < t 是字典序。所以 sort(v.begin(), v.end()) 对 vector<string> 一个比较器都不用写,greater<string>() 也照样能用(第 1 块那套原样适用)。
⚠ s.size() 是无符号的:和 int 比大小要写 (int)s.size() —— 和 vector 是同一条规矩。
完整动作表
复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。
| 写法 | 干什么 | 代价 |
|---|---|---|
string s; | 开一个空串 ★ 尖括号里不用写「装什么」—— 它装的就是字符 | O(1) |
cin >> s | 读一个词(空格 / 换行都断) ★★ 只要一个词时用它 | O(len) |
getline(cin, s) | 读一整行(含空格) ★★★ 一行里有空格就必须用它(陷阱 ①);⚠ 前面刚 cin >> 过要先 cin.ignore() | O(len) |
s.size() | 字符个数 ⚠ 无符号 —— 和 int 比大小写 (int)s.size() | O(1) |
s[i] | 第 i 个字符(类型是 char)★ 从 0 数; s[s.size()] 是结尾那个 '\0',能读、别改 | O(1) |
s + t / s += t | 拼接:前者造一个新串,后者接在自己后面 ★ 拼字符串就这一句,不用循环一个个塞 | O(len) |
s.substr(pos, len) | 从下标 pos 开始取 len 个字符★★★ 第二个参数是长度,不是「取到哪个下标」(陷阱 ②) | O(len) |
s.find(t, from) | 从 from 起找子串 t:返回下标;找不到是 string::npos★★★ 判「在不在」写 != string::npos(陷阱 ③);接着找写 s.find(t, p + 1) | 最坏 O(n·m) |
s == t / s != t | 比内容(逐字符比) ★★★ 比的是内容不是地址 —— 想比地址得取 &s == &t(几乎不会用到) | O(n) |
s < t | 比字典序(和查字典的顺序一样) ★★ 所以 sort 对 vector<string> 直接能用;greater<string>() 降序 | O(n) |
for (char c : s) | 遍历每个字符 ★ 要改字符得写 for (char &c : s) —— 少个 & 只是改了复制品 | O(n) |
reverse(s.begin(), s.end()) | 原地翻转整个串 | O(n) |
tolower(c) / toupper(c) | 转小写 / 转大写(返回 int)★ 忽略大小写比较时用它; <cctype> 里的,bits/stdc++.h 已经带进来了 | O(1) |
⭐ 这一节最容易错的地方(3 处)
这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。
① `cin >> s` 读到空格就停 —— 一行里的词只读进来第一个
hello wide world,cin >> s 只拿到 hello。剩下的 wide world 还留在缓冲区里 —— 下一次 cin 会把它读走:你以为读的是「第二行」,其实是同一行的第二个词。
★ 于是读入整个错位,后面全错,而程序不报错、看起来还挺合理。① cin >> s 拿到的是 [hello] 长度 5
② getline 拿到的是 [wide world] 长度 10string a, b;
cin >> a; // 一个词
getline(cin, b); // ⚠ 这时读到的是 a 后面剩下的部分,不是下一行
// 要「先读一个数,再读后面整整一行」:
int n; cin >> n; cin.ignore();
getline(cin, line); // ★ ignore 把行尾的换行吃掉cin >>,「读一整行」用 getline;两个混着用时,中间加一句 cin.ignore()。② `s.substr(pos, len)` 第二个参数是长度,不是结尾下标
substr(2, 4) —— 长度 = 5 − 2 + 1。
★ 写成 substr(2, 5) 会多取一个字符;它不报错、不越界(越长越界时截到末尾),只是取出来的东西多一个少一个 —— 输出差一点点。string s = "abcdef";
s.substr(2, 3); // "cde" ← 从下标 2 开始,取 3 个
s.substr(2); // "cdef" ← 不写长度就是「取到末尾」③ `s.find(t)` 找不到返回 `string::npos`,不是一个能随手当 −1 用的数
string::npos 是一个很大的无符号数(size_t 的最大值附近)。
★ int p = s.find(t); 接住它会变成 −1(这一句恰好凑对了),但接着 s[p] 就是拿 −1 当下标 —— 转成无符号是天文数字的越界,要么崩,要么读到不属于它的内存。而 if (s.find(t) == -1) 是拿无符号和 −1 比,编译器会给出 signedness 警告。strfind-npos.cpp:5:19: warning: comparison of integer expressions of different signedness: ‘std::__cxx11::basic_string<char>::size_type’ {aka ‘long unsigned int’} and ‘int’ [-Wsign-compare]size_t p = s.find(t);
if (p == string::npos) { /* 没找到 */ }
else { use(p); }
// 只问在不在:
if (s.find(t) != string::npos) { ... }string::npos —— 判存在写 != string::npos,拿位置先判 == string::npos 再用。本书哪一章真的用到它
⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。
| 章 | 书里的哪一份代码 | 用它来干什么 |
|---|---|---|
| 第 47 章 | code/47-string-basics/p1308.cpp | ★★ 第 47 章:第二行必须 getline 读一整行 —— 陷阱 ① 的原产地 |
| 第 47 章 | code/47-string-basics/p1308Pad.cpp | ★ find + npos 的标准循环:找到之后从 p + 1 接着找 |
| 第 40 章 | code/40-gcd-euclid/p2152.cpp | ★ 高精度除法按块截子串 —— 第二个参数 i - l 正是「取几个」 |
| 第 48 章 | code/48-kmp/p4391All.cpp | ★★ 「在不在」的标准写法就这一句;第 48 章的 KMP 是它的快版 |
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。
例题 ① 读一个词 vs 读一整行
一行文本里有多个词。分别用 cin >> 和 getline 读,看各自拿到了什么;再用 getline + substr 把每个词逐个切出来。
★ 这一题是整块的定性题:两种读法拿到的东西不一样,而错位了不报错。
输入
hello wide world
输出
① cin >> s 拿到的是 [hello] 长度 5 ② getline 拿到的是 [wide world] 长度 10 ③ 用 substr 逐个切出来: 第 0 个词 [wide],下标 0..3 第 1 个词 [world],下标 5..9 一共 2 个词
cin >> 只拿到第一个词 [hello];getline 拿到的是它后面剩下的那行 [wide world] —— 这就是陷阱 ① 说的「错位」。★ 想清楚「空格算不算内容」:cin >> 认为空格是分隔符,getline 认为空格是普通字符。
// STL 专题 · string —— 「读一个词」和「读一整行」是两件事//// ★ cin >> s 遇到空格 / 换行就停 —— 拿到的只是一个**词**// getline(cin, s) 遇到换行才停 —— 拿到的是**一整行**(含空格)//// ⚠ 前面刚用过 cin >> 读数字的话,行尾的换行还留在缓冲区,// 下一句 getline 会读到一个**空行** —— 要先 cin.ignore() 把它吃掉。//// ★ 要把一行里的词切开:s.substr(开始, 取几个)。⚠ 第二个参数是**长度**。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
string a; cin >> a; // ① 只读到第一个空格之前 cin.ignore(); // ★ 把剩下的「空格 + 换行」吃掉,好让 getline 读到真的一行
string line; getline(cin, line); // ② 读一整行(含空格)
cout << "① cin >> s 拿到的是 [" << a << "] 长度 " << a.size() << "\n"; cout << "② getline 拿到的是 [" << line << "] 长度 " << line.size() << "\n\n";
// ③ 逐个切词 cout << "③ 用 substr 逐个切出来:\n"; int cnt = 0, n = (int)line.size(), start = -1; for (int i = 0; i <= n; i++) { if (i < n && line[i] != ' ') { if (start < 0) start = i; // 一个词的开头 } else if (start >= 0) { string w = line.substr(start, i - start); // ★ 从 start 开始,取 (i - start) 个 cout << " 第 " << cnt << " 个词 [" << w << "],下标 " << start << ".." << i - 1 << "\n"; cnt++; start = -1; } } cout << "一共 " << cnt << " 个词\n"; return 0;}点「运行 ▶」看结果
- ★★ ①
cin >> a:只读到第一个空格之前 ——[hello],长度 5。剩下的wide world还在缓冲区里等着。 - ②
cin.ignore():吃掉紧跟着的那个字符(这里是空格),否则getline拿到的是「第一个词后面剩下的部分」,不是你要的下一行。 - ★★ ③
getline(cin, line):读到换行为止,含空格 ——[wide world]。 - ④ 切词:扫一遍,碰到空格就把
line.substr(start, i - start)取出来 —— ⚠ 第二个参数是长度i - start,不是下标i。
★★★ cin >> 给一个词,getline 给一整行。两个混着用的程序,错位是最常见的 bug —— 而它不报错。
例题 ② 回文 + 字典序排序
给 n 个单词:逐个判断是不是回文(忽略大小写),然后把它们按字典序排好,再按降序排一次。
★ 这一题想证明的是:字符串可以直接比、直接排 —— 一个比较器都不用写。
n。之后 n 行,每行一个单词(不含空格)。输入
5 Level madam python Abba racecar
输出
是不是回文(忽略大小写): Level -> 是 madam -> 是 python -> 不是 Abba -> 是 racecar -> 是 按字典序排(什么都不用写): Abba Level madam python racecar 字典序最小的是 Abba,最大的是 racecar 降序(greater<string>()): racecar python madam Level Abba
Level 和 Abba 都是「忽略大小写才算」的回文;python 不是。★ 字典序是 A < a(大写字母的编码更小),所以排完 Abba 在最前面。★ 判回文:两头往中间走,tolower 之后再比。⚠ 排序什么都不用写 —— 想想 s < t 比的是什么。
// STL 专题 · string —— 回文 + 字典序排序//// ★ 字符串可以直接比:// s == t 比**内容**(不是地址)// s < t 比**字典序**(和字典里的顺序一样)// 所以 sort 对 vector<string> 天然能用,一个比较器都不用写。//// ⚠ 判回文要忽略大小写:先把每个字符 tolower 之后再比。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; vector<string> v(n); for (string &s : v) cin >> s;
cout << "是不是回文(忽略大小写):\n"; for (const string &s : v) { bool ok = true; for (int i = 0, j = (int)s.size() - 1; i < j; i++, j--) { if (tolower(s[i]) != tolower(s[j])) { ok = false; break; } } cout << " " << s << " -> " << (ok ? "是" : "不是") << "\n"; }
cout << "\n按字典序排(什么都不用写):\n "; sort(v.begin(), v.end()); for (const string &s : v) cout << s << " "; cout << "\n\n字典序最小的是 " << v.front() << ",最大的是 " << v.back() << "\n";
// 顺手:字符串也能用 greater<string>() 降序 —— 第 0 块那套原样适用 sort(v.begin(), v.end(), greater<string>()); cout << "降序(greater<string>()): "; for (const string &s : v) cout << s << " "; cout << "\n"; return 0;}点「运行 ▶」看结果
- 判回文:
i从头、j从尾,往中间走;tolower(s[i]) != tolower(s[j])就不是回文,立刻break。 - 排序:
sort(v.begin(), v.end())—— 对vector<string>直接能用,因为string自带<(字典序)。第 1 块那套「比较器」在这儿是白送的。 - 最小 / 最大:排完
v.front()就是最小、v.back()就是最大 —— 这就是「排好序」带来的顺手好处。 - 降序:
greater<string>(),和greater<int>()只差一个类型名。
★★ 字符串的 == 比内容、< 比字典序 —— 这两条一记住,排序、去重、查表(map<string,int>)全都顺理成章。
用到的写法:s == t / s != t s < t for (char c : s) tolower(c) / toupper(c) reverse(s.begin(), s.end())
练习(2 道)
只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。
练习 ① 找子串:find 接着找,直到 npos
给一行文本和一个模式串,数一数模式在文本里出现了几次、每次在哪个下标;再判断另一个串在不在。
★ 这一题练的是 find 的循环写法:找到之后从 p + 1 接着找,直到 npos。
输入
banana anaconda ana
输出
文本:banana anaconda [ana] 出现 3 次:下标 1 3 7 [xyz] 在不在?不在 ← 判据是 != string::npos 从第一个匹配处截 3 个字符:[ana]
★ 循环条件是 p != string::npos;下一轮的起始位置写 p + 1。⚠ 想清楚为什么不是 p + 模式长度 —— 这两种写法在「有重叠的匹配」上结果不一样。
★★★ find + p + 1 + npos 这三样凑在一起就是「找所有出现位置」。判「在不在」只要一句 != string::npos。
练习 ② 一行里最长的词(并列取字典序小的)
读一行文本,切出所有词,输出词数、最长的词(并列时取字典序小的),再把词按长度从小到大排好(同长度按字典序)输出。
★ 三件事连起来:getline 读行 → substr 切词 → 自己写比较器排序。
输入
the quick brown fox jumps over the lazy dog
输出
一共 9 个词 最长的词 [brown](5 个字符) 按长度排好: (3) dog fox the the (4) lazy over (5) brown jumps quick
★ 切词沿用例题 ① 那段;「并列取字典序小的」想想比较要写成严格的哪种;排序是第 1 块的多关键字套路:先比长度,分不出再比字典序。
★★ 多关键字比较器在字符串上一样用:if (a.size() != b.size()) return a.size() < b.size(); return a < b; —— 和「分数、学号」那道题逐字同构。
← 上一块:vector:常用的几种写法 · 回 STL 专题目录 · 下一块:sort:排序和自定义比较器 →
⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。