C++ string 类详解:常用接口、字符串操作与模拟实现
文章目录
- [C++ `string` 类详解:常用接口、字符串操作与模拟实现](#C++
string类详解:常用接口、字符串操作与模拟实现) -
- [1 `string` 类为什么重要](#1
string类为什么重要)
- [1 `string` 类为什么重要](#1
- [2 `auto` 与范围 `for`](#2
auto与范围for) - [3 范围 `for`](#3 范围
for) - [4 `string` 的构造](#4
string的构造) -
- [4.1 默认构造](#4.1 默认构造)
- [4.2 使用 C 风格字符串构造](#4.2 使用 C 风格字符串构造)
- [4.3 构造指定数量的字符](#4.3 构造指定数量的字符)
- [4.4 拷贝构造](#4.4 拷贝构造)
- [5 `string` 的容量操作](#5
string的容量操作) -
- [5.1 `size()`](#5.1
size()) - [5.2 `length()`](#5.2
length()) - [5.3 `capacity()`](#5.3
capacity()) - [5.4 `empty()`](#5.4
empty()) - [5.5 `clear()`](#5.5
clear())
- [5.1 `size()`](#5.1
- [6 `reserve()`](#6
reserve()) - [7 `resize()`](#7
resize()) - [8 `string` 的访问与遍历](#8
string的访问与遍历) -
- [8.1 `operator\[\]`](#8.1
operator[]) - [8.2 迭代器遍历](#8.2 迭代器遍历)
- [8.3 反向迭代器](#8.3 反向迭代器)
- [8.4 范围 `for`](#8.4 范围
for)
- [8.1 `operator\[\]`](#8.1
- [9 `string` 的修改操作](#9
string的修改操作) -
- [9.1 `push_back()`](#9.1
push_back()) - [9.2 `append()`](#9.2
append()) - [9.3 `operator+=`](#9.3
operator+=)
- [9.1 `push_back()`](#9.1
- [10 `c_str()`](#10
c_str()) - [11 `find()` 与 `npos`](#11
find()与npos) -
- [11.1 指定开始位置](#11.1 指定开始位置)
- [11.2 `rfind()`](#11.2
rfind())
- [12 `substr()`](#12
substr()) - [13 `string` 的输入输出](#13
string的输入输出) -
- [13.1 `operator>>`](#13.1
operator>>) - [13.2 `getline()`](#13.2
getline())
- [13.1 `operator>>`](#13.1
- [14 `string` 的比较](#14
string的比较) - [15 `operator+` 为什么不建议大量使用](#15
operator+为什么不建议大量使用) - [16 `string` 的底层结构](#16
string的底层结构) -
- [16.1 小字符串优化](#16.1 小字符串优化)
- [16.2 不同实现不能混为一谈](#16.2 不同实现不能混为一谈)
- [17 `string` 高频算法思想](#17
string高频算法思想) - [18 仅反转字母](#18 仅反转字母)
- [19 找字符串中第一个只出现一次的字符](#19 找字符串中第一个只出现一次的字符)
- [20 最后一个单词的长度](#20 最后一个单词的长度)
- [21 判断回文字符串](#21 判断回文字符串)
- [22 字符串相加](#22 字符串相加)
- [23 `string` 模拟实现](#23
string模拟实现) - [24 一个存在问题的 `String`](#24 一个存在问题的
String) - [25 浅拷贝](#25 浅拷贝)
- [26 浅拷贝为什么会导致崩溃](#26 浅拷贝为什么会导致崩溃)
- [27 深拷贝](#27 深拷贝)
- [28 传统方式实现深拷贝](#28 传统方式实现深拷贝)
-
- [28.1 拷贝构造](#28.1 拷贝构造)
- [28.2 赋值运算符](#28.2 赋值运算符)
- [28.3 析构函数](#28.3 析构函数)
- [29 现代写法:利用 `swap`](#29 现代写法:利用
swap) - [30 写时拷贝](#30 写时拷贝)
-
- [30.1 引用计数](#30.1 引用计数)
- [30.2 写操作时复制](#30.2 写操作时复制)
- [31 `string` 学习中的核心联系](#31
string学习中的核心联系)
1 string 类为什么重要
C 语言中的字符串本质上是以 '\0' 结尾的字符数组
例如
cpp
char str[] = "hello";
内存中实际保存的是
text
h e l l o \0
C 语言提供了 strlen strcpy strcmp strcat 等字符串函数进行操作
这种方式存在两个比较明显的问题
第一,字符串本身和操作字符串的函数是分离的
第二,字符数组的空间需要程序员自己管理,稍不注意就容易发生越界
C++ 提供了 string 类,将字符串和相关操作封装到一个类中
cpp
#include <string>
using namespace std;
之后可以直接通过对象调用字符串相关操作
cpp
string s = "hello";
cout << s.size() << endl;
cout << s[0] << endl;
在实际 C++ 编程以及 OJ 题目中,字符串处理通常直接使用 string
2 auto 与范围 for
这两个语法并不是 string 本身的内容,但是在使用 string 时非常常见
2.1 auto
C++11 中,auto 可以让编译器根据初始化表达式自动推导变量类型
cpp
int a = 10;
auto b = a;
auto c = 'a';
编译器会根据右边的表达式推导
text
b -> int
c -> char
auto 声明的变量必须初始化
cpp
auto a = 10; // 正确
auto b; // 错误
因为编译器必须根据初始化表达式推导类型
指针
cpp
int x = 10;
auto y = &x;
auto* z = &x;
这两种写法得到的都是 int*
也就是说
cpp
auto y = &x;
等价于
cpp
int* y = &x;
引用
引用必须使用 &
cpp
int x = 10;
auto& y = x;
此时 y 是 x 的引用
如果写成
cpp
auto y = x;
那么得到的是一个新的 int 变量,而不是引用
同一条语句中的多个 auto
同一条声明中的多个变量必须推导出相同类型
cpp
auto a = 1;
auto b = 2;
可以
但是
cpp
auto a = 1, b = 2.0;
不可以
因为 a 推导为 int,b 推导为 double
auto 的限制
auto 不能直接作为普通函数参数
cpp
void func(auto a)
{
}
这种写法不成立
auto 可以作为函数返回值
cpp
auto func()
{
return 10;
}
但实际编程中通常需要根据情况使用,不要为了使用 auto 而使用 auto
auto 也不能直接用来声明数组
cpp
auto arr[] = {1, 2, 3};
这种写法不成立
3 范围 for
传统遍历数组需要自己控制下标
cpp
int arr[] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; ++i)
{
cout << arr[i] << endl;
}
C++11 提供了范围 for
cpp
for (auto e : arr)
{
cout << e << endl;
}
基本形式是
cpp
for (元素变量 : 被遍历对象)
{
// 循环体
}
例如遍历 string
cpp
string s = "hello";
for (auto ch : s)
{
cout << ch << endl;
}
每次循环都会依次取出字符串中的一个字符
如果需要修改原字符串中的字符,可以使用引用
cpp
for (auto& ch : s)
{
ch = 'A';
}
这里的关键区别是
cpp
auto ch
得到的是元素的副本
而
cpp
auto& ch
得到的是元素本身的引用
范围 for 本质上仍然可以理解成通过迭代器完成遍历
4 string 的构造
4.1 默认构造
cpp
string s;
创建一个空字符串
text
s = ""
4.2 使用 C 风格字符串构造
cpp
string s("hello");
也可以写成
cpp
string s = "hello";
本质上都是根据 C 风格字符串构造 string
4.3 构造指定数量的字符
cpp
string s(5, 'a');
结果
text
aaaaa
也就是构造一个包含 5 个 'a' 的字符串
4.4 拷贝构造
cpp
string s1("hello");
string s2(s1);
或者
cpp
string s2 = s1;
此时 s2 是根据 s1 构造出来的 string 对象
5 string 的容量操作
常用接口主要有
cpp
size()
length()
capacity()
empty()
clear()
reserve()
resize()
5.1 size()
获取字符串有效字符的数量
cpp
string s = "hello";
cout << s.size() << endl;
结果为
text
5
注意
size() 计算的是有效字符数量,不包括字符串结尾意义上的 '\0'
5.2 length()
cpp
string s = "hello";
cout << s.length() << endl;
结果同样是
text
5
size() 和 length() 的底层实现原理相同
之所以同时存在,是因为 STL 中其他容器通常使用 size() 表示元素数量
因此平时使用 size() 更常见
5.3 capacity()
cpp
string s = "hello";
cout << s.capacity() << endl;
capacity() 表示当前底层空间能够容纳多少有效字符
它和 size() 不一样
例如
text
size -> 当前有多少字符
capacity -> 当前空间最多能够容纳多少字符
可以理解为
text
size <= capacity
5.4 empty()
判断字符串是否为空
cpp
string s;
if (s.empty())
{
cout << "empty" << endl;
}
如果字符串中没有有效字符,则返回 true
否则返回 false
5.5 clear()
清空字符串中的有效字符
cpp
string s = "hello";
s.clear();
此时
cpp
s.size() == 0
但是要注意
clear() 只是清空有效字符,不会主动释放底层容量
也就是说
text
size 可能变成 0
capacity 通常保持不变
这也是为什么之后再次向字符串中添加数据时,可以继续利用之前申请的空间
6 reserve()
reserve() 用来提前申请空间
例如能够大概确定字符串最终会存放很多字符
cpp
string s;
s.reserve(1000);
可以提前为字符串预留空间
重要的一点是
cpp
reserve()
只改变容量,不改变有效字符数量
例如
cpp
string s = "hello";
s.reserve(1000);
此时
text
size 仍然是 5
capacity 至少能够容纳相应数量的字符
如果 reserve() 传入的值小于当前容量,一般不会导致容量缩小
7 resize()
resize() 用来改变字符串中有效字符的数量
例如
cpp
string s = "hello";
s.resize(3);
结果
text
hel
如果新的长度比原来小
text
有效字符减少
容量通常不会因此减少
如果新的长度比原来大
cpp
string s = "hello";
s.resize(8);
会增加有效字符
使用
cpp
resize(n)
增加出来的字符使用 '\0' 填充
也可以指定填充字符
cpp
s.resize(8, 'x');
则会使用 'x' 填充新增部分
因此
cpp
resize(10)
和
cpp
resize(10, 'a')
的区别主要就在新增字符的填充方式
8 string 的访问与遍历
8.1 operator[]
通过下标访问字符串中的字符
cpp
string s = "hello";
cout << s[0] << endl;
输出
text
h
可以通过下标修改字符
cpp
s[0] = 'H';
结果
text
Hello
8.2 迭代器遍历
cpp
string s = "hello";
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << endl;
++it;
}
其中
cpp
begin()
指向第一个字符
cpp
end()
指向最后一个字符的下一个位置
因此遍历通常写成
cpp
for (auto it = s.begin(); it != s.end(); ++it)
{
cout << *it << endl;
}
8.3 反向迭代器
对应的反向迭代器是
cpp
rbegin()
rend()
例如
cpp
for (auto it = s.rbegin(); it != s.rend(); ++it)
{
cout << *it << endl;
}
可以从字符串尾部向前遍历
8.4 范围 for
最简单的写法通常是
cpp
for (auto ch : s)
{
cout << ch;
}
如果需要修改字符串
cpp
for (auto& ch : s)
{
ch = 'x';
}
9 string 的修改操作
9.1 push_back()
在字符串尾部添加一个字符
cpp
string s = "hello";
s.push_back('!');
结果
text
hello!
9.2 append()
在字符串末尾追加内容
cpp
string s = "hello";
s.append(" world");
结果
text
hello world
也可以追加指定数量的字符
cpp
s.append(3, 'a');
9.3 operator+=
实际使用中非常常见
cpp
string s = "hello";
s += '!';
s += " world";
最终
text
hello! world
+= 既可以追加单个字符,也可以追加字符串
所以实际编程中通常非常方便
10 c_str()
string 可以通过 c_str() 获取 C 风格字符串
cpp
string s = "hello";
const char* str = s.c_str();
得到的是一个指向字符串内部字符数组的指针
它常用于需要 C 风格字符串的函数或接口
例如
cpp
printf("%s\n", s.c_str());
这里不能直接把 string 当成普通的 char*
需要通过
cpp
s.c_str()
取得 C 风格字符串
11 find() 与 npos
find() 用于查找字符或者字符串
例如
cpp
string s = "hello world";
size_t pos = s.find('o');
返回找到的位置
字符串下标从 0 开始
如果没有找到,则返回
cpp
string::npos
因此通常这样判断
cpp
size_t pos = s.find('x');
if (pos == string::npos)
{
cout << "not found" << endl;
}
npos 可以理解为
text
没有找到
11.1 指定开始位置
cpp
s.find('o', 5);
表示从下标 5 开始向后查找
11.2 rfind()
rfind() 与 find() 相似,但是搜索方向不同
cpp
s.rfind('o');
从后往前寻找
因此
text
find -> 找第一个符合条件的位置
rfind -> 从后面开始找
12 substr()
substr() 用于截取子字符串
cpp
string s = "hello world";
string sub = s.substr(6, 5);
得到
text
world
基本形式
cpp
s.substr(pos, len)
表示从 pos 开始,截取 len 个字符
例如
cpp
string s = "abcdef";
string sub = s.substr(2, 3);
得到
text
cde
13 string 的输入输出
13.1 operator>>
cpp
string s;
cin >> s;
这种方式遇到空格会停止
例如输入
text
hello world
最终
text
s = "hello"
因为 >> 默认以空白字符作为分隔
13.2 getline()
如果需要读取整行字符串
cpp
string s;
getline(cin, s);
输入
text
hello world
那么
text
s = "hello world"
所以处理包含空格的字符串时,经常使用 getline()
14 string 的比较
string 支持直接进行关系运算
例如
cpp
string a = "abc";
string b = "abd";
if (a < b)
{
cout << "a < b" << endl;
}
字符串比较按照字符的字典序进行
因此可以直接使用
cpp
==
!=
<
>
<=
>=
这比 C 语言中的 strcmp() 使用起来更加直接
15 operator+ 为什么不建议大量使用
可以使用
cpp
string c = a + b;
但是需要注意效率问题
operator+ 通常会产生新的字符串对象,并涉及返回对象以及相关资源处理
如果在循环中大量使用类似操作
cpp
string s;
for (...)
{
s = s + x;
}
可能产生较多额外开销
相比之下
cpp
s += x;
通常更加直接
因此实际编程中字符串追加通常优先考虑 +=
如果能够提前知道大概需要多少空间,还可以配合
cpp
reserve()
减少扩容带来的额外开销
16 string 的底层结构
不同编译器和标准库实现中的 string 内部结构并不完全相同
有些地方的结构是在特定平台和实现下观察得到的,不能把它当成 C++ 标准规定的固定布局
一个非常重要的思想是
string的本质仍然是对一块字符存储空间的封装
16.1 小字符串优化
某些实现会采用小字符串优化
也就是字符串很短的时候,直接利用 string 对象内部的固定空间保存字符,而不是去堆上申请空间
例如书中的 VS 实现中
text
字符串较短
↓
直接使用对象内部空间
字符串较长
↓
从堆上申请空间
这样做的目的主要是减少短字符串频繁进行动态内存申请带来的开销
16.2 不同实现不能混为一谈
某些旧版 G++ 实现中的写时拷贝机制
其中多个 string 对象可以共同使用同一份底层资源
同时通过引用计数记录资源的使用者数量
但具体 STL 实现可能随着编译器版本发生变化
因此学习这里时,更重要的是理解
text
string
↓
管理字符资源
↓
需要处理空间申请
↓
需要处理拷贝
↓
需要处理资源释放
而不是死记某个版本的对象大小
17 string 高频算法思想
字符串题目中经常出现的核心思路包括
text
双指针
字符统计
字符串遍历
模拟
逆序
查找
截取
18 仅反转字母
例如字符串
text
a-bC-dEf-ghIj
要求只反转字母,其他字符位置保持不变
可以使用左右双指针
text
begin → ← end
左边遇到非字母就向右移动
右边遇到非字母就向左移动
当两边都找到字母之后交换
核心代码
cpp
while (begin < end)
{
while (begin < end && !isLetter(S[begin]))
++begin;
while (begin < end && !isLetter(S[end]))
--end;
swap(S[begin], S[end]);
++begin;
--end;
}
判断字母可以自己写函数
cpp
bool isLetter(char ch)
{
if (ch >= 'a' && ch <= 'z')
return true;
if (ch >= 'A' && ch <= 'Z')
return true;
return false;
}
这种题最重要的是理解双指针如何跳过无效字符
19 找字符串中第一个只出现一次的字符
典型思路是
text
第一次遍历
统计每个字符出现次数
第二次遍历
按照原字符串顺序寻找出现次数为 1 的字符
例如
cpp
int count[256] = {0};
for (int i = 0; i < s.size(); ++i)
{
count[s[i]]++;
}
for (int i = 0; i < s.size(); ++i)
{
if (count[s[i]] == 1)
return i;
}
return -1;
这里使用数组统计字符出现次数
因为字符可以直接作为数组下标使用
核心思想并不是 string 本身,而是
text
统计 + 再遍历
20 最后一个单词的长度
假设输入
text
hello world
最后一个单词是
text
world
长度为
text
5
可以使用
cpp
getline(cin, line);
读取整行
然后使用
cpp
rfind(' ')
寻找最后一个空格
cpp
size_t pos = line.rfind(' ');
cout << line.size() - pos - 1 << endl;
这里要注意
如果题目允许字符串末尾存在空格,就需要进一步处理末尾空格
21 判断回文字符串
回文的特点是
text
正着读和反着读相同
例如
text
abcba
可以使用左右双指针
text
begin → ← end
比较
cpp
s[begin]
s[end]
如果不同,直接返回 false
如果相同
cpp
++begin;
--end;
继续向中间移动
如果题目要求忽略非字母数字字符,那么左右指针遇到这些字符时直接跳过
cpp
while (begin < end && !isLetterOrNumber(s[begin]))
++begin;
while (begin < end && !isLetterOrNumber(s[end]))
--end;
这种题的核心仍然是
text
双指针 + 条件跳过 + 对称比较
22 字符串相加
两个非常大的数字不能直接使用普通整数类型保存
例如
text
num1 = "123456789123456789"
num2 = "987654321987654321"
可以直接按照小学竖式加法的方式进行
从最后一位开始
text
个位
↓
十位
↓
百位
↓
...
核心代码思想
cpp
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int value1 = 0;
int value2 = 0;
if (end1 >= 0)
value1 = num1[end1--] - '0';
if (end2 >= 0)
value2 = num2[end2--] - '0';
int value = value1 + value2 + next;
if (value > 9)
{
next = 1;
value -= 10;
}
else
{
next = 0;
}
result += value + '0';
}
由于我们是从低位向高位计算,而字符串最终要求从高位到低位排列,因此最后需要
cpp
reverse(result.begin(), result.end());
这里有一个非常值得记住的转换
cpp
num1[end1] - '0'
把数字字符转换成真正的数字
而
cpp
value + '0'
把数字转换成对应的字符
23 string 模拟实现
学习 string 模拟实现的核心,并不是重新写一个完整 STL,而是理解
text
构造函数
拷贝构造
赋值运算符
析构函数
动态内存管理
浅拷贝
深拷贝
写时拷贝
这些内容和 C++ 类的资源管理密切相关
24 一个存在问题的 String
例如
cpp
class String
{
public:
String(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
表面上看起来没有问题
构造的时候申请空间
析构的时候释放空间
但是
cpp
String s1("hello");
String s2(s1);
就会出现严重问题
25 浅拷贝
如果没有自己实现拷贝构造函数
编译器会生成默认拷贝构造
默认拷贝本质上是成员逐字节拷贝
于是
text
s1
↓
+----------+
| 0x1000 |──────→ "hello"
+----------+
s2
↓
+----------+
| 0x1000 |──────→ "hello"
+----------+
两个对象中的 _str 指针保存的是同一个地址
也就是说
text
s1._str == s2._str
两个对象共同管理同一块堆空间
这就是浅拷贝带来的资源管理问题
26 浅拷贝为什么会导致崩溃
假设
cpp
String s1("hello");
String s2(s1);
最终
text
s1 ─┐
├──→ 同一块堆空间
s2 ─┘
当 s2 析构时
cpp
delete[] _str;
堆空间被释放
但是 s1._str 仍然保存着原来的地址
此时这个指针已经成为悬空指针
等 s1 再析构
cpp
delete[] _str;
就可能对同一块空间再次释放
最终产生
text
double free
等一系列严重问题
所以只要一个类自己管理动态资源,就必须认真考虑拷贝行为
27 深拷贝
深拷贝的思想非常简单
text
每个对象拥有自己独立的资源
例如
text
s1 ───→ "hello"
s2 ───→ "hello"
虽然两个字符串内容相同
但是底层空间不同
因此一个对象销毁时不会影响另一个对象
28 传统方式实现深拷贝
构造函数
cpp
String(const char* str = "")
{
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
这里
cpp
strlen(str) + 1
中的 +1 非常重要
因为还需要保存字符串结尾的
cpp
'\0'
28.1 拷贝构造
cpp
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
这里重新申请了一块空间
然后把原字符串内容复制过去
因此两个对象拥有独立的资源
28.2 赋值运算符
cpp
String& operator=(const String& s)
{
if (this != &s)
{
char* pStr = new char[strlen(s._str) + 1];
strcpy(pStr, s._str);
delete[] _str;
_str = pStr;
}
return *this;
}
这里必须注意
cpp
if (this != &s)
它是在防止自己给自己赋值
例如
cpp
s1 = s1;
如果没有这个判断,可能在复制数据之前就把原来的资源释放掉
28.3 析构函数
cpp
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
负责释放构造函数申请的动态空间
这三个函数需要联系起来理解
text
构造函数
↓
申请资源
拷贝构造
↓
复制资源
赋值运算符
↓
替换资源
析构函数
↓
释放资源
这就是典型的资源管理问题
29 现代写法:利用 swap
还可以利用临时对象和 swap 简化资源管理
拷贝构造可以写成
cpp
String(const String& s)
: _str(nullptr)
{
String strTmp(s._str);
swap(_str, strTmp._str);
}
赋值运算符可以写成
cpp
String& operator=(String s)
{
swap(_str, s._str);
return *this;
}
这种写法的关键是
text
先构造一个临时对象
↓
让临时对象拥有新的资源
↓
swap
↓
当前对象获得新资源
↓
临时对象获得旧资源
↓
临时对象析构
↓
自动释放旧资源
这样能够利用析构函数自动完成旧资源释放
30 写时拷贝
写时拷贝的核心思想是
text
能共享就先共享
真正修改时再复制
它建立在浅拷贝的基础上,同时增加引用计数
例如有两个对象共同使用一块资源
text
s1 ─┐
├──→ 同一份资源
s2 ─┘
引用计数 = 2
如果再增加一个对象
text
s1 ─┐
s2 ─┼──→ 同一份资源
s3 ─┘
引用计数 = 3
30.1 引用计数
引用计数用于记录当前有多少个对象使用这份资源
基本过程
text
创建资源
↓
引用计数 = 1
又有对象使用
↓
引用计数++
对象销毁
↓
引用计数--
引用计数归零
↓
释放资源
这样多个对象可以共享一份资源
30.2 写操作时复制
假设
text
s1 ─┐
├──→ "hello"
s2 ─┘
此时两个对象共享资源
如果只是读取
cpp
cout << s1;
没有必要复制
但如果
cpp
s1[0] = 'H';
此时不能直接修改共享资源
因为这样会同时影响 s2
所以需要先复制一份
text
修改前
s1 ─┐
├──→ "hello"
s2 ─┘
修改 s1
s1 ───→ "Hello"
s2 ───→ "hello"
这就是写时拷贝
不过现代 C++ 标准库实现中并不能简单地认为 std::string 一定使用这种方式,具体实现由标准库决定
31 string 学习中的核心联系
string 看起来只是一个字符串类,但实际上把很多 C++ 核心知识联系起来了
text
string
│
├── 类与对象
│
├── 构造函数
│
├── 拷贝构造
│
├── 运算符重载
│
├── 动态内存管理
│
├── 深拷贝
│
├── 浅拷贝
│
├── 析构函数
│
├── 迭代器
│
├── 范围 for
│
├── auto
│
└── STL
而字符串算法本身最常见的思路主要集中在
text
双指针
字符统计
字符串模拟
查找
截取
逆序
掌握这些之后,绝大多数基础字符串题都可以从这些思路出发进行分析
08.string 08.string