字符串专题总结:从模拟运算到模板掌握
字符串专题总结:从模拟运算到模板掌握
目录
- 学习记录
- 刷题记录
- 我的学习过程
- 字符串核心概念
- 中心扩散法
- 大数运算模拟
- 字符串比较策略
- 典型题目分类
- 类型1:字符串比较与前缀
- 类型2:回文串问题
- 类型3:大数运算
- 我踩的坑总结
- 典型模板总结
- 我的理解
- 后记
学习记录
- 刷题日期: 2025年10月20日(Day20)
- 完成题量: 4题全部AC
- 题目分布: 字符串比较1题 + 回文串1题 + 大数运算2题
- 学习用时: 约3-4小时
刷题记录
Day20(10.20):字符串专题集中突破
今天开始字符串专题的学习。之前在Day18做过链表加法(LeetCode 2),当时处理进位的思路今天又用上了。
完成题目:
- LeetCode 14 - 最长公共前缀 (easy) - AC
- LeetCode 5 - 最长回文子串 (medium) - AC
- LeetCode 67 - 二进制求和 (easy) - AC
- LeetCode 43 - 字符串相乘 (medium) - AC
具体情况:
第1题:最长公共前缀
- 状态:开始不知道怎么遍历
vector<string>
,搞不清楚如何记录结果 - 第一种方法(横向比较):经过引导后AC
- 想法:主动要求学第二种方法(纵向比较),最初理解不了行列关系
- 陷阱:
strs[i][j]
和strs[j][i]
搞混,还有i >= strs[j].size()
判断容易写错 - 结果:两种方法都掌握了
第2题:最长回文子串
- 状态:这题是重点,中心扩散法
- 困惑点:
- 奇数长度和偶数长度要分开处理(开始不理解)
- 循环结束后 left 和 right 的位置(已经越界了)
- 起始位置计算公式
begin = i - (curlen-1)/2
不太直观
- 结果:独立完成,注释写得很清楚,说明理解透了
第3题:二进制求和
- 状态:和Day18的链表加法很像,以为能秒了
- 大坑:
if(i)
判断,当 i=0 时直接跳过了!- 测试用例
a="11", b="1"
预期"100"
,实际输出"1"
- 调试了一会儿才发现:指针可以直接
if
,但索引必须if(i >= 0)
- 测试用例
- 用时:发现bug后改对,AC
- 收获:深刻理解了指针判断和索引判断的区别
第4题:字符串相乘(今天最难的)
- 状态:开始想转成整数再乘,后来知道会溢出
- 核心难点:
num1[i] × num2[j]
影响的位置是ret[i+j]
和ret[i+j+1]
- 无进位相乘 + 统一处理进位的思想
- Bug记录:
- Bug1:
ret[i+j+1] = ...
应该用+=
(同一位置多次累加) - Bug2:
ret[i] - '0'
应该是ret[i] + '0'
(数字转字符)- 导致
5 - 48 = -43
,负数索引,运行时错误
- 导致
- Bug1:
- 用时:踩坑后调试,最终AC
- 收获:大数乘法的核心思想掌握了
当天总体感受:
- 字符串题和链表、哈希表感觉不太一样,更像是在模拟手工运算
- 中心扩散法很巧妙,但需要理解回文的对称性
- 字符和数字的转换总是搞混方向
我的学习过程
第一阶段:字符串比较的理解
最长公共前缀这题,一开始卡在:
- 怎么遍历
vector<string>
? → 原来strs[i]
就是string
类型 - 怎么比较两个字符串的公共部分? → 逐位比较,
substr
截取 substr(0, i)
为什么是前 i 个? → 第二个参数是长度,不是结束位置
学了两种方法:
- 横向比较:两两比较,逐步缩小公共前缀
- 纵向比较:按列比较,所有字符串的同一位置
纵向比较时,行列关系搞混了好几次:
strs[i][j]
表示什么? → 第 i 个字符串的第 j 个字符- 外层循环是列号 i,内层循环是字符串编号 j
i >= strs[j].size()
判断越界(不是j < strs[i].size()
)
第二阶段:中心扩散法的掌握
最长回文子串是今天的核心题。
理解过程:
- 回文串有中心对称的特点
- 但中心有两种情况:
- 奇数长度:“aba” 的中心是 ‘b’
- 偶数长度:“abba” 的中心是两个 ‘b’ 之间
- 所以每个位置要扩散两次
难点突破:
- while 循环结束后,left 和 right 的位置理解错了
- 以为它们指向回文的边界,实际上已经越界或不匹配了
- 真正的回文是
[left+1, right-1]
- 长度:
(right-1) - (left+1) + 1 = right - left - 1
起始位置公式:begin = i - (curlen-1)/2
- i 是中心位置
- 向左偏移半个长度(向下取整)
第三阶段:大数运算的串联
今天做了两道大数运算:二进制求和、字符串相乘。
串联起来看:
- Day18:链表加法(十进制,逢十进一,指针遍历)
- Day20:二进制求和(二进制,逢二进一,索引遍历)
- Day20:字符串相乘(十进制,无进位相乘 + 统一处理进位)
核心思想都是:模拟手工运算,逐位处理,进位传递
二进制求和的大坑:
最开始写成 while(i || j || t)
和 if(i)
,以为和链表一样。
结果测试用例 a="11", b="1"
直接错了:
- 当 i=0 时,
if(i)
是 false,a[0]
没被处理 - 当 j=0 时,
if(j)
是 false,b[0]
没被处理
原因:
- 指针:
nullptr
是 false,非空是 true ✓ - 索引:
0
是 false,但 0 是合法索引 ✗
正确写法:
- 指针判断:
if(cur)
或while(cur)
- 索引判断:
if(i >= 0)
或while(i >= 0)
字符串相乘的理解:
这题最难,但掌握了很有成就感。
核心规律:num1[i] × num2[j]
影响 ret[i+j]
和 ret[i+j+1]
为什么?
num1[i]
从右往左数第m-1-i
位num2[j]
从右往左数第n-1-j
位- 相乘后影响从右往左数第
(m-1-i) + (n-1-j)
位 - 转成数组索引正好是
i+j
和i+j+1
分两步处理很巧妙:
- 先无进位相乘,直接累加到对应位置
- 最后统一从右往左处理进位
字符转换的陷阱:
今天在这里踩了两次坑:
- 字符→数字:
ch - '0'
(减法) - 数字→字符:
num + '0'
(加法)
方向容易搞反!第4题写成 ret[i] - '0'
导致负数索引。
字符串核心概念(我的理解)
中心扩散法
适用场景: 回文串问题
核心思想:
回文串具有中心对称的特点,从中心往两边扩散,左右字符相同。
关键点:
- 中心有两种情况:
- 奇数长度:中心是一个字符
- 偶数长度:中心是两个字符之间
- 需要对每个位置分别进行两次扩散
模板代码:
for(int i = 0; i < n; i++) {// 奇数长度int left = i, right = i;while(left >= 0 && right < n && s[left] == s[right]) {// 处理回文子串 s[left...right]left--;right++;}// 循环结束后,真正的回文是 [left+1, right-1]// 长度 = right - left - 1// 偶数长度left = i, right = i + 1;while(left >= 0 && right < n && s[left] == s[right]) {// 处理回文子串 s[left...right]left--;right++;}// 长度 = right - left - 1
}
注意事项:
- 循环结束时 left 和 right 已越界或不匹配
- 计算长度:
right - left - 1
- 计算起始位置:
begin = i - (len-1)/2
大数运算模拟
为什么需要大数运算?
int
最大约 21亿(10位)long long
最大约 9×10^18(19位)- 题目输入可能超过 19 位,任何内置类型都会溢出
核心思想:
模拟手工运算,逐位处理,进位传递
关键技巧:
-
字符与数字转换(重要!)
// 字符 → 数字(减法) char ch = '5'; int num = ch - '0'; // 53 - 48 = 5// 数字 → 字符(加法) int num = 5; char ch = num + '0'; // 5 + 48 = 53 = '5'
容易搞反!记住:字符变数字减,数字变字符加
-
进位处理
int t = 0; // 进位变量 for(int i = n-1; i >= 0; i--) { // 从右往左t += ret[i]; // 当前位 + 进位ret[i] = t % 10; // 保留个位t /= 10; // 十位及以上作为进位 }
-
索引判断 vs 指针判断
// 指针判断(可以直接判断) while(cur1 || cur2) {if(cur1) { ... } }// 索引判断(必须用 >= 0) while(i >= 0 || j >= 0) {if(i >= 0) { ... } // 不能写 if(i),因为 0 是合法索引 }
大数加法模板:
string ret;
int i = a.size()-1, j = b.size()-1;
int t = 0; // 进位while(i >= 0 || j >= 0 || t) {if(i >= 0) {t += a[i] - '0';i--;}if(j >= 0) {t += b[j] - '0';j--;}ret += (t % 10) + '0'; // 当前位t /= 10; // 进位
}reverse(ret.begin(), ret.end()); // 反转
return ret;
大数乘法模板:
int m = num1.size(), n = num2.size();
vector<int> ret(m + n, 0);// 步骤1:无进位相乘
for(int i = 0; i < m; i++) {for(int j = 0; j < n; j++) {ret[i+j+1] += (num1[i]-'0') * (num2[j]-'0'); // 注意 +=}
}// 步骤2:统一处理进位
int t = 0;
for(int i = ret.size()-1; i >= 0; i--) {t += ret[i];ret[i] = t % 10;t /= 10;
}// 步骤3:去除前导0,转字符串
string result;
int i = 0;
while(i < ret.size() && ret[i] == 0) i++;
for(; i < ret.size(); i++) {result += ret[i] + '0'; // 注意是 +
}
return result.empty() ? "0" : result;
字符串比较策略
横向比较(两两比较):
string ret = strs[0];
for(int i = 1; i < strs.size(); i++) {ret = findCommon(ret, strs[i]);
}
纵向比较(统一比较):
for(int i = 0; i < strs[0].size(); i++) {char ch = strs[0][i];for(int j = 1; j < strs.size(); j++) {if(i >= strs[j].size() || strs[j][i] != ch) {return strs[0].substr(0, i);}}
}
区别:
- 横向:每次处理两个字符串
- 纵向:每次处理所有字符串的同一列
典型题目分类
类型1:字符串比较与前缀
代表题目: LeetCode 14 - 最长公共前缀
特点:
- 需要遍历字符串数组
- 逐位比较字符
- 注意边界条件(长度不同)
核心技巧:
substr(start, length)
第二个参数是长度- 循环条件要同时检查多个边界
- 横向和纵向两种思路
易错点:
substr
参数理解错误- 边界检查不完整导致越界
- 行列索引搞混(纵向比较时)
类型2:回文串问题
代表题目: LeetCode 5 - 最长回文子串
特点:
- 利用回文的中心对称性质
- 中心扩散法
核心技巧:
- 枚举每个可能的中心
- 分别处理奇数和偶数长度
- 记录起始位置和最大长度
易错点:
- 只考虑奇数长度,漏掉偶数长度
- 循环结束后指针位置理解错误
- 起始位置计算公式不熟悉
模板:
int begin = 0, maxLen = 1;
for(int i = 0; i < n; i++) {// 奇数扩散int left = i, right = i;while(left >= 0 && right < n && s[left] == s[right]) {left--;right++;}int len1 = right - left - 1;// 偶数扩散left = i, right = i + 1;while(left >= 0 && right < n && s[left] == s[right]) {left--;right++;}int len2 = right - left - 1;// 更新最大值int curLen = max(len1, len2);if(curLen > maxLen) {maxLen = curLen;begin = i - (curLen-1)/2;}
}
return s.substr(begin, maxLen);
类型3:大数运算
代表题目:
- LeetCode 67 - 二进制求和(加法)
- LeetCode 43 - 字符串相乘(乘法)
特点:
- 数字太大,内置类型无法存储
- 用字符串模拟手工运算
- 需要处理进位
核心技巧:
大数加法:
- 从右往左遍历(低位到高位)
- 维护进位变量 t
- 索引判断用
i >= 0
,不能用if(i)
- 字符串从低位构建,最后反转
大数乘法:
- 结果数组长度:
m + n
num1[i] × num2[j]
影响ret[i+j]
和ret[i+j+1]
- 无进位相乘(累加
+=
)+ 统一处理进位 - 去除前导0
易错点:
- 索引判断陷阱:
if(i)
在 i=0 时跳过 - 赋值 vs 累加:字符串相乘必须用
+=
- 字符转换方向:减法变数字,加法变字符(容易反)
'0'
vs"0"
:'0'
(char) 用于转换和单字符比较"0"
(string) 用于字符串比较和返回值
字符转换速记:
字符 → 数字: 减去 '0'
数字 → 字符: 加上 '0'索引判断: >= 0
指针判断: 直接 if
我踩的坑总结
1. 索引判断陷阱(LeetCode 67)
错误代码:
while(i || j || t) {if(i) {t += a[i] - '0';i--;}
}
问题:
当 i=0 时,if(i)
判断为 false,导致 a[0]
没被处理。
测试用例:
输入: a = "11", b = "1"
预期: "100"
实际: "1"
原因:
混淆了指针判断和索引判断:
- 指针:
nullptr
是 false,非空是 true ✓ - 索引:
0
是 false,但 0 是合法索引 ✗
正确写法:
while(i >= 0 || j >= 0 || t) {if(i >= 0) {t += a[i] - '0';i--;}
}
教训:
- 指针可以直接
if(cur)
- 索引必须
if(i >= 0)
2. 累加 vs 赋值(LeetCode 43)
错误代码:
ret[i+j+1] = (num1[i]-'0') * (num2[j]-'0'); // ❌
问题:
同一个位置可能被多次写入,用 =
会覆盖之前的值。
例子:
num1 = "12", num2 = "34"i=0, j=1: ret[2] = 1*4 = 4
i=1, j=0: ret[2] = 2*3 = 6 // 覆盖了之前的4!正确应该是: ret[2] = 4 + 6 = 10
正确写法:
ret[i+j+1] += (num1[i]-'0') * (num2[j]-'0'); // ✓
3. 字符与数字转换方向(LeetCode 43)
错误代码:
result += ret[i] - '0'; // ❌
问题:
ret[i] = 5 (数字)
ret[i] - '0' = 5 - 48 = -43
result += -43 // 访问负数索引,运行时错误!
错误信息:
runtime error: index -42 out of bounds
正确写法:
result += ret[i] + '0'; // ✓
记忆口诀:
- 字符→数字:减
'0'
- 数字→字符:加
'0'
4. '0'
vs "0"
混淆
错误理解:
不清楚什么时候用单引号,什么时候用双引号。
正确区分:
写法 | 类型 | 使用场景 |
---|---|---|
'0' | char | 字符转换、单字符比较 |
"0" | string | 字符串比较、返回值 |
具体例子:
// 字符串比较 → "0"
if(num1 == "0") return "0";// 字符转数字 → '0'
ret[i+j+1] += (num1[i] - '0') * (num2[j] - '0');// 数字转字符 → '0'
result += ret[i] + '0';// 单字符比较 → '0'
if(s[i] == '0') { ... }
快速判断:看左边的类型
string
→ 用"0"
char
→ 用'0'
s[i]
返回char
→ 用'0'
5. 行列索引搞混(LeetCode 14 纵向比较)
错误理解:
strs[i][j]
中,i 和 j 分别代表什么?
正确理解:
列0 列1 列2
字符串0: f l o ← strs[0]
字符串1: f l o ← strs[1]
字符串2: f l i ← strs[2]
strs[j][i]
:第 j 个字符串的第 i 个字符- 外层循环 i:列号
- 内层循环 j:字符串编号
越界判断:
if(i >= strs[j].size() || strs[j][i] != ch) // ✓
不是:
if(j < strs[i].size()) // ❌ 行列反了
6. substr 参数混淆
错误理解:
以为 substr(start, end)
第二个参数是结束位置。
正确理解:
substr(start, length) // 第二个参数是长度!
例子:
string s = "flower";
s.substr(0, 4) // "flow"(从0开始取4个字符)
s.substr(0, 3) // "flo"(从0开始取3个字符)
常见错误:
// 想取前 i 个字符
return s.substr(0, i-1); // ❌ 只取了 i-1 个
return s.substr(0, i); // ✓ 正确
典型模板总结
模板1:中心扩散法(回文串)
class Solution {
public:string longestPalindrome(string s) {int n = s.size();int begin = 0, maxLen = 1;for(int i = 0; i < n; i++) {// 奇数长度int left = i, right = i;while(left >= 0 && right < n && s[left] == s[right]) {left--;right++;}int len1 = right - left - 1;// 偶数长度left = i;right = i + 1;while(left >= 0 && right < n && s[left] == s[right]) {left--;right++;}int len2 = right - left - 1;// 更新最大值int curLen = max(len1, len2);if(curLen > maxLen) {maxLen = curLen;begin = i - (curLen - 1) / 2;}}return s.substr(begin, maxLen);}
};
关键点:
- 分别处理奇数和偶数
- 长度:
right - left - 1
- 起始:
i - (len-1)/2
模板2:大数加法
class Solution {
public:string addBinary(string a, string b) {string ret;int i = a.size() - 1, j = b.size() - 1;int t = 0;while(i >= 0 || j >= 0 || t) {if(i >= 0) {t += a[i] - '0';i--;}if(j >= 0) {t += b[j] - '0';j--;}ret += (t % 2) + '0'; // 二进制用 %2t /= 2; // 二进制用 /2}reverse(ret.begin(), ret.end());return ret;}
};
关键点:
- 索引判断:
i >= 0
- 字符转数字:
- '0'
- 数字转字符:
+ '0'
- 最后反转结果
变体:
- 十进制:
t % 10
和t / 10
- 二进制:
t % 2
和t / 2
模板3:大数乘法
class Solution {
public:string multiply(string num1, string num2) {if(num1 == "0" || num2 == "0") return "0";int m = num1.size(), n = num2.size();vector<int> ret(m + n, 0);// 步骤1:无进位相乘for(int i = 0; i < m; i++) {for(int j = 0; j < n; j++) {ret[i+j+1] += (num1[i]-'0') * (num2[j]-'0');}}// 步骤2:统一处理进位int t = 0;for(int i = ret.size() - 1; i >= 0; i--) {t += ret[i];ret[i] = t % 10;t /= 10;}// 步骤3:去除前导0,转字符串string result;int i = 0;while(i < ret.size() && ret[i] == 0) i++;for(; i < ret.size(); i++) {result += ret[i] + '0';}return result.empty() ? "0" : result;}
};
关键点:
- 结果长度:
m + n
- 位置关系:
num1[i] × num2[j]
→ret[i+j+1]
- 必须用
+=
累加 - 两步处理:无进位相乘 + 统一处理进位
模板4:字符串比较(横向)
class Solution {
public:string longestCommonPrefix(vector<string>& strs) {string ret = strs[0];for(int i = 1; i < strs.size(); i++) {ret = findCommon(ret, strs[i]);}return ret;}string findCommon(string& s1, string& s2) {int i = 0;while(i < s1.size() && i < s2.size() && s1[i] == s2[i]) {i++;}return s1.substr(0, i);}
};
关键点:
- 两两比较,逐步缩小
- 边界检查:
i < s1.size() && i < s2.size()
substr(0, i)
取前 i 个
模板5:字符串比较(纵向)
class Solution {
public:string longestCommonPrefix(vector<string>& strs) {for(int i = 0; i < strs[0].size(); i++) {char ch = strs[0][i];for(int j = 1; j < strs.size(); j++) {if(i >= strs[j].size() || strs[j][i] != ch) {return strs[0].substr(0, i);}}}return strs[0];}
};
关键点:
- 按列比较
- i 是列号,j 是字符串编号
strs[j][i]
表示第 j 个字符串的第 i 个字符- 越界判断:
i >= strs[j].size()
我的理解
字符串题的本质
字符串题和之前的哈希表、链表不太一样。
哈希表:用空间换时间,快速查找
链表:指针操作,断链、连接
字符串:模拟手工运算,逐位处理
字符串题更像是在"翻译"现实中的算法过程:
- 比较两个字符串 → 逐位比较
- 找回文串 → 从中心扩散
- 大数相加 → 模拟竖式加法
- 大数相乘 → 模拟竖式乘法
中心扩散法的巧妙之处
最开始不理解为什么要"中心扩散",暴力枚举所有子串不行吗?
后来明白了:
- 暴力:O(n²) 枚举子串,O(n) 判断回文 → O(n³)
- 中心扩散:O(n) 枚举中心,O(n) 扩散 → O(n²)
而且中心扩散更直观,利用了回文的对称性质。
但有个细节:奇数和偶数长度要分开。
最开始只想到奇数长度(中心是一个字符),后来发现 “abba” 这种偶数长度的回文,中心是两个字符之间,不是某个字符。
大数运算的串联
今天做了二进制求和和字符串相乘,加上之前Day18的链表加法,发现它们的核心思想是一样的。
共同点:
- 从低位到高位处理
- 维护进位变量 t
- 当前位 = (累加值) % 进制
- 进位 = (累加值) / 进制
不同点:
- 链表用指针遍历,字符串用索引遍历
- 链表天然逆序,字符串要手动 reverse
- 十进制 %10 /10,二进制 %2 /2
字符串相乘的巧妙思想:
分两步处理:
- 无进位相乘(直接累加到对应位置)
- 统一处理进位(从右往左)
这样避免了复杂的竖式模拟,代码清晰很多。
索引判断 vs 指针判断
这个坑踩了才深刻理解。
指针:
ListNode* cur = head;
while(cur) { // cur 为 nullptr 时停止if(cur) { ... }
}
索引:
int i = s.size() - 1;
while(i >= 0) { // i < 0 时停止if(i >= 0) { ... }
}
为什么不能 if(i)
?
- C++ 中,0 被认为是 false
- 但对于数组索引,0 是合法的
if(i)
在 i=0 时会跳过,导致 bug
记忆方法:
- 指针有"空"的概念 → 可以直接判断
- 索引没有"空"的概念,0 是有效值 → 必须
>= 0
字符转换的方向
这个真的容易搞混。
记忆方法:
- 字符比数字"大"(ASCII码值)
- 要变成数字,需要"减小" → 减去
'0'
- 要变成字符,需要"增大" → 加上
'0'
速记口诀:
字符变数字:减去 '0'
数字变字符:加上 '0'
实际例子:
'5' - '0' = 53 - 48 = 5 // 字符5 变 数字5
5 + '0' = 5 + 48 = 53 // 数字5 变 字符5
后记
今天字符串专题做了4道题,从easy到medium都有涉及。
收获最大的:
- 中心扩散法:这个套路以后遇到回文串直接用
- 大数运算:和链表加法串联起来,形成了"进位处理"系列
- 索引判断陷阱:这个坑印象深刻,以后不会再犯
踩坑最多的:
- 字符与数字转换方向(踩了2次)
- 累加 vs 赋值(字符串相乘)
- 索引判断(和指针混淆)
时间分配:
- 第1题:边学边做,掌握两种方法
- 第2题:独立完成,理解中心扩散法
- 第3题:踩坑,调试索引判断
- 第4题:最难,花时间最多,但收获也最大
后续计划:
字符串专题还有很多经典题目,后面会继续补充:
- KMP算法相关
- 双指针类字符串题
- 字符串DP问题
- 更多回文串变形题
这次先把基础打牢,模板掌握了,后面的题目应该会更顺。
总结: 字符串题的核心是"模拟",把手工运算过程翻译成代码。中心扩散法和大数运算是两个重要模板,掌握后很多题都能套用。