
作者:逆境不可逃
技术永无止境
希望我的内容可以帮助到你!!!!
题目链接:415. 字符串相加|143. 重排链表
行文结构参考:CSDN 参考文章
这次整理两道题。
第一道题把小学学过的竖式加法翻译成代码;第二道题需要重新排列链表,可以拆成找中点、反转后半段、交叉合并三个步骤。
一、415. 字符串相加

题目给出两个字符串形式的非负整数 num1 和 num2,要求返回它们的和。
限制是:
- 不能使用
BigInteger - 不能直接把整个字符串转换成整数
例如:
输入:num1 = "456", num2 = "77"
输出:"533"
1. 解题思路
这道题其实就是模拟竖式加法。
平时计算加法时,会从个位开始:
4 5 6
7 7
---------
5 3 3
所以我们分别用两个指针指向字符串的最后一位:
int i = num1.length() - 1;
int j = num2.length() - 1;
再用 c 保存进位:
int c = 0;
每次计算当前两位和进位的总和:
int t = x + y + c;
那么:
t % 10
是当前这一位需要写入的数字;
t / 10
是留给下一位的进位。
2. 完整代码
class Solution {
public String addStrings(String num1, String num2) {
int i = num1.length() - 1;
int j = num2.length() - 1;
int c = 0;
StringBuilder ans = new StringBuilder();
while (i >= 0 || j >= 0 || c != 0) {
int x = i >= 0 ? num1.charAt(i--) - '0' : 0;
int y = j >= 0 ? num2.charAt(j--) - '0' : 0;
int t = x + y + c;
ans.append(t % 10);
c = t / 10;
}
return ans.reverse().toString();
}
}
3. 代码过程
还是用这个例子:
num1 = "456"
num2 = "77"
两个指针一开始分别指向 6 和 7:
4 5 6
↑ i
7 7
↑ j
第一次计算:
6 + 7 + 0 = 13
当前位:
13 % 10 = 3
新的进位:
13 / 10 = 1
因此:
ans = "3"
c = 1
第二次计算:
5 + 7 + 1 = 13
继续写入 3,保留进位 1:
ans = "33"
c = 1
第三次计算时,num2 已经没有数字了,所以 y 按 0 处理:
4 + 0 + 1 = 5
此时:
ans = "335"
不过我们是从个位开始添加的,所以结果顺序是反的。最后执行:
ans.reverse()
得到:
533
4. 为什么短字符串缺少的位按 0 处理?
两个字符串的长度不一定相同。
例如:
num1 = "456"
num2 = "77"
计算到百位时,num2 已经没有数字了。这时候相当于:
4 + 0 + 进位
所以代码写成:
int x = i >= 0 ? num1.charAt(i--) - '0' : 0;
int y = j >= 0 ? num2.charAt(j--) - '0' : 0;
指针没有越界就读取数字,越界以后就使用 0。
5. 为什么循环条件还要判断进位?
循环条件是:
while (i >= 0 || j >= 0 || c != 0)
前两个条件很好理解:只要其中一个字符串还有数字,就应该继续计算。
c != 0 是为了处理最后一次进位。
例如:
99 + 1
计算过程是:
9 + 1 = 10
写 0,进 1
9 + 0 + 1 = 10
写 0,进 1
这时候两个指针都已经越界,但还剩一个进位 1。如果不判断 c != 0,这个 1 就会丢失,结果会变成 "00",而不是 "100"。
6. num1.charAt(i) - '0' 是什么意思?
charAt() 得到的是字符,例如 '6',并不是数字 6。
字符 '0' 到 '9' 的编码是连续的,因此:
'6' - '0'
结果就是:
6
所以这句:
num1.charAt(i) - '0'
可以把数字字符转换成对应的整数。
7. 注意点
进位变量不要重新声明
进位已经在循环外定义:
int c = 0;
循环中更新它时直接写:
c = t / 10;
不要再写成:
int c = t / 10;
否则会重新创建一个只在当前代码块中生效的局部变量,外面的 c 没有被更新。
t 已经包含进位
这里:
int t = x + y + c;
已经把进位加进去了,当前位直接取:
t % 10
不需要再加一次 c。
使用字符数组时要转换成字符
本题使用 StringBuilder 最方便:
ans.append(t % 10);
如果改用 char[] 保存结果,则要写成:
ans[k++] = (char) ('0' + t % 10);
因为字符数组里保存的是 '0'、'1' 这样的字符,不是整数。
字符数组不能直接调用 toString() 得到内容
如果 ans 是 char[],下面这种写法得不到预期结果:
return ans.toString();
应该使用:
return new String(ans);
不过这道题直接使用 StringBuilder,代码会简单很多。
8. 复杂度分析
设两个字符串的最大长度为 n。
- 时间复杂度:
O(n) - 空间复杂度:
O(n)
每一位只处理一次,结果字符串最多比原字符串多一位。
9. 一句话记忆
两个指针从字符串末尾开始:
当前位 = (x + y + 进位) % 10
新进位 = (x + y + 进位) / 10
最后把结果反转。
二、143. 重排链表

题目给出一个单链表:
L0 -> L1 -> L2 -> ... -> Ln
要求把它重新排列成:
L0 -> Ln -> L1 -> Ln-1 -> L2 -> Ln-2 -> ...
例如:
原链表:
1 -> 2 -> 3 -> 4 -> 5 -> 6
重排后:
1 -> 6 -> 2 -> 5 -> 3 -> 4
题目要求实际调整节点之间的连接,不能只交换节点中的值。
1. 解题思路
观察最终顺序:
1 -> 6 -> 2 -> 5 -> 3 -> 4
可以把它看成两部分交叉合并:
前半部分:1 -> 2 -> 3 -> 4
后半部分:6 -> 5 -> 4
因此整个过程分为三步:
找中点 -> 反转中点之后的链表 -> 交叉合并
2. 完整代码
class Solution {
public void reorderList(ListNode head) {
ListNode mid = middleNode(head);
ListNode head2 = reverseList(mid);
while (head2.next != null) {
ListNode nxt = head.next;
ListNode nxt2 = head2.next;
head.next = head2;
head2.next = nxt;
head = nxt;
head2 = nxt2;
}
}
// 找到链表的中间节点
private ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// 反转链表
private ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
}
下面用:
1 -> 2 -> 3 -> 4 -> 5 -> 6
完整走一遍。
三、第一步:找到中间节点
使用快慢指针:
private ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
慢指针每次走一步,快指针每次走两步。
开始时:
slow
↓
1 -> 2 -> 3 -> 4 -> 5 -> 6
↑
fast
第一次移动:
1 -> 2 -> 3 -> 4 -> 5 -> 6
↑ ↑
slow fast
第二次移动:
1 -> 2 -> 3 -> 4 -> 5 -> 6
↑ ↑
slow fast
第三次移动后,慢指针来到 4,快指针越过链表末尾:
1 -> 2 -> 3 | 4 -> 5 -> 6
↑
slow
所以:
mid = 4
对于长度为偶数的链表,这种写法找到的是两个中间节点中的后一个。
四、第二步:反转中点开始的链表
接下来执行:
ListNode head2 = reverseList(mid);
反转前:
4 -> 5 -> 6
反转后:
6 -> 5 -> 4
此时可以理解为:
head:
1 -> 2 -> 3 -> 4
head2:
6 -> 5 -> 4
这里没有提前把前半段和后半段断开,所以两个链表最后都会到达节点 4。
这也是后面使用:
while (head2.next != null)
而不是一直处理到 head2 == null 的原因。
五、链表反转是怎么完成的?
反转代码如下:
private ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
三个指针的作用分别是:
pre:已经反转好的部分
cur:当前正在处理的节点
nxt:提前保存下一个节点
为什么必须先保存 nxt?
因为执行:
cur.next = pre;
以后,cur 原本指向下一个节点的连接就被改变了。如果没有提前保存,后面的链表就找不到了。
每轮的顺序是:
ListNode nxt = cur.next; // 保存后面
cur.next = pre; // 当前节点反向连接
pre = cur; // pre 向前移动
cur = nxt; // cur 继续处理下一个节点
反转结束后,pre 就是新链表的头节点。
六、第三步:交叉合并两个链表
当前有两个链表:
head:
1 -> 2 -> 3 -> 4
head2:
6 -> 5 -> 4
需要把它们交叉连接成:
1 -> 6 -> 2 -> 5 -> 3 -> 4
合并代码:
while (head2.next != null) {
ListNode nxt = head.next;
ListNode nxt2 = head2.next;
head.next = head2;
head2.next = nxt;
head = nxt;
head2 = nxt2;
}
第一次循环
当前:
head = 1
head2 = 6
先保存两个指针原来的下一个节点:
ListNode nxt = head.next; // nxt = 2
ListNode nxt2 = head2.next; // nxt2 = 5
然后把 6 插入 1 和 2 之间:
head.next = head2;
head2.next = nxt;
得到:
1 -> 6 -> 2 -> 3 -> 4
移动两个指针:
head = nxt;
head2 = nxt2;
此时:
head = 2
head2 = 5
第二次循环
把 5 插入 2 和 3 之间:
1 -> 6 -> 2 -> 5 -> 3 -> 4
移动后:
head = 3
head2 = 4
此时:
head2.next == null
循环结束,最终结果是:
1 -> 6 -> 2 -> 5 -> 3 -> 4
七、为什么循环条件是 head2.next != null?
这段代码直接从 mid 开始反转,没有先把两部分断开。
对于:
1 -> 2 -> 3 -> 4 -> 5 -> 6
找到的中点是 4。反转后,两部分都包含节点 4:
1 -> 2 -> 3 -> 4
6 -> 5 -> 4
节点 4 是最后的公共节点,它本来就应该留在最终链表的末尾,不需要再插入一次。
因此循环写成:
while (head2.next != null)
当 head2 到达最后的公共节点时就停止,可以避免重复连接造成环。
这条判断要和当前的"从 mid 直接反转"配套理解,不能随意换成其他循环条件。
八、奇数长度也能处理吗?
可以。
例如:
1 -> 2 -> 3 -> 4 -> 5
中点是:
1 -> 2 | 3 -> 4 -> 5
↑
mid
从 3 开始反转:
5 -> 4 -> 3
交叉合并:
1 -> 5 -> 2 -> 4 -> 3
最后的中间节点 3 自然留在末尾,不需要单独处理。
九、注意点
合并前要先保存后继节点
下面两句必须放在修改 next 之前:
ListNode nxt = head.next;
ListNode nxt2 = head2.next;
如果先修改连接,再去寻找原来的下一个节点,链表结构已经发生变化,很容易丢失后续节点。
快指针要同时判断自己和下一个节点
循环条件写成:
while (fast != null && fast.next != null)
因为循环内部会执行:
fast = fast.next.next;
如果不检查 fast.next,可能出现空指针异常。
这里不需要创建新节点
题目要求调整原链表节点的顺序。这种方法只修改节点的 next 指针,没有额外创建链表,因此空间开销是常数级。
head2.next != null 与当前写法绑定
有些重排链表的写法会先断开前后两部分,然后反转第二部分。那种写法的合并条件通常不同。
当前代码没有主动断链,而且从 mid 开始反转,所以才使用:
while (head2.next != null)
理解了这一点,就不容易把不同版本的代码混在一起。
十、复杂度分析
415. 字符串相加
- 时间复杂度:
O(max(m, n)) - 空间复杂度:
O(max(m, n))
143. 重排链表
找中点、反转链表和交叉合并都会遍历链表,总时间复杂度为:
O(n)
算法只使用有限个指针变量,因此额外空间复杂度为:
O(1)
十一、总结
字符串相加模拟的是竖式加法:
从右往左取数字
当前位写 sum % 10
进位更新为 sum / 10
最后反转结果
重排链表可以记成:
快慢指针找中点
反转后半段
前后两部分交叉合并
两道题虽然一个处理字符串,一个处理链表,但思路很接近:先把最终操作拆成几个简单步骤,再用指针控制处理顺序。
