第四节:链表相关(删除倒数第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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2024-03-12 08:44  Yaopengfei  阅读(8)  评论(1编辑  收藏  举报