在C/C++编程中,字符串处理是每个开发者都无法绕开的基础课题。string作为标准库中最常用的容器之一,其重要性不亚于数组和链表。但很多初学者(甚至一些有经验的开发者)在面试和实际开发中,仍然会在string操作上栽跟头。
我整理这个刷题集的初衷,源于去年面试候选人的真实经历:一位自称"精通C++"的应聘者,在面对"反转字符串中的单词"这道基础题时,竟然用了3个嵌套循环才勉强实现。这让我意识到,系统性地掌握string操作技巧有多么重要。
string类的构造函数有7种重载形式,但实际开发中最常用的是这3种:
cpp复制string s1; // 空字符串
string s2("hello"); // 从C风格字符串构造
string s3(5, 'a'); // 包含5个'a'的字符串
遍历字符串时,我强烈推荐使用迭代器而非下标访问:
cpp复制for(auto it = s.begin(); it != s.end(); ++it) {
cout << *it;
}
注意:在C++11及以上版本中,范围for循环更简洁:
cpp复制for(char c : s) { cout << c; }
find()系列方法是最常用的查找函数,但很多人不知道它们可以指定起始位置:
cpp复制size_t pos = s.find("ll", 3); // 从下标3开始查找"ll"
更高效的查找策略是KMP算法,特别适合需要多次匹配的场景。这里给出一个next数组的生成实现:
cpp复制vector<int> getNext(const string& p) {
vector<int> next(p.size());
next[0] = -1;
int j = -1;
for(int i = 1; i < p.size(); ++i) {
while(j >= 0 && p[i] != p[j+1]) {
j = next[j];
}
if(p[i] == p[j+1]) {
++j;
}
next[i] = j;
}
return next;
}
erase()和insert()操作的时间复杂度都是O(n),在循环中使用时要特别小心:
cpp复制// 错误示范:每次erase都导致后续字符移动
for(int i = 0; i < s.size(); ) {
if(s[i] == 'a') {
s.erase(i, 1); // 效率极低
} else {
++i;
}
}
// 正确做法:双指针原地修改
int slow = 0;
for(int fast = 0; fast < s.size(); ++fast) {
if(s[fast] != 'a') {
s[slow++] = s[fast];
}
}
s.resize(slow);
最经典的题目莫过于反转字符串。看似简单,却能考察对多种方法的掌握:
cpp复制reverse(s.begin(), s.end());
cpp复制int left = 0, right = s.size() - 1;
while(left < right) {
swap(s[left++], s[right--]);
}
cpp复制void reverse(string& s, int left, int right) {
if(left >= right) return;
swap(s[left], s[right]);
reverse(s, left+1, right-1);
}
"实现strStr()"是LeetCode上的经典题目,完整实现KMP算法如下:
cpp复制int strStr(string haystack, string needle) {
if(needle.empty()) return 0;
auto next = getNext(needle);
int j = -1;
for(int i = 0; i < haystack.size(); ++i) {
while(j >= 0 && haystack[i] != needle[j+1]) {
j = next[j];
}
if(haystack[i] == needle[j+1]) {
++j;
}
if(j == needle.size() - 1) {
return i - j;
}
}
return -1;
}
"字符串解码"这类题目考察对嵌套结构的处理能力。以"3[a2[c]]"为例,正确解法需要用到栈:
cpp复制string decodeString(string s) {
stack<pair<int, string>> st;
string current;
int num = 0;
for(char c : s) {
if(isdigit(c)) {
num = num * 10 + (c - '0');
} else if(c == '[') {
st.emplace(num, move(current));
num = 0;
current.clear();
} else if(c == ']') {
auto [cnt, prev] = st.top();
st.pop();
string temp;
for(int i = 0; i < cnt; ++i) {
temp += current;
}
current = prev + temp;
} else {
current += c;
}
}
return current;
}
C++11引入的移动语义可以显著提升字符串处理性能:
cpp复制string processString(string&& s) { // 接受右值引用
// 处理过程...
return s; // 返回值优化(RVO)
}
string result = processString("original"); // 零拷贝
处理超大字符串时(如日志分析),应该避免整体加载:
示例代码框架:
cpp复制void processLargeFile(const string& filename) {
ifstream file(filename, ios::binary);
const int BUFFER_SIZE = 4096;
char buffer[BUFFER_SIZE];
while(file) {
file.read(buffer, BUFFER_SIZE);
string chunk(buffer, file.gcount());
// 处理当前块...
}
}
处理UTF-8等编码时,直接使用string可能导致问题。推荐方案:
UTF-8字符长度判断示例:
cpp复制int getUtf8CharLen(char firstByte) {
if((firstByte & 0x80) == 0) return 1;
if((firstByte & 0xE0) == 0xC0) return 2;
if((firstByte & 0xF0) == 0xE0) return 3;
return 4;
}
string使用时最常遇到的内存问题是越界访问:
cpp复制string s = "hello";
char c = s[10]; // 未定义行为
安全访问建议:
使用perf工具分析字符串处理瓶颈:
bash复制perf record -g ./your_program
perf report
常见优化点:
不同平台下string的实现可能有差异:
解决方案:
cpp复制#ifdef _WIN32
const char PATH_SEP = '\\';
#else
const char PATH_SEP = '/';
#endif
C++17引入的string_view可以避免不必要的拷贝:
cpp复制void process(string_view sv) {
// 只读访问,不拷贝数据
cout << sv.substr(2, 3);
}
process("hello world"); // 不会构造临时string
C++20引入了format库,比传统方法更安全:
cpp复制string s = format("The answer is {}.", 42);
C++20协程可以简化异步字符串处理:
cpp复制async_generator<string> readLines(string_view filename) {
ifstream file(filename.data());
string line;
while(getline(file, line)) {
co_yield line;
}
}
按照难度分级整理的必刷题目:
在多年字符串处理实践中,我总结了这些血泪教训:
永远检查空字符串:很多边界条件崩溃都源于此
cpp复制if(s.empty()) return; // 好习惯
避免在循环中拼接字符串:
cpp复制// 糟糕
string result;
for(auto& item : items) {
result += item; // 每次都可能重新分配内存
}
// 优化
string result;
result.reserve(totalLength); // 预分配
for(auto& item : items) {
result += item;
}
善用标准算法:
cpp复制// 统计特定字符出现次数
int cnt = count(s.begin(), s.end(), 'a');
// 删除特定字符
s.erase(remove(s.begin(), s.end(), ' '), s.end());
理解SSO优化:大多数实现对小字符串(通常<=15字符)有特殊优化,了解这点可以避免过早优化
多考虑编码问题:处理用户输入时,永远不要假设字符串是ASCII