1. 19.删除链表的倒数第 N 个结点 跳往力扣页面
1.1 栈
1.1.1 思路
题目要求删除倒数第 n 个节点,那么只需要将倒数第 n+1 个节点的 next 指针指向倒数第 n-1 个节点即可。
既然需要从末尾处开始寻找符合要求的节点,可以考虑将所有节点放入一个栈结构中,这样出栈的顺序不就是从链表末尾向前遍历的顺序吗?
1.1.2 使用栈结构完成
function removeNthFromEnd(head, n) {
// 我们在创建了一个全新的头结点,想想为什么要这样做?这样做的好处是什么?
let link = new ListNode(-1, head)
let stack = []
let currentNode = link
while (currentNode) {
stack.push(currentNode)
currentNode = currentNode.next
}
for (let index = 1; index <= n + 1; index++) {
let pre = stack.pop()
// 被删除的前面一个节点
if (index === n + 1) {
pre.next = pre.next.next || null
return link.next
}
}
return link.next
}
1.1.3 解惑
Q: 我们在创建了一个全新的头结点,想想为什么要这样做?这样做的好处是什么?
A: 因为题目并没有说明,链表的头结点不会被删除。如果被删掉的节点刚刚是头结点,在出栈寻找倒数第 n-1 个节点时候,会提示错误。所以,我们不妨在链表的头结点之前接入一个新的节点,这样做完全避免了原链表的头结点被删除时,寻找倒数第 n-1 个节点提示错误的情况。当然你也可以不用这种方法,当栈的大小刚刚等于 n 的时候,将链表的头结点改为头结点的下一个节点即可!
function removeNthFromEnd(head, n) {
let stack = []
let currentNode = head
while (currentNode) {
stack.push(currentNode)
currentNode = currentNode.next
}
// 特殊情况特殊处理
if (n === stack.length) {
head = head.next
return head
}
let next = null
for (let index = 1; index <= n + 1; index++) {
let pre = stack.pop()
if (index === n + 1) {
pre.next = pre.next.next || null
return link.next
}
}
return head
}
1.2 双指针
1.2.1 思路
这道题目可以使用双指针解决,先设想定义了双指针 slow 和 fast ;如果两个指针正好间隔 n 个节点个数,那么指针 fast 指向链表末尾时,指针 slow 是不是刚刚指向链表的倒数第 N-1 个结点呢?
1.2.2 动画解释
1.2.3 代码的实现
function removeNthFromEnd(node, n) {
let head = new ListNode(-1, node)
let fast = head
let slow = head
// 快指针与慢指针间隔 n 个元素
while (n >= 0) {
fast = fast.next
--n
}
// 快慢指针同事向前走,当fast指向空时,改变慢指针指向节点的 next 指向即可。
while (fast) {
fast = fast.next
slow = slow.next
}
// 考虑到链表倒数第一个节点被删除这个特殊情况
slow.next = slow.next.next || null
return head.next
}
2. 92.反转链表 II 跳往力扣页面
2.1 栈
2.1.1 思路
题目要求翻转规定区间内部的节点,我们可以再次利用栈数据类型 后进先出 的特点,将区间内部的节点依次入栈。因为需要将指定区间的节点反转之后接入原链表中,所以我们需要找到区间的前一个节点和后一个节点。之后将栈中的节点依次出栈,并按照如下规则改变节点的指向关系:
(1)区间的前一个节点的 next 指向第一个出栈的节点。
(2)第 n 个出栈的节点的 next 指向第 n + 1 个出栈的节点(n>1)。
(3)最后一个出栈的节点的 next 指向区间的后一个节点。
2.2.3 解惑
问题一: 经过分析之后,在实现过程中需要找到区间的前一个和后一个节点,那如果翻转区间正好是将该链表的尾部位置和头部位置进行翻转时(比如 :请反转从位置 1 到位置 4 区间内的链表节点,链表为:2->3->4->5 ),区间的前一个节点是不是就不存在呢?
回答一:我们可以创建一个节点,让此节点的 next 指向原链表的头部。
2.2.3 代码实现
var reverseBetween = function (head, left, right) {
// 创建一个栈空间,将符合条件的节点放入栈中。
let stack = []
// 创建一个虚拟头结点
let link = new ListNode(-1, head)
// 保存头结点内存地址
let res = link
let index = 1
// 保存区间的前一个和后一个节点
let pre = null
let next = null
// 我们动态在链表的基础上新增了一个头结点,所以,需要将区间整体右移1位
left++
right++
// 将区间内部的节点放入栈空间
// 保存区间开始的前一个节点和区间结束的下一个节点
while (link) {
if (index === left - 1) {
pre = link
}
if (index === right + 1) {
next = link
}
link = link.next
index++
if (index >= left && index <= right) {
// 区间内部的节点开始入栈
stack.push(link)
}
}
let current = null
while (stack.length > 0) {
// 出栈
let node = stack.pop()
// 根据规则改变节点的指向
if (current === null) {
pre.next = node
} else {
current.next = node
}
current = node
}
// 最后一个出栈的节点的 next 指向区间的后一个节点。
current.next = next
return res.next
};
2.2 双指针-头插法
2.2.1 思路
- 我们定义两个指针,分别称之为 g 和 p 。 我们首先将 g 移动到第一个要反转的节点的前面,将 p 移动到第一个要反转的节点的位置上。
- 将 p 的下一个节点删除,然后添加到 g 节点的后面。将 p 的下一个节点更新为 p 的下一个节点的下一个节点。
- 根据给定的区间,计算第二步重复的次数。
2.2.2 图解
2.2.3 代码实现
function reverseBetween(head, left, right) {
let link = new ListNode(-99, head)
let g = link
let p = link
let index = 1
left++
right++
// 将指针 p 和指针 g 安置于合理的位置
while (index < left - 1) {
if (index < left - 1) {
g = g.next
}
index++
}
p = g.next
for (let i = 0; i < right - left; ++i) {
// 保存指针 p 的下一个节点
let deleted = p.next
// 更新指针 p 的下一个节点
p.next = p.next.next
// p 的下一个节点放置于指针 g 节点的下一位
g.next = deleted
// 将指针接入链表
deleted.next = g.next
}
return link.next
}
3. 876.链表的中间结点 跳往力扣页面
3.1 栈
3.1.2 思路
我们可以维护栈(或者队列)的数据结构,具体步骤如下:
首先,先将链表中的所有节点依次入栈(或者进队);
然后,通过栈(或者队列)的总大小计算出中间位置;
最后,根据中间位置合理出栈(或者出队)。
3.1.2 代码实现
// 正式代码
var middleNode = function (head) {
let stack = []
let len = 0
let curNode = head
// 将所有节点入栈
while (curNode) {
stack.push(curNode)
curNode = curNode.next
len++
}
// 计算中间位置
let middle = Math.ceil(len / 2)
// 根据中间位置,合理出栈
let top = null
while (middle > 0) {
top = stack.pop()
middle--
}
return top
};
3.2 双指针
3.2.1 思路
举例:甲、乙两个人参加百米冲刺跑步,若甲的速度是乙速度的2
倍,裁判一声令下,甲、乙同时
开跑。当甲到达终点的时候,此时,乙应该在赛道的那个位置呢?毫无疑问,此时的乙应该正位于赛道的正中间。
通过上述案例,再思考这道题目得到以下思路:
-
- 声明
两个指针
,分别命名为快指针和慢指针。 - 快指针的移动速度是慢指针移动速度的
2
倍,换言之:快指针一次移动2位,慢指针一次移动1位。 - 当快指针移动到链表尾部时,返回慢指针指向的节点。
- 声明
3.2.2 图解
3.2.3 代码实现
function middleNode(head) {
let fast = head
let slow = head
while (fast && fast.next) {
fast = fast?.next?.next || null
slow = slow.next
}
return slow
}
4. 21.合并两个有序链表 跳往力扣页面
4.1 队列
4.1.1 思路
我们可以维护一个队列,具体步骤如下:
-
- 首先,将两个链表中的所有节点排序放入一个队列中。
- 然后,依次出队并改变指针指向。第
n
个出队的节点的 next 指向第n + 1
个出队的节点,其中n > 0
。
4.1.2 代码实现
function mergeTwoLists(list1, list2) {
let queue = []
let p = list1
let q = list2
while (p || q) {
// 处理特殊情况
if (!p && q) {
queue.push(q)
q = q.next
} else if (!q && p) {
queue.push(p)
p = p.next
}
// 如果链表1中当前节点的值大于链表2中当前节点的值
else if (p.val > q.val) {
queue.push(q)
q = q.next
} else {
// 如果链表1中当前节点的值小于或等于链表2中当前节点的值
queue.push(p)
p = p.next
}
}
let link = null
let curent = null
while (queue.length > 0) {
// 依次出队
let top = queue.shift()
// 第一个出队的节点为头结点,对头结点单独处理
if (link === null) {
curent = top
link = top
} else {
// 第 n 个出队的节点的 next 指向第 n + 1 个出队的节点,其中n > 0。
curent.next = top
curent = top
}
}
return link
};
4.2 双指针
4.2.1 思路
我们可以动态创建一个链表,然后依次遍历两个已有的链表,如果链表1当前节点的值小于链表2当前节点的值,那么将链表1当前节点追加到我们创建的链表中,反之也是如此。
4.2.2 图解
4.2.3 代码实现
function mergeTwoLists(list1, list2) {
let p = list1
let q = list2
// 创建新链表
let top = new ListNode(-1)
let topLast = top
while (p || q) {
// 处理特殊情况
if (!p && q) {
topLast.next = q
q = q.next
} else if (p && !q) {
topLast.next = p
p = p.next
}
// 如果链表1当前节点的值小于链表2当前节点的值
// 将链表1当前节点追加到我们创建的链表尾部
else if (p.val < q.val) {
topLast.next = p
p = p.next
}
// 将链表2当前节点追加到我们创建的链表尾部
else {
topLast.next = q
q = q.next
}
// 更新新链表尾部的节点
topLast = topLast.next
}
return top.next
}