【算法】【链表】给单链表加一--通俗讲解

67 阅读6分钟

一、题目是啥?一句话说清

给定一个单链表,每个节点代表一个数字位,链表头是最高位,表示一个非负整数。将这个整数加1,并返回结果链表。

示例:

  • 输入:1 -> 2 -> 3 表示数字123,加1后变成124,输出1 -> 2 -> 4
  • 输入:9 -> 9 表示99,加1后变成100,输出1 -> 0 -> 0

二、解题核心

从最低位开始加1,处理进位问题。由于链表头是最高位,我们需要反向遍历链表,但单链表只能正向遍历,所以可以使用递归或反转链表的方法来模拟从低位到高位的操作。

关键点:处理进位,特别是当所有位都是9时,需要添加新节点作为最高位。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 反向遍历链表

  • 是什么:加1操作从最低位开始,所以我们需要先访问链表的最后一个节点,然后向前处理进位。
  • 为什么重要:因为进位是从低位向高位传播的,如果不反向遍历,无法正确处理进位。例如,数字999加1,进位需要从最低位逐步传递到最高位。

2. 处理进位

  • 是什么:当某一位加1后等于10时,产生进位,当前位变为0,进位1加到更高位。
  • 为什么重要:如果不处理进位,结果会错误。特别是连续进位的情况,如999加1变成1000,需要连续处理多个进位。

3. 可能添加新节点

  • 是什么:如果最高位有进位,需要在链表头部添加一个新节点(值为1)。
  • 为什么重要:否则,结果会缺少最高位,例如99加1应为100,如果没有新节点,链表只能表示00。

四、解法步骤(通俗理解版本)

  1. 递归到链表末尾:通过递归函数先到达链表最后一个节点,模拟反向遍历。
  2. 从后往前加1:在递归返回的过程中,对每个节点加上进位值,并计算新的进位。
  3. 检查最高位进位:如果递归完成后还有进位,则在链表头部添加一个新节点。

这就像我们做加法竖式时从右往左计算一样,递归帮我们实现了“从右往左”的效果。

五、C++代码实现(附详细注释)

#include <iostream>
using namespace std;

// 链表节点定义
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    ListNode* plusOne(ListNode* head) {
        int carry = helper(head); // 递归处理链表,返回进位
        if (carry == 1) { // 如果还有进位,添加新节点
            ListNode* newHead = new ListNode(1);
            newHead->next = head;
            return newHead;
        }
        return head;
    }

    // 递归函数:返回进位值
    int helper(ListNode* node) {
        if (node == nullptr) return 1; // 到达链表末尾,返回1表示加1
        int carry = helper(node->next); // 递归到下一个节点,获取进位
        int sum = node->val + carry; // 当前节点值加上进位
        node->val = sum % 10; // 更新当前节点值
        return sum / 10; // 返回新的进位
    }
};

// 辅助函数:打印链表
void printList(ListNode* head) {
    while (head != nullptr) {
        cout << head->val;
        head = head->next;
    }
    cout << endl;
}

// 测试代码
int main() {
    Solution solution;
    
    // 示例1: 1->2->3
    ListNode* head1 = new ListNode(1);
    head1->next = new ListNode(2);
    head1->next->next = new ListNode(3);
    cout << "输入: ";
    printList(head1);
    ListNode* result1 = solution.plusOne(head1);
    cout << "输出: ";
    printList(result1); // 期望输出: 124

    // 示例2: 9->9
    ListNode* head2 = new ListNode(9);
    head2->next = new ListNode(9);
    cout << "输入: ";
    printList(head2);
    ListNode* result2 = solution.plusOne(head2);
    cout << "输出: ";
    printList(result2); // 期望输出: 100

    return 0;
}

六、时间空间复杂度

  • 时间复杂度:O(n),其中n是链表长度。递归遍历链表一次。
  • 空间复杂度:O(n),由于递归调用栈的深度为n,需要O(n)的栈空间。

七、注意事项

  • 递归深度:如果链表非常长(例如数万节点),递归可能导致栈溢出。在这种情况下,可以使用迭代方法(如反转链表)来避免递归。
  • 进位处理:确保在递归返回时正确更新每个节点的值,并传递进位。
  • 链表修改:算法直接修改了原始链表。如果要求不修改原链表,需要先复制链表。

八、替代方法(反转链表)

如果不希望使用递归,可以反转链表,然后从头部(原最低位)开始加1和处理进位,最后再反转回来。这种方法的空间复杂度为O(1),但需要编写反转链表的代码。

示例步骤:

  1. 反转链表,使头节点成为最低位。
  2. 遍历反转后的链表,加1并处理进位。
  3. 如果最后还有进位,添加新节点。
  4. 再次反转链表,恢复顺序。

代码略,但这种方法更节省空间,适合长链表。

总结

理解此题的关键在于模拟从低位到高位的加法过程,并正确处理进位。递归方法代码简洁,但需要注意栈空间;反转链表方法空间高效,但代码稍复杂。根据实际情况选择合适的方法。


算法通俗讲解推荐阅读**
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解
【算法】【链表】148.排序链表--通俗讲解
【算法】【链表】160.相交链表--通俗讲解
【算法】【链表】203.移除链表元素--通俗讲解
【算法】【链表】206.反转链表--通俗讲解
【算法】234.回文链表--通俗讲解
【算法】【链表】237.删除链表中的节点--通俗讲解


关注公众号,获取更多底层机制/ 算法通俗讲解干货!