一、解题前置知识------链表操作(可跳过)
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;
: 返回结果链表的头节点。
举例解析





五、结语
再见!