在竞赛中,string 的优势在于极其方便的拼接、比较和 STL 联动;但它的劣势在于底层涉及动态内存分配,一旦用错极容易造成 TLE (超时)RE (运行错误)

一、 初始化与 I/O 

1. 极速初始化

string s;                  // 空串
string s1 = "abc";         // 直接赋值
string s2(5, 'a');         // "aaaaa" (构造长度为 5 的相同字符,极常用)
string s3(s1, 1, 2);       // 截取 s1 从下标 1 开始,长度为 2 的部分 ("bc")

2. 竞赛中的读取策略

cin >> s;                  // 读入,遇到空格、回车、Tab 停止
getline(cin, s);           // 读入一整行,包含空格,遇到回车停止

 爆零坑点(getline 吞回车):

如果前面有 cin >> n;,必须加一句 cin.get();cin.ignore(); 吃掉缓冲区遗留的回车符,否则 getline 会读入一个空串直接结束!

二、 核心操作 (极简且极速)

1. 长度与容量

int len = s.length(); // 或 s.size(),两者等价,C++11 起强制 O(1) 复杂度
bool empty = s.empty(); // 判断是否为空串,比 s.size() == 0 更优雅

2. 元素访问

char c1 = s[i];       // O(1) 访问,竞赛推荐,不作越界检查(极速)
char c2 = s.at(i);    // O(1) 访问,会做越界检查并抛出异常(略慢,不推荐)
char head = s.front(); // 获取首字符 (C++11)
char tail = s.back();  // 获取尾字符 (C++11)

3. 追加与删除 (尾部操作)

s += 'a';             // 追加单个字符,均摊 O(1)
s += "abc";           // 追加字符串
s.push_back('a');     // 同 s += 'a'
s.pop_back();         // 极速删掉最后一个字符 (C++11)

三、 高频算法函数 

1. 截取子串 substr

// s.substr(起始下标, 截取长度)
string sub = s.substr(2, 3); // 从下标 2 开始,截取 3 个字符
string sub2 = s.substr(2);   // 从下标 2 开始,一直截取到末尾

超时警告: substr 的复杂度是 O(L)(L 为截取长度),并且会产生全新的内存拷贝。绝对不要在多重循环里高频调用 substr 提取子串来比较(比如咱们做字符串哈希或 KMP 时,老老实实用下标或哈希值,千万别用 substr 切片比较)。

2. 查找子串 find

// 返回匹配到的第一个首字母下标
int pos = s.find("abc"); 
int pos2 = s.find("abc", 2); // 从下标 2 开始往后找

// 找不到时,返回 string::npos
if (s.find("abc") == string::npos) {
    cout << "Not Found";
}

 性能提示: std::string::find 的最坏复杂度是$O(N \times M)$。虽然 STL 内部有优化,但在面对毒瘤出题人专门构造的卡常数据时(如 aaaaa...b),请自觉换用 KMP

3. 增删改 (中间操作)

s.insert(2, "xyz");      // 在下标 2 处插入 "xyz"
s.erase(2, 3);           // 从下标 2 开始,删掉 3 个字符
s.replace(2, 3, "xyz");  // 把从下标 2 开始的 3 个字符替换为 "xyz"

性能提示: 这三个操作的复杂度全部是O(N),因为需要大面积移动内存。在百万级数据的题中,慎用。


四、 STL 算法库无缝联动

string 本质上是一个 vector<char>,可以直接套用 <algorithm> 库:

#include <algorithm>

reverse(s.begin(), s.end()); // 字符串翻转 O(N)
sort(s.begin(), s.end());    // 字符升序排序 O(N log N)

// 字符去重 (配合 erase 缩容)
s.erase(unique(s.begin(), s.end()), s.end()); 

// 下一个字典序全排列
next_permutation(s.begin(), s.end()); 

五、 类型转换 (C++11/14 神器)

省去了以前写 stringstream 极其痛苦的转换过程:

// 数字转 string
string s_num = to_string(123456); 

// string 转数字
int num = stoi(s);           // string -> int
long long llnum = stoll(s);  // string -> long long
double dnum = stod(s);       // string -> double

六、 考场三大铁律

这三条铁律学生要刻在骨子里,这是无数 OIer 用眼泪换来的:

铁律 1:无符号整型的减法惨案 (s.size() - 1)

s.size() 的返回值类型是 size_t(无符号整型)。

如果此时字符串是空串s.size() 为 0。

你在循环里写 for(int i = 0; i < s.size() - 1; i++)

发生什么? 0 - 1 在无符号整型下会下溢出,变成 18446744073709551615。这个循环会变成死循环,直接导致数组越界,稳稳吃一个 RE / TLE

✅ 解法: 永远写成 i + 1 < s.size() 或者强转 i < (int)s.size() - 1

铁律 2:+=+ 的天壤之别

s = s + 'a'; // ❌ 极其致命的错误!
s += 'a';    // ✅ 正确做法

s = s + 'a' 会产生一个临时的拷贝对象,申请内存、复制一遍原串,再加一个字符。如果放在循环里,复杂度直接从O(N)飙升到O(N^2)。

s += 'a' 则是利用内部的容量翻倍机制在尾部直接拼接,均摊复杂度是绝对的O(1)。

铁律 3:函数传参必须带引用 &

void dfs(string s) // ❌ 每次递归都会把整个字符串 O(N) 复制一遍,瞬间 MLE/TLE!
void dfs(const string& s) // ✅ 只传递地址,O(1) 开销!

更多推荐