LeetCode 双题:415. 字符串相加与 143. 重排链表

作者:逆境不可逃

技术永无止境

希望我的内容可以帮助到你!!!!

题目链接:415. 字符串相加143. 重排链表

行文结构参考:CSDN 参考文章

这次整理两道题。

第一道题把小学学过的竖式加法翻译成代码;第二道题需要重新排列链表,可以拆成找中点、反转后半段、交叉合并三个步骤。


一、415. 字符串相加

题目给出两个字符串形式的非负整数 num1num2,要求返回它们的和。

限制是:

  • 不能使用 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"

两个指针一开始分别指向 67

复制代码
    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 已经没有数字了,所以 y0 处理:

复制代码
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() 得到内容

如果 anschar[],下面这种写法得不到预期结果:

复制代码
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 插入 12 之间:

复制代码
head.next = head2;
head2.next = nxt;

得到:

复制代码
1 -> 6 -> 2 -> 3 -> 4

移动两个指针:

复制代码
head = nxt;
head2 = nxt2;

此时:

复制代码
head = 2
head2 = 5

第二次循环

5 插入 23 之间:

复制代码
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
最后反转结果

重排链表可以记成:

复制代码
快慢指针找中点
反转后半段
前后两部分交叉合并

两道题虽然一个处理字符串,一个处理链表,但思路很接近:先把最终操作拆成几个简单步骤,再用指针控制处理顺序。

相关推荐
2601_950760791 小时前
TNF-α:自身免疫疾病治疗的核心靶点与信号通路解析
人工智能·算法·蛋白
不会代码的小猴2 小时前
2. 了解Qt
开发语言·c++·笔记·qt·算法
比奇堡裤头村2 小时前
统计学习方法——支持向量机
算法·支持向量机·学习方法
程序喵大人4 小时前
【C++进阶】STL算法与函数对象 - 09 函数对象保存状态并复用规则
开发语言·c++·算法·stl·函数对象
致Great9 小时前
DeepSeek Harness插件开发实战教程:我让它自己写了一个 arXiv 搜索插件
算法
罗西的思考13 小时前
【Agent OS / AIOS】AOHP 深度解读:当 OS 开始为 Agent 而设计
人工智能·算法·机器学习
民乐团扒谱机14 小时前
【微实验】组合优化matlab实战(马科维茨投资模型):在收益与风险之间,寻找最优的人生配比
大数据·人工智能·算法·机器学习·数学建模·matlab·组合优化
Nil20814 小时前
leetcode 160相交链表
算法·leetcode·链表
迷途之人不知返15 小时前
算法系列2:滑动窗口
算法