题目:Add Two Numbers 两数相加
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)输出:7 -> 0 -> 8原因:342 + 465 = 807
链表类:ListNode
class ListNode {int val;ListNode next;ListNode(int x) { val = x; }public static ListNode getLinkedNode(Integer ... nums) {LinkedList<Integer> linkedList = new LinkedList();Collections.addAll(linkedList, nums);return getLinkedNode(linkedList);}public static ListNode getLinkedNode(List<Integer> nums) {Integer num = nums.get(0);ListNode listNode = new ListNode(num);nums.remove(0);if (nums.size() != 0) {listNode.next = getLinkedNode(nums);}return listNode;}}
为了方便,我写了两个工具方法用来构造入参
ListNode a = ListNode.getLinkedNode(new Integer[]{2, 4, 3});ListNode b = ListNode.getLinkedNode(new Integer[]{5, 6, 4});
思路分析:
链表是以逆序存储数字,这大大方便了我们相加,因为加法运算也是从个位开始的
我们按照顺序遍历两个链表,对每一位进行相加,同时我们需要保存进位,将运算结果存入新的链表中,如图
遍历链表可以使用迭代,也可以使用递归,我们首先考虑性能较好的迭代法。通过创建一个head头来存放新的链表,初始化一个进位器add,创建current指针指向head,每次迭代完之后让current = current.next,同时也对l1和l2 进行同样的操作,这样三者迭代的进度一致,如上图所示,具体实现如下
Java解法一:指针迭代
public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {// 创建一个表头,用来存放接下里的链表ListNode head = new ListNode(0);// 创建一个指针Node,初始指向head,循环一次后会指向nextNode,以此延长head长度ListNode current = head;// 设置一个进位器int add = 0;// 如果next都等于null,遍历完毕跳出循环结束while(l1 != null || l2 != null) {int value1 = l1 == null ? 0 : l1.val ;int value2 = l2 == null ? 0 : l2.val ;int sum = value1 + value2 + add;// 利用int的机制保留进位add = sum/10;// 将当前位的运算结果存入新的链表中ListNode newNode = new ListNode(sum%10);current.next = newNode;// 如果不等于null,更新指针的值指向nextif (l1 != null) l1 = l1.next;if (l2 != null) l2 = l2.next;// 更新current指针current = current.next;}// 如果进位器还有值,那么创建一个新的Node插入队尾if (add == 1) {current.next = new ListNode(add);}return head.next;}
复杂度分析:
- 时间复杂度:O(max(m, n)),假设 m 和 n 分别表示 l1 和 l2 的长度,上面的算法最多重复max(m, n)次。
- 空间复杂度:O(max(m, n)), 新列表的长度最多为max(m,n) + 1。
LeetCode用时:
非会员LeetCode执行这段代码有点慢,实际运行时间其实只有2ms
Java解法二:递归(占用栈空间)
迭代能搞定的事情,我们递归也能做到,而且代码更加简洁。
但是递归会占用栈内的大量空间,所以不推荐public static ListNode addTwoNumbers(ListNode l1, ListNode l2, int add) {if (l1 == null && l2 == null && add == 0) return null;int sum = (l1 != null ? l1.val : 0) + (l2 != null ? l2.val : 0) + add;ListNode node = new ListNode(sum % 10);node.next = addTwoNumbers(l1 != null ? l1.next : null, l2 != null ? l2.next : null, sum / 10);return node;}
复杂度分析:
同上LeetCode用时:
LeetCode 不支持改变方法的入参
java解法三:2次迭代(性能较差)
先遍历一次两个链表,将数据存在hashmap中,然后再来遍历生成新的链表
public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {// 依次遍历两个链表,利用map储存他们的值再来计算HashMap<Integer, Integer> map1 = new HashMap<>();HashMap<Integer, Integer> map2 = new HashMap<>();int index = 1;// 如果next都等于null,遍历完毕跳出循环结束while(l1 != null || l2 != null) {int value1 = l1 == null ? 0 : l1.val ;int value2 = l2 == null ? 0 : l2.val ;// 如果不等于null,储存当前值if (l1 != null) {map1.put(index, value1);l1 = l1.next;}if (l2 != null) {l2 = l2.next;map2.put(index, value2);}index ++;}// 获取最大长度int max = Math.max(map1.size(),map2.size());ListNode head = new ListNode(0);ListNode current = head;int add = 0;// 从map尾部遍历,利用头插法生成新的链表for (int i = 1;i <= max;i++){Integer integer1 = map1.get(i) == null ? 0 : map1.get(i);Integer integer2 = map2.get(i) == null ? 0 : map2.get(i);int sum = integer1 + integer2 + add;add = sum/10;// 尾插法ListNode newHead = new ListNode(sum % 10);current.next = newHead;current = current.next;}if (add == 1) {current.next = new ListNode(add);}return head.next;}
复杂度分析:
- 时间复杂度:O(2 max(m, n)),假设 m 和 n 分别表示 l1 和 l2 的长度,上面的算法最多重复2max(m, n)次。
- 空间复杂度:O(max(m, n)), 新列表的长度最多为max(m,n) + 1。
LeetCode用时:
时间多了一倍
拓展:如果链表中的数字不是按逆序存储而是正序呢?
输入:(3 -> 4 -> 2) + (4 -> 6 -> 5)输出:8 -> 0 -> 7原因:342 + 465 = 807
拓展解法一:逆序迭代(头插法)
按照解法三的思路,先遍历一遍保存两个链表的所有值,然后通过头插法,从尾部向前遍历得到新链表
这里和解法三的区别在于:
解法三是正向遍历,使用了尾插法构建新链表
这里是反向遍历,使用了头插法构建新联表
这里直接遍历或者使用递归都十分繁琐,不做考虑
public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {// 依次遍历两个链表,利用map储存他们的值再来计算HashMap<Integer, Integer> map1 = new HashMap<>();HashMap<Integer, Integer> map2 = new HashMap<>();int index = 1;// 如果next都等于null,遍历完毕跳出循环结束while(l1 != null || l2 != null) {int value1 = l1 == null ? 0 : l1.val ;int value2 = l2 == null ? 0 : l2.val ;// 如果不等于null,储存当前值if (l1 != null) {map1.put(index, value1);l1 = l1.next;}if (l2 != null) {l2 = l2.next;map2.put(index, value2);}index ++;}// 获取最大长度int max = Math.max(map1.size(),map2.size());// 获取长度差值int abs = Math.abs(map1.size()-map2.size());ListNode current = new ListNode(0);int add = 0;int abs1 = 0;int abs2 = 0;// 从map尾部遍历,利用头插法生成新的链表for (int i = max;i >=abs;i--){Integer integer1 = map1.get(i);Integer integer2 = map2.get(i);// 由于长度差的存在,可能出现取出为null的情况,补上下标差abs// l2 比 l1长时会改变abs1 = integer1 == null ? abs : 0;// l1 比 l2长时会改变abs2 = integer2 == null ? abs : 0;integer1 = map1.get(i-abs) == null ? 0 :map1.get(i-abs);integer2 = map2.get(i-abs) == null ? 0 :map2.get(i-abs);int sum = integer1 + integer2 + add;add = sum/10;current.val = sum % 10;// 头插法ListNode newHead = new ListNode(0);newHead.next = current;current = newHead;}return current.next;}
复杂度分析:
- 时间复杂度:O(max(m, n) +min(m, n) ),假设 m 和 n 分别表示 l1 和 l2 的长度,上面的算法最多重复max(m, n) +min(m, n) 次。
- 空间复杂度:O(max(m, n)), 新列表的长度最多为max(m,n) + 1。
