【算法通关村】链表反转经典问题解析

news2025/1/18 20:27:16

🚩本文已收录至算法学习之旅

一.基础反转

我们通常有两种方法反转链表,一种是直接操作链表实现反转操作,一种是建立虚拟头节点辅助实现反转操作

力扣习题链接:206. 反转链表
在这里插入图片描述

(1) 直接操作实现反转

我们需要一个变量pre来保存当前节点的上一个节点,否则无法进行反转操作也就是指向上一个节点。我们还需要一个节点next来保存当前节点的下一个节点,因为我们一旦操作当前节点的指针域后将会丢失下一个节点的地址。知道需要两个变量进行辅助,接下来我们就可以严格按照一定的顺序来反转指针的。1.先记录当前节点的下一个指针(不记录将会在下一步操作中丢失) 2.令当前节点指向前一个节点(如此我们便完成了这两个节点的反转操作,可以开始移动指针进行下两个节点之间的操作) 3.令pre指针指向当前指针(移动后当前指针变成了前一个指针,用于后续节点的反转)4. 将当前指针移向下一个指针(正是我们通过next变量保存了才能找到当前节点的下一个指针,否则就在第2步中丢失了后续节点)5.如此往复直到当前指针移动到null,我们也完成了反转操作。

	public ListNode reverseList(ListNode head) {
        // 记录前一个节点
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            // 记录后一个节点
            ListNode next = curr.next;
			 // 令当前节点指向前一个节点
            curr.next = prev;
            // 保存当前节点
            prev = curr;
            // 移动到下一个节点
            curr = next;
        }
        return prev;
    }

强烈建议各位手动模拟,注意链表节点与节点之间是如何进行链接的,体会每个变量的作用,示意图如下:
在这里插入图片描述

(2) 虚拟节点辅助实现反转

在上一篇中,我们发现处理头节点与中间节点和尾部节点方法不一致,如果单独处理则比较麻烦,因此我们可以先建立一个虚拟头节点并且指向链表头节点head,这样我们的操作便统一了。通过使用虚拟节点辅助实现反转,我们可以每次从旧的链表拆下来一个结点接到ans后面,然后将其他线调整好就可以了。

public static ListNode reverseList(ListNode head) {
    ListNode ans = new ListNode(-1);
    ListNode cur = head;
    while (cur != null) {
        ListNode next = cur.next;
        cur.next = ans.next;
        ans.next = cur;
        cur = next;
    }
    return ans.next;
}

与直接反转类似,强烈建议各位手动模拟,注意链表节点与节点之间是如何进行链接的,体会每个变量的作用,示意图如下:

在这里插入图片描述

二.经典问题分析

(1) 指定区间反转

力扣链接:反转链表 II

在这里插入图片描述

解决这道题目我们有两种方法,一种是头插法,一种是穿针引线法。

(1.1) 头插法

头插法反转的整体思想:在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。

在这里插入图片描述

这个插入过程就是前面所学习的带虚拟结点的插入操作,每走一步都要考虑各种指针怎么指,既要将结点摘下来接到对应的位置上,还要保证后续结点能够找到。

public ListNode reverseBetween(ListNode head, int left, int right) {
    // 设置 dummyNode 是这一类问题的一般做法(统一节点不同位置处理)
    ListNode dummyNode = new ListNode(-1);
    dummyNode.next = head;
    ListNode pre = dummyNode;
    // 找到待反转区间的前一个节点
    for (int i = 0; i < left - 1; i++) {
        pre = pre.next;
    }
    // 指向反转区间的起始节点
    ListNode cur = pre.next;
    ListNode next;
    // 遍历区间内的节点并将其移向待反转区间的起始位置
    for (int i = 0; i < right - left; i++) {
        next = cur.next;
        cur.next = next.next;
        next.next = pre.next;
        pre.next = next;
    }
    return dummyNode.next;
}

(1.2) 穿针引线法

穿针引线法的整体思想:首先找到待反转区间的开始节点与结束节点以及反转区间的前一个节点以及后一个节点。将区间内节点进行反转,最后再按照我们记录的标识点进行连接即可。

在这里插入图片描述

public ListNode reverseBetween(ListNode head, int left, int right) {
    // 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
    ListNode dummyNode = new ListNode(-1);
    dummyNode.next = head;
    ListNode pre = dummyNode;
    // 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
    for (int i = 0; i < left - 1; i++) {
        pre = pre.next;
    }
    // 第 2 步:从 pre 再走 right - left + 1 步,来到 right 节点
    ListNode rightNode = pre;
    for (int i = 0; i < right - left + 1; i++) {
        rightNode = rightNode.next;
    }
    // 第 3 步:切出一个子链表
    ListNode leftNode = pre.next;
    ListNode succ = rightNode.next;
    // 模拟设置为链表末尾节点指向null
    rightNode.next = null;

    // 第 4 步:反转链表的子区间
    reverseLinkedList(leftNode);
    // 第 5 步:接回到原来的链表中
    pre.next = rightNode;
    leftNode.next = succ;
    return dummyNode.next;
}
	// 反转链表
	private void reverseLinkedList(ListNode head) {
		ListNode pre = null;
		ListNode cur = head;
		while (cur != null) {
	    ListNode next = cur.next;
	    cur.next = pre;
	    pre = cur;
	    cur = next;
		}
	}

(2) 两两交换链表中的节点

力扣链接:两两交换链表中的节点

在这里插入图片描述

当链表存在两个及以上未遍历到的节点时,我们按照如下步骤模拟反转就行了。示意图如下:

在这里插入图片描述

    public ListNode swapPairs(ListNode head) {
        ListNode dummyNode = new ListNode();
        dummyNode.next = head;
        ListNode cur = dummyNode.next,pre = dummyNode;
        //在至少存在两个节点时进行翻转
        while(cur != null && cur.next != null){ 
            ListNode next = cur.next;
            cur.next = next.next;
            next.next = cur;
            pre.next = next;
            pre = cur;
            cur = cur.next;
        }
        return dummyNode.next;
    }

(3) 两数相加

力扣链接: 两数相加 II

在这里插入图片描述

我们可以用栈轻松解决就不在此赘述代码实现:先将两个链表的元素分别压栈,然后再一起出栈,将两个结果分别计算。之后对计算结果取模,模数保存到新的链表中,进位保存到下一轮。以此往复即可。

我们主要讲解如何针对链表操作,由于本题的链表从首到尾刚好是从最高位到最低位,而我们正常加法运算都是最低尾开始计算的,因此我们需要先将两条链表进行反转操作,然后再同时遍历每一位模拟加法运算即可轻松解决问题。

class Solution {
 public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
     	// 定义虚拟头节点开始记录新链表
        ListNode ans = new ListNode();
     	// 反转两条链表
        ListNode rl1 = reverseListNode(l1);
        ListNode rl2 = reverseListNode(l2);
     	// 记录进位
        int add = 0;
     	// 链表不为空,进位不为0则持续计算
        while (rl1 != null || rl2 != null || add != 0) {
            int sum = 0;
            // 若不为空,则累加上该节点的值,同时后移一位
            if (rl1 != null) {
                sum += rl1.val;
                rl1 = rl1.next;
            }
            if (rl2 != null) {
                sum += rl2.val;
                rl2 = rl2.next;
            }
            // 加上上一轮的进位
            sum += add;
            // 记录本轮的进位
            add = sum / 10;
            // 计算进位后的数
            sum %= 10;
            // 从低位到高位拼接新的链表
            // ans -> 0 -> 7  8  ==>  ans -> 8 -> 0 -> 7  
            ListNode node = new ListNode(sum);
            node.next = ans.next;
            ans.next = node;
        }
        return ans.next;
    }
	// 直接操作反转链表
    public static ListNode reverseListNode(ListNode head) {
        ListNode pre = null;
        while (head != null) {
            ListNode next = head.next;
            head.next = pre;
            pre = head;
            head = next;
        }
        return pre;
    }
}

(4) 回文链表

力扣链接:回文链表

在这里插入图片描述

在上一篇中我们讲解了判断回文链表可以使用集合+双指针或者栈来解决,我们还可以通过将双指针+反转一半链表再进行比较判断来实现。

class Solution {
    public boolean isPalindrome(ListNode head) {
        if (head == null) {
            return true;
        }

        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode firstHalfEnd = endOfFirstHalf(head);
        ListNode secondHalfStart = reverseList(firstHalfEnd.next);

        // 判断是否回文
        ListNode p1 = head;
        ListNode p2 = secondHalfStart;
        boolean result = true;
        while (result && p2 != null) {
            if (p1.val != p2.val) {
                result = false;
            }
            p1 = p1.next;
            p2 = p2.next;
        }        

        // 还原链表并返回结果
        firstHalfEnd.next = reverseList(secondHalfStart);
        return result;
    }
	// 反转链表
    private ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
	// 通过双指针找到后半部分第一个节点
    private ListNode endOfFirstHalf(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }
}

(5) K 个一组翻转链表

力扣链接:K 个一组翻转链表

在这里插入图片描述

这一题其实可以看做是指定区间反转的进阶形式,只要我们足够细心,其实这一题并不难。我们同样要使用到穿针引线法来解决问题,相当于使用穿针引线法反转若干个指定区间。

    public ListNode reverseKGroup(ListNode head, int k) {
       ListNode dummyNode = new ListNode();
        dummyNode.next = head;
        // 记录穿针引线法需要使用的四个标记点
        ListNode pre = dummyNode, right, left, succ;
        // 反转若干个指定区间
        while (true) {
            right = pre;
            // 1.寻找到反转区间right节点
            for (int i = 0; i < k; i++) {
                // 当一组不足k个时,表明不需要反转了,直接返回结果
                if (right.next == null) {
                    return dummyNode.next;
                }
                right = right.next;
            }
            // 2.记录left节点以及succ节点
            left = pre.next;
            succ = right.next;
            // 3.反转指定区间
            right.next = null;
            right = reverse(left);
            // 4.穿针引线拼接链表
            pre.next = right;
            left.next = succ;
            // 5.更新为下一个区间的起始节点的前一个节点
            pre = left;
        }
    }
	// 反转链表
    public static ListNode reverse(ListNode head) {
        ListNode pre = null;
        while (head != null) {
            ListNode next = head.next;
            head.next = pre;
            pre = head;
            head = next;
        }
        return pre;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1298900.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

13. MySQL 日志

目录 错误日志 binlog日志 概述 日志格式 查询日志 慢查询日志 错误日志 错误日志是MySQL中最重要的日志之一&#xff0c;它记录了当mysqld启动和停止时&#xff0c;以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时&#…

51单片机独立按键以及矩阵按键的使用以及其原理--独立按键 K1 控制 D1 指示灯亮灭以及数码管显示矩阵按键 S1-S16 按下后键值 0-F

IO 的使用–按键 本文主要涉及8051单片机按键的使用&#xff0c;包括独立按键以及矩阵按键的使用以及其原理&#xff0c;其中代码实例包括: 1.独立按键 K1 控制 D1 指示灯亮灭 2.通过数码管显示矩阵按键 S1-S16 按下后键值 0-F 文章目录 IO 的使用--按键一、按键消抖二、独立按…

MobaXterm成功连接到开发环境后,过一段时间会自动断开。

问题现象 MobaXterm成功连接到开发环境后&#xff0c;过一段时间会自动断开。 原因 配置MobaXterm工具时&#xff0c;没有勾选“SSH keepalive”或专业版MobaXterm工具的“Stop server after”时间设置太短。

8. MySQL 触发器

目录 概述 定义 触发器特性&#xff1a; 基础操作 创建触发器 NEW和OLD 其他操作 查看触发器 删除触发器 注意事项 概述 定义 触发器&#xff0c;就是一种特殊的存储过程。触发器和存储过程一样是一个能够完成特定功能、存储在数据库服务器上的SQL片段&#xff0c;但是触…

跨平台的文本编辑器——CudaText

CudaText 是一个轻量级、跨平台的文本编辑器&#xff0c;它免费开源&#xff0c;启动速度非常快&#xff0c;有拓展功能&#xff0c;可安装插件。 下载 浏览器搜索框输入CudaText - Home进行搜索&#xff0c; 选择官网进入&#xff0c; 进入官网界面如下&#xff1a;选择点击…

C++初阶-vector的介绍及使用

vector的介绍及使用 一、vector的介绍1.1 vector的概念 二、vector的使用2.1 vector的定义2.2 vector iterator的使用2.3 vector空间增长问题2.4 vector的增删改查2.5 vector的整体代码实现2.5.1 vector的常用内置函数使用2.5.2 vector的访问方式及测试函数 三、vector迭代器失…

15.Java程序设计-基于SSM框架的微信小程序校园求职系统的设计与实现

摘要&#xff1a; 本研究旨在设计并实现一款基于SSM框架的微信小程序校园求职系统&#xff0c;以提升校园求职流程的效率和便捷性。通过整合微信小程序平台和SSM框架的优势&#xff0c;本系统涵盖了用户管理、职位发布与搜索、简历管理、消息通知等多个功能模块&#xff0c;为…

Ubuntu与Windows通讯传输文件(FTP服务器版)(没用的方法,无法施行)

本文介绍再Windows主机上建立FTP服务器&#xff0c;并且在Ubuntu虚拟机上面访问Windows上FTP服务器的方法 只要按照上图配置就可以了 第二部&#xff1a;打开IIS管理控制台 右击网站&#xff0c;新建FTP站点。需要注意的一点是在填写IP地址的时候&#xff0c;只需要填写Window…

python操作MySQL数据库简单示例

通过python的pymysql模块&#xff0c;实现数据库表的创建、插入以及查询的简单示例。 1. 数据库表创建。 # -*- coding:utf-8 -*- """ 使用python操作MySQL数据库示例 执行sql_create语句建立数据库表books """import pymysqldef main():# 首先…

nodejs微信小程序+python+PHP北京地铁票务APP-计算机毕业设计推荐 -安卓

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

postgresql自带指令命令系列三

目录 简介 bin目录 28.pg_verifybackup 29.pg_waldump 30.postgres 31.postmaster -> postgres 32.psql 33.reindexdb 34.vacuumdb 35.vacuumlo 总结&#xff1a; 简介 在安装postgresql数据库的时候会需要设置一个关于postgresql数据库的PATH变量 export PATH/…

PTA 输出三角形面积和周长

#include<stdio.h> #include<math.h>//使用sqrt需要使用此头文件 int main() {int a, b, c, d;float s, area, perimeter;scanf("%d %d %d", &a, &b, &c);if (a b < c || a c < b || b c < a)//三角形任意两边之和大于第三边pri…

【小白专用】在 vs 中使用 nuget 安装NPOI

C#操作Excel有多种方法&#xff0c;如通过数据库的方式来读写Excel的OleDb方式&#xff0c;但是OleDb方式需要安装微软office&#xff0c;还可以通过COM组件方式操作Excel&#xff0c;也需要安装微软Excel。如果不想安装微软办公套餐可以使用ClosedXML、EPPlus、NPOI。本文主要…

【Linux】如何对文本文件进行有条件地划分?——cut命令

cut 命令可以根据一个指定的标记&#xff08;默认是 tab&#xff09;来为文本划分列&#xff0c;然后将此列显示。 例如想要显示 passwd 文件的第一列可以使用以下命令&#xff1a;cut –f 1 –d : /etc/passwd cut&#xff1a;用于从文件的每一行中提取部分内容的命令。-f 1&…

windows建立软链 报 无法将“mklink”项识别为 cmdlet、函数、脚本文件或可运行程序的名称

当我执行网上提供的mklink 的时候&#xff0c;出现 mklink : 无法将“mklink”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。怎么回事&#xff0c;原来&#xff0c;要在执行的签名加 cmd /c 当我执行建立软链接时&#xff0c;提示 没有足够的权限&#xff0c;要用管理…

玩转大数据13: 数据伦理与合规性探讨

1. 引言 随着科技的飞速发展&#xff0c;数据已经成为了现代社会的宝贵资产。然而&#xff0c;数据的收集、处理和利用也带来了一系列的伦理和合规性问题。数据伦理和合规性不仅关乎个人隐私和权益的保护&#xff0c;还涉及到企业的商业利益和社会责任。因此&#xff0c;数据…

【ArcGIS微课1000例】0079:ArcGIS Earth根据经纬坐标生成点shapefile

本文以气象台站数据的生成为例,详细介绍ArcGIS Earth中导入X、Y经纬度坐标,生成Shapefile点数据的流程。 文章目录 一、气象台站分布二、添加经纬度坐标三、符号化设置四、另存为一、气象台站分布 根据气象台站的经纬度坐标,可以很方便的在各种GIS平台上生成点,并保存为多…

CPU运行AI模型记录

使用 CPU 运行 AI 模型 目前人工智能很火&#xff0c;但是手头没有合适的显卡&#xff0c;成了瓶颈和门槛&#xff0c;一直没机会试下。正好知乎上看到一篇文章&#xff0c; CPU 运行中文模型&#xff0c;最近换了台机器。垃圾佬 E5 平台&#xff0c;性能不高&#xff0c;但是…

附录A SQL入门之样例表

编写SQL语句需要良好地理解基本数据库设计。如果不知道什么信息存放在什么表中&#xff0c;表与表之间如何互相关联&#xff0c;行中数据如何分解&#xff0c;那么要编 写高效的SQL是不可能的。 强烈建议读者实际练习本书的每个例子。所有课都共同使用了一组数据文件。为帮助你…

Linux CentOS 7.6安装jdk1.8教程

安装教程 第一种方式&#xff08;通过yum安装&#xff09;&#xff1a;第一步&#xff1a;输入查找命令&#xff1a;第二步&#xff1a;输入安装命令&#xff1a;第三步&#xff1a;安装完成&#xff0c;输入安装命令后&#xff0c;等到出现Complete&#xff01;代表安装完成第…