文字比较多,但是耐下心来,看完绝对能懂。(我也是上课,做的笔记,整理归纳,再加上我的理解,总结的)
1. 题目
-
两个非空
链表
用来 表示两个非负整数
-
各自的位数是按照
逆序
的方式存储, 并且 每个节点只能存储一位
数字 -
如果我们将这两个数相加起来, 返回一个新的链表, 并且表示它们的和 (逆序)
-
假设除了数字 0 之外,这两个数都不会以 0 开头
2. 链表(⭐)
这道题的前置知识,链表,必须了解
-
一种
物理存储上
非连续、非顺序 的存储结构 -
由一系列节点组成,可以在运行时动态生成
-
每个结点包括两个部分:数据域;指针域
-
单向链表的代码实现(Java):
public class ListNode { int val; // 数据域 ListNode next;// 指针域(下一个节点的地址值) public ListNode() { } // 无参构造 public ListNode(int val) { this.val = val; } // 有参构造 }
-
注意:
-
代码中的数据域,类型为 int, 那是因为在这个算法体中,存储的是int类型。
-
在实际开发中,ListNode应该使用泛型
public class ListNode<T> { T val; ListNode<T> next; public ListNode() { }// 无参构造 public ListNode(T val) { this.val = val; }// 有参构造 public ListNode(T val, ListNode<T> next) { this.val = val; this.next = next; }// 全参构造 }
-
-
-
特点:
-
链表的数据存储,不要求连续空间,不限制容量,可在运行时动态生成
- 注:数组的存储空间,是一块儿连续的内存。且数组一旦创建,就固定了大小
-
数据的逻辑顺序通过指针链接次序实现
-
从链表的头节点,依次访问后面的节点(即单向链表)
- 双向链表,可以从尾节点,依次向前面的节点访问
- 双向链表,有两个指针域,一个指向前节点,一个指向后节点
-
在链表表头插入数据的时间复杂度是O(1)
- 在头结点前插入数据,只需要将节点的指向头结点,这么一个操作。所以时间复杂度为 O(1)
-
3. 解题思路
-
解法1 :分别将链表转换成数字,再相加,在将结果转换为 链表
-
解法2 :链表一一对应,将链表对应位置的数字相加
3.1 暴力解法
即解法1,因为最直接,所以往往这总方式被称为暴力解法
-
步骤:
-
遍历链表
,通过数学的思维(除or取模),将每一个节点的数字,拼凑成整数- 除:
101/10=10
,取商 - 取模:
101%10=1
,取余数
- 除:
-
对两个整数进行求和得到结果:sum
-
将 sum 按照数学思维 再转成链表
-
-
暴力解法的 边界和细节问题:
-
边界问题:
-
链表什么时候指向尾节点呢?
- 尾节点作为循环结束的标志:next == null
-
整数,在转化为链表时,什么时候结束?
- 即整数值value = 0 时,结束
-
-
细节问题:
- 链表存储为逆序,最高位其实是最后一个节点。
- 题目没有给出整数的范围:
- 题目中表示,链表的一个节点只存储一个数字,不会发生越界问题。
- 但是,这一条链表所表示的数值可以非常大,可能会超出数据类型的范围。
- Java提供了 BigInteger 、BigDecimal 等等,大数类型,来替代基本数据类型
- 若 int 溢出可以用long,long 溢出?
- 所以,当链表所表示的数值,溢出时。暴力解法,将不可使用
-
-
代码实现:
-
将链表转换为整数
public static long listToNumber(ListNode node) { long nodeOfValue = 0; // 初始化,节点所代表的整数值 int n = 0; // 位数,多少个0:0代表个位, 1代表十位, 以此类推 while (node != null) { // 判断是否为 尾节点 int pow = (int) Math.pow(10, n); //10的n次方,代表位数:1,10,100,... nodeOfValue += (long) node.val * pow; // 累加, 最终得到整个链表所表示的值 n++; node = node.next;// 将当前节点后移,得到下一个节点 } return nodeOfValue; }
-
将整数转换为链表
public static ListNode numberToList(long sum) { // 初始化一个新链表 ListNode headNode = new ListNode();// 头结点 ListNode curNode = headNode;// 当前节点 if (sum == 0) { // 边界判断 headNode = new ListNode(0); return headNode; } // 数字转成链表 while (sum > 0) { int val = (int) (sum % 10); // 每次取当前最低位 curNode.next = new ListNode(val); //插⼊入链表尾部 sum = sum / 10; // 移除最低位 curNode = curNode.next; //链表尾部指针移动 } return headNode.next; }
- 注意:
- 头结点,就代表整个链表。所以,返回值一般返回:
headNode.next
- 操作链表时,一般会创建
HeadNode
和CurrnetNode
?- 创建头结点:HeadNode,是用来指向该链表的起始位置,很重要。不然该链表就会丢失
- 创建当前节点:CurrnetNode,用来对链表进行操作
- 头结点和当前节点,最开始指向的是同一个节点。第一次对当前节点操作,实际上就是对头结点操作。
- 头结点,就代表整个链表。所以,返回值一般返回:
- 注意:
-
核心代码:
public ListNode addTwoNumbers(ListNode l1, ListNode l2) { // 得到两个链表所代表的整数 long value1 = listToNumber(l1); long value2 = listToNumber(l2); //数字相加 long sum = value1 + value2; // 得到结果 return numberToList(sum); }
-
3.3 数学思维解法
链表一一对应,将链表对应位置的数字相加
-
步骤:
-
遍历链表
- 和暴力解法不同的是,不是单个链表的遍历,而是同时遍历。
- 如果两个链表长度不一致的情况呢?以长的为准
-
对应节点的数值,进行相加
-
每个对应节点,相加的结果,直接插入到新链表的尾部
-
如果结果大于10,那么进位,加到下一个节点
-
-
-
边界和细节问题:
-
边界问题:
- 链表的边界条件:链表结尾时,next == null
-
细节问题:
- 两个链表长度不一致,短的链表高位视为 0
- 链表的高位发生进位,那么结果需要链表增加一个节点存放进位的数
-
-
代码:
public ListNode addTwoNumbers(ListNode l1, ListNode l2) { ListNode resHeadNode = new ListNode();// 实例化了结果链表的头结点 ListNode curNode = resHeadNode;// 结果链表的遍历指针 int carryDigit = 0; // 进位数 // 1. 遍历两个链表 while (l1 != null || l2 != null) { // 不等于null,表示还有节点可以遍历 // 获取当前节点的值,若没有则取0 int x = l1 != null ? l1.val : 0; int y = l2 != null ? l2.val : 0; // 2. 取到值后,对应节点位相加, 还要加上进位数 int sum = x + y + carryDigit; // 对求和结果进行 进位处理 carryDigit = sum / 10; // 得到进位数,若没有则为0 int num = sum % 10; // 得到节点中的数值 // 3. 得到相加的结果,将其插入新链表的尾部 curNode.next = new ListNode(num); curNode = curNode.next; // 指针后移,指向新插入的节点 // 4. 相加的两条链表的指针也要后移 l1 = (l1 == null) ? l1 : l1.next; l2 = (l2 == null) ? l2 : l2.next; } // 5. 遍历结束后,判断 carryDigit 看是否发生进位 if (carryDigit>0){ curNode.next = new ListNode(carryDigit); } return resHeadNode.next; }
总结步骤:
- 同时遍历两个链表 —— 构建结果链表
- 在循环中,首先取到当前节点的值
- 对应节点的两值相加,还要加上进位数,得到结果sum
- 对sum,进行进位数处理,得到新的进位数、待插入链表的值num
- 将num实例化为节点,并且插入到新链表的尾部
- 新链表的操作节点后移,指向插入到链表
- 相加的两条链表的指针也要后移,指向下一个 要相加的节点
- 依次循环,等待结束
- 遍历结束后,构建链表成功。但是,还要判断,进位数是否大于 0, 若大于0 那么就要发生进位
- 同时遍历两个链表 —— 构建结果链表