一、解题前置知识------链表操作(可跳过)
1. 什么是链表
链表是一种线性数据结构,它由一系列节点组成,每个节点包含两部分:
- 数据 (data): 存储实际的值。
- 指针 (next): 指向下一个节点,表示链表中下一个元素的位置。
与数组不同,链表中的元素不必存储在连续的内存位置中。 这使得链表在插入和删除元素时更加灵活,因为不需要移动大量的元素。
2. JavaScript 中的链表实现
应题目要求,我们使用函数的方式来创建链表,链表格式如下
js
function ListNode(val, next) {
this.val = (val===undefined ? 0 : val)
this.next = (next===undefined ? null : next)
}
3. 链表的基本操作
以下是一些常见的链表操作,以及它们在 JavaScript 中的实现方法:
-
创建链表:初始化头节点。
csharplet head = null; // 空链表为何let head = null 为一个空链表?这是一个很有意思的问题
笔者认为这里的head是不是空链表主要是看head的使用场景,这里是链表的场景所以称之为空链表。
let head = null;表示空链表的原因是:1.
head是链表的入口点。2.
null表示"没有"或"不存在"。3.当
head为null时,链表没有起始节点,因此被认为是空的。 -
在链表头部添加节点 (prepend):
inifunction prepend(head, val) { const newNode = new ListNode(val); newNode.next = head; return newNode; } // 示例: head = prepend(head, 1); head = prepend(head, 2);1.const newNode = new ListNode(val);这行代码使用
ListNode函数创建一个新的节点。新节点的val属性被设置为传入的val值,而next属性默认初始化为null(如果ListNode函数的定义是像你提供的那样)。 此刻,我们创建了一个孤立的节点,它还没有连接到链表上。2.newNode.next = head;这行代码将新创建的节点 (
newNode) 的next指针设置为当前的head。 简而言之,newNode的next指针现在指向链表的第一个节点 (原来的 head 所指向的节点)。 这实际上将newNode放在了链表的 "前面"。3.return newNode;这行代码返回
newNode。 在调用prepend函数时,我们需要更新head变量,让它指向这个新的头节点。 这就是为什么在调用时我们使用head = prepend(head, 1);这样的语句。 -
在链表尾部添加节点 (append):
inifunction append(head, val) { const newNode = new ListNode(val); if (!head) { return newNode; } let current = head; while (current.next) { current = current.next; } current.next = newNode; return head; } // 示例: head = append(head, 3);1.const newNode = new ListNode(val);创建一个新的节点,其
val属性被设置为传入的val值,next属性默认为null。2.if (!head) { return newNode; }这是一个重要的边界情况处理 。 如果
head是null,这意味着链表当前是空的。 在这种情况下,新节点newNode将成为链表的第一个(也是唯一一个)节点,因此函数直接返回newNode作为新的head。3.let current = head;如果链表不为空,我们就需要找到链表的最后一个节点。
current变量用于从链表的头部开始遍历。4.while (current.next) { current = current.next; }这是一个循环,用于遍历链表,直到找到最后一个节点。
current.next检查当前节点的next指针是否为null。 如果current.next不是null,说明current不是最后一个节点,所以我们将current移动到下一个节点current = current.next。 当循环结束时,current将指向链表的最后一个节点(即current.next是null的节点)。5.current.next = newNode;这是添加新节点的核心步骤。 将最后一个节点(
current)的next指针指向新创建的节点newNode。 这样,newNode就成为了链表的新的最后一个节点。6.return head;函数返回
head。 注意: 只有在链表一开始为空的时候,head才会被更新!如果链表一开始不为空,我们只是在尾部添加了一个节点,head仍然指向原来的头节点。 -
插入节点 (insert):
inifunction insert(head, val, position) { if (position < 0) { console.log("Invalid position"); return head; } if (position === 0) { return prepend(head, val); } const newNode = new ListNode(val); let current = head; let previous = null; let index = 0; while (current && index < position) { previous = current; current = current.next; index++; } if (index < position) { console.log("Position out of range"); return head; } newNode.next = current; previous.next = newNode; return head; } // 示例: head = insert(head, 10, 1);1.校验位置有效性 (`position < 0`)函数首先检查
position是否小于 0。如果是,说明插入位置无效,函数输出错误信息并返回原始的head,不做任何修改。这是处理一种异常情况。2.处理在头部插入的情况 (`position === 0`)如果
position等于 0,意味着需要在链表的头部插入新节点。 此时,函数直接调用我们之前讨论过的prepend函数来完成插入操作,并返回新的head。 这样做的好处是代码复用,同时简化了insert函数的逻辑。3.创建新节点 (`const newNode = new ListNode(val);`)创建一个值为
val的新节点。 这个节点将要被插入到链表中的指定位置。``4.初始化
current,previous和 `index```current:指向当前正在遍历的节点,初始化为head。previous:指向current节点的前一个节点,初始化为null。index:记录当前遍历的节点的位置,初始化为 0。5.遍历链表到目标位置 (`while (current && index < position)`)这个
while循环遍历链表,直到到达要插入的位置。 注意循环的两个条件:(1)current:确保current不为null,即没有到达链表的末尾。(2)index < position:确保还没有到达目标位置。在循环内部:
previous = current;:将previous更新为current。current = current.next;:将current移动到下一个节点。index++;:增加index的计数。循环结束后,
current将指向目标位置的节点(或者null,如果目标位置超出了链表的范围),previous将指向目标位置的前一个节点。6.检查位置是否超出范围 (`index < position`)如果在遍历结束后,
index仍然小于position,这意味着目标位置超出了链表的范围(例如,链表只有 3 个节点,但要插入到位置 5)。 在这种情况下,函数输出错误信息并返回原始的head,不做任何修改。7.插入新节点newNode.next = current;:将新节点的next指针指向current。这意味着newNode将插入到current之前。previous.next = newNode;:将previous节点的next指针指向newNode。这意味着previous节点现在指向newNode,从而将newNode插入到链表中。8.返回 `head`返回链表的
head。 由于只有当position为0时head会发生变化,其他时候都是返回原head,函数会正确地返回链表的头部。 -
删除节点 (deleteNode):
inifunction deleteNode(head, position) { if (!head) { return null; } if (position < 0) { console.log("Invalid position"); return head; } if (position === 0) { return head.next; } let current = head; let previous = null; let index = 0; while (current && index < position) { previous = current; current = current.next; index++; } if (!current) { console.log("Node not found at position"); return head; } previous.next = current.next; return head; } // 示例: head = deleteNode(head, 1);1.if (!head) { return null; }处理链表为空的情况。 如果
head是null,说明链表是空的,没有节点可以删除,函数直接返回null。2.if (position < 0) { ... }处理无效的位置。 如果
position是负数,说明位置无效,函数输出错误信息并返回原来的head,不做任何修改。3.if (position === 0) { return head.next; }处理删除头节点的情况。 如果
position是 0,说明要删除的是头节点。 在这种情况下,函数直接返回head.next,即将链表的第二个节点作为新的头节点。 相当于从链表中移除了原来的头节点。 注意: 这种情况下,原来的头节点并没有被显式地释放内存。4.let current = head; let previous = null; let index = 0;初始化三个变量:
current:指向当前正在遍历的节点,初始值为head。previous:指向current的前一个节点,初始值为null(因为头节点没有前一个节点)。index:记录当前遍历到的节点的位置,初始值为 0。5.while (current && index < position) { ... }这个
while循环遍历链表,直到到达要删除的节点的位置。current && index < position:确保current不为null(即没有到达链表尾部) 并且index小于目标position。在循环内部:
(1)previous = current;:将previous更新为current。(2)current = current.next;:将current移动到下一个节点。(3)index++;:增加index计数。循环结束后,
current会指向要删除的节点,而previous会指向要删除节点的前一个节点。6.if (!current) { ... }在遍历结束后,需要检查是否找到了要删除的节点。 如果
current是null,说明遍历到了链表的末尾,但是仍然没有到达指定的位置 (即position大于等于链表的长度),这意味着要删除的节点不存在。 函数输出错误信息并返回原始的head,不进行任何删除操作。7.previous.next = current.next;如果找到了要删除的节点,这一步是删除节点的核心操作。 让
previous节点的next指针指向current节点的next指针。 这相当于将current节点从链表中"跳过",从而删除了该节点。 关键: 这一步并没有显式地释放current节点占用的内存空间。8.return head;返回链表的
head。在大多数情况(除了删除的是头节点),head不会改变。 -
查找节点 (search):
inifunction search(head, val) { let current = head; let index = 0; while (current) { if (current.val === val) { return index; } current = current.next; index++; } return -1; } // 示例: const index = search(head, 3); if (index !== -1) { console.log("Node found at index:", index); } else { console.log("Node not found"); }1.let current = head;初始化
current指针,指向链表的头部head。current指针用于遍历链表。2.let index = 0;初始化
index变量,用于记录当前遍历到的节点的索引。 链表的第一个节点的索引为 0。3.while (current) { ... }这个
while循环遍历链表,直到到达链表的末尾(current变为null)。 循环条件current用于判断当前节点是否有效。如果current为null,说明已经遍历到了链表的末尾,循环结束。4.if (current.val === val) { return index; }在循环内部,检查当前节点的值
current.val是否等于要查找的值val。 如果相等,说明找到了目标节点,函数立即返回该节点的索引index。5.current = current.next;如果当前节点的值与目标值不匹配,将
current指针移动到链表的下一个节点。6.index++;将
index变量增加 1,以反映当前遍历到的节点的索引。7.return -1;如果
while循环结束,说明已经遍历了整个链表,但没有找到目标节点。 在这种情况下,函数返回 -1,表示目标节点不存在于链表中。 -
反转链表 (reverseList):
inifunction reverseList(head) { let previous = null; let current = head; let next = null; while (current) { next = current.next; current.next = previous; previous = current; current = next; } return previous; } //示例 head = reverseList(head);1.let previous = null;初始化
previous为null。previous变量将用于存储当前节点current的前一个节点。 在反转后的链表中,当前节点的前一个节点实际上是原始链表中的后一个节点。 因为 head 节点会变成 tail 节点,所以 tail 节点的 previous 应该是 null。2.let current = head;初始化
current为head。current变量将用于遍历链表。3.let next = null;初始化
next为null。next变量用于临时存储current的下一个节点,以便在反转current的next指针后,仍然可以访问链表的剩余部分。4.while (current) { ... }这个
while循环遍历链表,直到current变为null,表示已经到达链表的末尾。5.next = current.next;在修改
current.next之前,先将current的下一个节点保存到next变量中。 这是至关重要的一步,因为在下一步中会覆盖current.next。6.current.next = previous;这是反转链表的核心步骤。 将
current的next指针指向previous。 这实际上将current节点从原始链表中分离出来,并将其插入到反转后的链表的头部。7.previous = current;将
previous更新为current。 在下一次迭代中,current将成为下一个节点的previous节点。8.current = next;将
current更新为next。 移动到链表中的下一个节点。9.return previous;当
while循环结束时,current将为null,而previous将指向反转后的链表的头部。 返回previous。 -
打印链表 (printList):
inifunction printList(head) { let current = head; let str = ""; while (current) { str += current.val + " -> "; current = current.next; } str += "null"; console.log(str); } // 示例: printList(head);1.let current = head;初始化
current指针,指向链表的头部head。current指针用于遍历链表,就像你在search函数中所做的那样。2.let str = "";初始化一个空字符串
str。 这个字符串将用于构建链表的表示。3.while (current) { ... }这个
while循环遍历链表,直到current指针变为null,表示已经到达链表的末尾。4.str += current.val + " -> ";在循环内部,将当前节点的值
current.val和字符串" -> "添加到str字符串中。 这创建了链表中节点之间的箭头表示。5.current = current.next;将
current指针移动到链表的下一个节点。6.str += "null";在
while循环结束后,将字符串"null"添加到str字符串中,表示链表的末尾。 这是链表表示的标准约定。7.console.log(str);使用
console.log()函数将构建好的字符串str打印到控制台。
4. 链表的优点和缺点
-
优点:
- 动态大小: 链表的大小可以在运行时动态调整,不需要预先分配固定大小的内存。
- 插入和删除效率高: 在已知节点位置的情况下,插入和删除操作的时间复杂度为 O(1)。
-
缺点:
- 需要额外的内存空间: 每个节点都需要额外的内存空间来存储指针。
- 访问效率低: 访问链表中特定位置的节点需要从头节点开始遍历,时间复杂度为 O(n)。
5. 何时使用链表
- 当需要频繁进行插入和删除操作,并且不需要频繁访问特定位置的元素时。
- 当无法预先确定数据的大小时。
二、题目描述------2.两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:

ini
输入: l1 = [2,4,3], l2 = [5,6,4]
输出: [7,0,8]
解释: 342 + 465 = 807.
示例 2:
css
输入: l1 = [0], l2 = [0]
输出: [0]
示例 3:
css
输入: 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- 题目数据保证列表表示的数字不含前导零
三、解题方案
本人的拼尽全力只能想到两种方案
方案一暴力拆分
将两个链表的数组表示出来,加一起之和再转换回去
方案二不拆分,直接处理
采用进位的方式直接在原链表上进行相加
这里我们选择方案二进行解题
四、具体代码
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
let carry = 0;
let head = null;
let tail = null;
let current1 = l1;
let current2 = l2;
while (current1 || current2 || carry) {
const digit1 = current1 ? current1.val : 0;
const digit2 = current2 ? current2.val : 0;
const sum = digit1 + digit2 + carry;
const digit = sum % 10;
carry = Math.floor(sum / 10);
const newNode = new ListNode(digit);
if (head === null) {
head = newNode;
tail = newNode;
} else {
tail.next = newNode;
tail = newNode;
}
current1 = current1 ? current1.next : null;
current2 = current2 ? current2.next : null;
}
return head;
};
详细解释
1. let carry = 0; : 初始化进位 carry 为 0。 进位用于处理两个数字相加超过 9 的情况。
2. let head = null; : 初始化结果链表的头节点 head 为 null。 这是新建链表的标准做法。
3. let tail = null; : 初始化结果链表的尾节点 tail 为 null。 tail 指针方便在链表末尾添加新节点,而无需每次都从头遍历。
4. let current1 = l1; : 初始化 current1 指针,指向链表 l1 的头部。
5. let current2 = l2; : 初始化 current2 指针,指向链表 l2 的头部。
6. while (current1 || current2 || carry) { ... } : 一个 while 循环,只要 l1 和 l2 中还有节点或者 carry 不为 0,就继续循环。 这意味着即使一个链表已经遍历完,但另一个链表还有剩余节点或者有进位,都需要继续处理。
7. const digit1 = current1 ? current1.val : 0; : 获取 l1 当前节点的值。 使用三元运算符来处理链表 l1 已经遍历完的情况。 如果 current1 为 null,则 digit1 为 0。
8. const digit2 = current2 ? current2.val : 0; : 获取 l2 当前节点的值。 使用三元运算符来处理链表 l2 已经遍历完的情况。 如果 current2 为 null,则 digit2 为 0。
9. const sum = digit1 + digit2 + carry; : 计算当前位的和,包括 l1 和 l2 的当前位的数字以及进位 carry。
10. const digit = sum % 10; : 计算当前位的结果。通过将 sum 除以 10 取余数来获得。
11. carry = Math.floor(sum / 10); : 计算进位。通过将 sum 除以 10 并向下取整来获得。
12. const newNode = new ListNode(digit); : 创建一个新节点 newNode,其值为 digit。
13. if (head === null) { ... } : 检查结果链表是否为空。 如果 head 为 null,说明这是结果链表的第一个节点。
14. head = newNode; : 设置 newNode 为结果链表的头节点。
14. tail = newNode; : 设置 newNode 为结果链表的尾节点。 因为链表只有一个节点,所以头和尾都是同一个节点。
15. else { ... } : 如果结果链表不为空,则执行此操作。
16. tail.next = newNode; : 将 newNode 添加到结果链表的尾部。
17. tail = newNode; : 更新 tail 指针,使其指向新的尾节点。
18. current1 = current1 ? current1.next : null; : 将 current1 移动到 l1 的下一个节点。 如果 l1 已经遍历完,则将 current1 设置为 null。
19. current2 = current2 ? current2.next : null; : 将 current2 移动到 l2 的下一个节点。 如果 l2 已经遍历完,则将 current2 设置为 null。
20. return head; : 返回结果链表的头节点。
举例解析





五、结语
再见!