算法小白刷力扣 2 - 两数相加

本文讲述了作者在解决LeetCode题目“AddTwoNumbers”时,从错误的解法(先将链表正序化再相加)到正确解法(直接链表相加并处理进位)的过程,强调了算法思维的重要性。
摘要由CSDN通过智能技术生成

1. 题目描述

原题链接:https://leetcode.cn/problems/add-two-numbers/


给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:
在这里插入图片描述
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807

示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]

示例 3
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

提示
每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零

1. 我的解法

先说一下,我的解法是有问题的。看了力扣官方题解后,我才发现我想复杂了,可以说是小学数知识没有活学活用啊,不知道加减法的实质。我想两个逆序数相加求和,怎么着都得把它们先翻转成正序再求和吧,然后再把结果添加到逆序添加到链表中就行了。代码如下:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        // 遍历第一个链表并正序计算出第1个数
        int i1 = 0;
        int t = 1;
        for(ListNode n = l1; n != null; n = n.next) {
            i1 += n.val * t;
            t *= 10;
        }

        // 遍历第一个链表并正序计算出第2个数
        int i2 =0;
        t = 1;
        for(ListNode n = l2; n != null; n = n.next) {
            i2 += n.val * t;
            t *= 10;
        }

        // 求和
        int num = i1 + i2;
        if (num == 0) {
            return new ListNode(0);
        }

        // 此时的和值为正序,逆序取出各位数字插入结果链表中
        ListNode sum = null;
        ListNode current = null;
        while(num != 0) {
            // 取出最低位数字
            int i = num % 10;
            // 去掉已经取出的最低位
            num /= 10;
            
            // 遍历插入到链尾
            if (sum == null) {
                current = sum = new ListNode(i);
            } else {
                current.next = new ListNode(i);
                current = current.next;
            }
        }

        return sum;
    }
}

本来我自认为我的解法很完美,三个示例的测试用例通过了两个,一个因为没有考虑到 0 的情况没通过,修改之后就通过了,但是当我提交后最终结果却是有问题的。
在这里插入图片描述
在线调试了一下,发现是因为报错的那个用例的一个数 l2 = 9999999991 太大超出 Java int 能表示的最大范围,所以最后输出变成一个很奇怪的数。好家伙,这用例是够刁钻的,防着我呢。不过也罢,用例能覆盖大多数场景也不错了,毕竟算法小白。

在解析正确的解法之前先先记录一下我的解法里面的几个编程的知识点:

  • 如何从一个逆序存储的单向链表中得到正序的整数?
    比如,将示例 1 中的链表2->4->3转为342,将5->6->4转为465。可以这样想,一个整数拆分为单个的数字,其实对应的是每一位上的数,342的百、十、个位分别为3、4、2,即342 = 3 * 100 + 4 * 10 + 2 * 1,也即342 = 2 * 1 + 4 * 10 + 3 * 100,显然,将链表2->4->3的每一位遍历出来分别乘以相应 10 的 n 次幂然后相加即可得到正序整数,亦即2 * 1 + 4 * 10 + 3 * 100 = 342465也是同样的道理。用代码实现就是前面两个for 循环,分别得到 i1 = 342,i2 = 465。
    正常来说,如果能想到这里,就能想到两数正序相加和逆序时的相加其实质是一样的,都需要从个位、十位、百位等依次相加,而逆序的数又正好是个、十、百位这样排列的数,所以可以直接依次相加而不用转成正序的数,这也是官方提供的正确的解决思路,但奈何被自己惯性思维所限,根本没往这方面想,一心要按从小用到大的正序数从个位往前进位加,所以说固定思维害死人,而学习算法有助于思维的培养。

  • 如何将一个整数按从低到高位取出各位数字?
    得到两个正序的数后将其相加得到和值,例如,342 + 465 = 807。按我的思路,我需要将 807 的个、十、百位数字通过在链尾添加节点保存到新的链表中,从而实现题目要求的将和值逆序保存在链表中。那么怎么从 807 中分别取出 7、0、8 呢?学过计算机的都知道,将一个十进制数转为二进制数可以用除法取模实现,那要从一个十进制数中取出每一位也是这样。将除数换成 10,连续相除每次取模即得到从个位到最高位的每位数字,这个过程可用短除法来演示。例如,1234不断对10`取模结果如下:
    在这里插入图片描述
    用代码实现如下:
    在这里插入图片描述

  • 如何从链表尾部插入新的节点?
    我的解法中最后取模得到的各位数字需要插入到一个新链表中。由于是将一组数字保存到一个新的链表中,因此需要遍历这组数字,就相当于遍历一个数组,将数组中的数字插到链表尾部。为此,首先肯定要有一个指向链表头节点的引用 sum,和一个向链表尾部移动的节点引用 current:
    在这里插入图片描述
    如果是将一个节点插入到一个已有的链表尾部呢?即如何向单向链表 LinkedList 尾部插入新节点?单向链表肯定要维护一个头结点,那就是从头结点开始向后遍历找到尾节点,然后将新节点插入到尾节点之后:
    在这里插入图片描述

3. 正确解法

先贴代码:

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    	// 头结点、尾结点
        ListNode head = null, tail = null;
        // 两数相加的进位值
        int carry = 0;
        while (l1 != null || l2 != null) {
        	// 从链表取数,如果某一个链表节点较短而无对应值则为 0
            int n1 = l1 != null ? l1.val : 0;
            int n2 = l2 != null ? l2.val : 0;
            
            // 下面是核心逻辑:
            // 和等于对应位相加,再加上一次计算的进位值 1
            int sum = n1 + n2 + carry;
            // 相加后对应位的值等于 和对10取模,
            // 例如 (8 + 5) % 10 = 3,对应位的值是 3,但同时需要向前进一位,carry 需要加 1,也即下面的 carry = sum / 10
            if (head == null) {
                head = tail = new ListNode(sum % 10);
            } else {
                tail.next = new ListNode(sum % 10);
                tail = tail.next;
            }
            // 进位值:如果有,只可能为 1
            carry = sum / 10;
            
            // 指针移动,遍历链表而已,非核心逻辑
            if (l1 != null) {
                l1 = l1.next;
            }
            if (l2 != null) {
                l2 = l2.next;
            }
        }
        // 注意,这一步很难想到,如果最后一次相加后有进位,则需要新增一个节点存进位值 1
        // 例如 逆序 987 + 333 = 221,1
        if (carry > 0) {
            tail.next = new ListNode(carry);
        }
        return head;
    }
}

咱就是说,上过小学的人都知道,两数相加,是从个位相加依次往前相加,大于 10 了就向前进一位。这个题,人家已经通过链表把个位放到前面来了,直接加就行了嘛,但是怎么就就没往这方面想,是义务教育填鸭中毒太深,还是咱脑瓜子太笨啊。
看吧,代码多么简单。由于 l1 和 l2 的长度未知,不知道哪个更长,所以可以两个一起遍历,哪个先遍历完,对应到另一个链表节点的值就为 0 呗。比如逆序456 + 5678就等于4560 + 5678,这里也提供了一种同时遍历多个链表的思路,以后说不定能用到。核心逻辑是相加后每一位上的值和当有进位时的处理,看注释!

参考

文心一言

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值