第四节:链表相关(删除倒数第N节点、相邻位置交换、反转链表)
一. 删除倒数第N个节点
一. 题目描述
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例:
leetcode地址:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/
难度:【中等】
二. 思路分析
(经典的链表问题,双指针(快慢指针)解决)
(1).创建虚拟节点dummy, 即它的next指向head第一个节点 【主要为了方便处理边界情况,dummy是头节点head的前一个节点】
(2).创建快慢双指针,等于dummy
(3). 让fast快指针先移动n+1步,然后让fast和slow指针同时移动, 直到fast为空 【精髓!】
此时slow指针恰巧指向被删除节点的前一个节点
(4). 修改slow指针的指向,达到删除的目的
(5). 返回头节点,即dummy.next
三. 代码实操
class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}
/**
* 删除链表倒数第N个节点
* @param head 头节点
* @param n 需要被删除的倒数第n个节点
* @returns 返回头节点
*/
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
//1.创建虚拟节点
let dummy = new ListNode(-1);
dummy.next = head;
//2.创建双指针(快慢指针)
let fast = dummy;
let slow = dummy;
//3.让fast快指针移动n+1步
for (let i = 0; i <= n; i++) {
fast = fast.next!;
}
//4.让fast和slow指针同时移动,直到fast为空(即超过了最后一个元素)
while (fast) {
fast = fast.next!;
slow = slow.next!;
}
//5.此时slow指针恰巧指向被删除节点的前一个节点
slow.next = slow.next?.next!;
//6.返回头节点
return dummy.next;
}
二. 链表相邻位置两两交换
一. 题目描述
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
leetcode:https://leetcode.cn/problems/swap-nodes-in-pairs/description/
难度:【中等】
二. 思路分析
(1). 创建虚拟节点dummy节点,指向head节点
(2). 创建一个current节点,默认指向虚拟节点(这里因为有虚拟节点,所以可以直接调用next)
(3). 遍历节点(接下来两个节点都存在,一直循环)
A. 取出接下来的两个节点
B. 交换位置
C. current赋值,开始下一轮循环
(4).返回头节点
三. 代码实操
class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}
/**
* 链表相邻位置的两两交换
* @param head 头节点
* @returns 头节点
*/
function swapPairs(head: ListNode | null): ListNode | null {
// 1. 创建虚拟节点
let dummy = new ListNode(-1);
dummy.next = head;
//2. 创建current节点,指向虚拟节点
let current = dummy;
//3. 循环进行两两交换
while (current.next && current.next.next) {
let node1 = current.next;
let node2 = current.next.next;
//交换node1和node2位置
current.next = node2;
node1.next = node2.next;
node2.next = node1;
//开始下一次交换
current = node1;
}
return dummy.next;
}
三. 反转链表
1. 题目说明
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
leetcode:https://leetcode.cn/problems/reverse-linked-list/description/
难度:【简单】
2. 方案1-栈
解决思路:
1. 没有现成的栈,利用数组模拟栈, 主要利用:push和pop方法
2. 先入栈,然后利用栈的特性,出栈,并组装新的链表
class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}
/**
* 反转链表
* @param head 头节点
* @returns 反转后链表的头节点
*/
function reverseList(head: ListNode | null): ListNode | null {
//1.前置判断
if (head === null || head.next === null) return head;
//2. 反转业务
let newHead: ListNode | null = null;
while (head) {
//2.1 提前保留head.next,否则后续给head.next赋值后, 就再也拿不到原先的head.next了
let current: ListNode = head.next;
//2.2 设置当前遍历节点的反转后的指向
head.next = newHead;
//2.3 设置newHead向后移位,用于下次遍历的指向
newHead = head;
//2.4 head向后移动一位,便于while循环
head = current;
}
return newHead;
}
3. 方案2-迭代
解决思路:
1. 前置判断
2. 提前声明一个newHead节点,该节点的作用就是每次遍历节点需要反转后指向的就是newHead节点,默认为null
2. 遍历每个节点,修改它的反转后的指向
(1). 设置current节点, 用来提前保留head.next,否则后续给head.next赋值后, 就再也拿不到原先的head.next了
(2). 设置当前遍历节点的反转后的指向, 即head=newHead
(3). 让newHead向后移动一位,即newHead=head,用于下次遍历head节点的指向
(4). 将head节点向后移位,维护while循环的逻辑
/**
* 反转链表
* @param head 头节点
* @returns 反转后链表的头节点
*/
function reverseList(head: ListNode | null): ListNode | null {
//1.前置判断
if (head === null || head.next === null) return head;
//2. 反转业务
let newHead: ListNode | null = null;
while (head) {
//2.1 提前保留head.next,否则后续给head.next赋值后, 就再也拿不到原先的head.next了
let current: ListNode = head.next;
//2.2 设置当前遍历节点的反转后的指向
head.next = newHead;
//2.3 设置newHead向后移位,用于下次遍历的指向
newHead = head;
//2.4 head向后移动一位,便于while循环
head = current;
}
return newHead;
}
4. 方案3-递归
( 一). 递归相关深层理解
1. 如何写递归代码
(1).首先要有递归的结束条件
(2).调用递归
(3).可以使用第一次调用的代码 或者 递归结束后对应节点的位置来编写业务代码,模拟业务
2. 递归调用后面的代码什么时候执行? 【重点】
(1). 假设依次调用递归 A → B → C → D, 走到D的时候,触发了递归的结束条件, 然后走【递归调用】下面的代码。
(2). 函数内部递归调用了N次, 那么【递归调用】后面的代码就执行N次
(3). 【递归调用】后面代码执行的顺序为:D→C→B→A
3. 解决思路:
(1).假设单链表为 1 → 2 → 3 → 4,头节点head=1
(2).首先声明递归结束的条件,head=null 或者 head.next=null
(3).函数内部递归调用,传入的参数为 head.next,需要连续递归调用3次 【因为最外层调用的时候传入参数为head,内层第1次调用传入head.next】
第1次: head.next=2 此时的head=1
第2次: head.next=3 此时的head=2
第3次: head.next=4 触发递归结束的条件 4.next=null, 此时的head=3
(4).开始执行递归调用后面的代码, 函数内部递归3次,所以递归调用后面代码也执行3次
第1次进入递归调用后面代码时:head=3 (倒数第2个节点)
执行业务:head.next.next=head (此时head=3, head.next=4 即让 4.next=3, 4节点反转执行成功)
此时链表为: 此时链表为 1 -> 2 -> 3 <-> 4, 3和4节点互相指向,所以需要断开,
执行业务:head.next=null, 即 3.next=null 最终为:1->2->3<-4
第2次进入递归调用后面代码时:head=2
执行业务:head.next.next=head (此时head=2, head.next=3 即让 3.next=2, 3节点反转执行成功)
此时链表为: 此时链表为1 -> 2 <-> 3 <- 4, 2和3节点互相指向,所以需要断开,
执行业务:head.next=null, 即 2.next=null 最终为:1->2<-3<-4
第3次进入递归调用后面代码时:head=1
执行业务:head.next.next=head (此时head=2, head.next=3 即让 2.next=1, 2节点反转执行成功)
此时链表为: 此时链表为1 <-> 2 <- 3 <- 4, 1和2节点互相指向,所以需要断开,
执行业务:head.next=null, 即 1.next=null 最终为:1<-2<-3<-4
/**
* 反转链表
* @param head 头节点
* @returns 反转后链表的头节点
*/
function reverseList(head: ListNode | null): ListNode | null {
//1. 递归的结束条件
if (head === null) {
return null;
}
if (head.next === null) {
return head;
}
// 等价
// if (!head || !head.next) return head;
//核心代码
const newHead = reverseList(head?.next ?? null);
// 完成想要做的操作是在这个位置
// 第一次来到这里的时候, head指向的是倒数第二个节点 (会进入这个位置length-1次)
// 因为最后一次调用reverseList的时候里面的参数 head.next=4, 所以head=3,即倒数第二个节点
head.next.next = head;
head.next = null;
console.log(`第${count++}次:${head.val}`);
return newHead;
}
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。