【力扣 - 回文链表】

题目描述

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
在这里插入图片描述
提示:
链表中节点数目在范围[1, 100000] 内
0 <= Node.val <= 9

方法一:将值复制到数组中后用双指针法

思路

如果你还不太熟悉链表,下面有关于列表的概要讲述。

有两种常用的列表实现,分别为数组列表和链表。如果我们想在列表中存储值,它们是如何实现的呢?

数组列表底层是使用数组存储值,我们可以通过索引在 O(1) 的时间访问列表任何位置的值,这是由基于内存寻址的方式。
链表存储的是称为节点的对象,每个节点保存一个值和指向下一个节点的指针。访问某个特定索引的节点需要 O(n) 的时间,因为要通过指针获取到下一个位置的节点。
确定数组列表是否回文很简单,我们可以使用双指针法来比较两端的元素,并向中间移动。一个指针从起点向中间移动,另一个指针从终点向中间移动。这需要 O(n) 的时间,因为访问每个元素的时间是 O(1),而有 n 个元素要访问。

然而同样的方法在链表上操作并不简单,因为不论是正向访问还是反向访问都不是 O(1)。而将链表的值复制到数组列表中是 O(n)),因此最简单的方法就是将链表的值复制到数组列表中,再使用双指针法判断。

算法

一共为两个步骤:

复制链表值到数组列表中。
使用双指针法判断是否为回文。
第一步,我们需要遍历链表将值复制到数组列表中。我们用 currentNode 指向当前节点。每次迭代向数组添加 currentNode.val,并更新 currentNode = currentNode.next,当 currentNode = null 时停止循环。

执行第二步的最佳方法取决于你使用的语言。在 Python 中,很容易构造一个列表的反向副本,也很容易比较两个列表。而在其他语言中,就没有那么简单。因此最好使用双指针法来检查是否为回文。我们在起点放置一个指针,在结尾放置一个指针,每一次迭代判断两个指针指向的元素是否相同,若不同,返回 false;相同则将两个指针向内移动,并继续判断,直到两个指针相遇。

在编码的过程中,注意我们比较的是节点值的大小,而不是节点本身。正确的比较方式是:node_1.val == node_2.val,而 node_1 == node_2 是错误的。

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
// 判断给定的单链表是否为回文链表的函数
bool isPalindrome(struct ListNode* head) {// 定义一个数组 vals 用于存储链表节点的值,数组大小为 50001int vals[50001];// 定义一个变量 vals_num 用于记录数组中元素的个数,初始化为 0int vals_num = 0;// 遍历链表,将链表节点的值存储到数组 vals 中while (head != NULL) {// 将当前链表节点的值存储到数组 vals 中,并更新 vals_numvals[vals_num++] = head->val;// 移动到链表的下一个节点head = head->next;}// 使用双指针从数组 vals 的两端向中间遍历,比较对应位置的值是否相等// 注意这里只用了一个循环,本来想用双循环,但是双循环的话没有办法首尾对应for (int i = 0, j = vals_num - 1; i < j; ++i, --j) {// 如果对应位置的值不相等,则链表不是回文链表,返回 falseif (vals[i] != vals[j]) {return false;}}// 如果双指针都遍历到了中间,且对应位置的值都相等,则链表是回文链表,返回 truereturn true;
}

复杂度分析

时间复杂度: O(n),其中 n 指的是链表的元素个数。
第一步: 遍历链表并将值复制到数组中,O(n)。
第二步:双指针判断是否为回文,执行了 O(n/2) 次的判断,即 O(n)。
总的时间复杂度:O(2n)=O(n)。
空间复杂度:O(n),其中 n 指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值。

方法二:递归

思路

为了想出使用空间复杂度为 O(1) 的算法,你可能想过使用递归来解决,但是这仍然需要 O(n) 的空间复杂度。

递归为我们提供了一种优雅的方式来方向遍历节点。

function print_values_in_reverse(ListNode head)if head is NOT nullprint_values_in_reverse(head.next)print head.val

如果使用递归反向迭代节点,同时使用递归函数外的变量向前迭代,就可以判断链表是否为回文。

算法

currentNode 指针是先到尾节点,由于递归的特性再从后往前进行比较。frontPointer 是递归函数外的指针。若 currentNode.val != frontPointer.val 则返回 false。反之,frontPointer 向前移动并返回 true。

算法的正确性在于递归处理节点的顺序是相反的(回顾上面打印的算法),而我们在函数外又记录了一个变量,因此从本质上,我们同时在正向和逆向迭代匹配。

计算机在递归的过程中将使用堆栈的空间,这就是为什么递归并不是 O(1) 的空间复杂度。

// 定义一个全局变量 frontPointer,用于记录当前链表节点的指针位置
struct ListNode* frontPointer;// 递归检查函数,用于检查给定的单链表是否为回文链表
bool recursivelyCheck(struct ListNode* currentNode) {// 如果当前节点不为空if (currentNode != NULL) {// 递归调用 recursivelyCheck 函数,传入当前节点的下一个节点,检查链表后半部分是否为回文if (!recursivelyCheck(currentNode->next)) {return false; // 如果不是回文,则返回 false}// 如果当前节点的值与 frontPointer 所指向的节点的值不相等,则链表不是回文,返回 falseif (currentNode->val != frontPointer->val) {return false;}// 将 frontPointer 指向下一个节点,继续向后比较frontPointer = frontPointer->next;}// 如果链表遍历完成且没有发现不同,则链表是回文,返回 truereturn true;
}// 判断给定的单链表是否为回文链表的函数
bool isPalindrome(struct ListNode* head) {// 将全局变量 frontPointer 指向头节点,表示开始比较链表的头部frontPointer = head;// 调用递归检查函数,传入头节点,检查整个链表是否为回文return recursivelyCheck(head);
}

这段代码使用了一个全局变量 frontPointer,它指向当前需要比较的节点。函数 recursivelyCheck 通过递归的方式从链表的尾部开始向前比较节点的值,同时从链表的头部开始向后移动 frontPointer 指针,实现了对单链表的回文性质进行检查。函数 isPalindrome 是入口函数,用于调用递归检查函数并返回结果。

假设有一个单链表的结构如下所示:

1 -> 2 -> 3 -> 2 -> 1

这个链表是一个回文链表,因为正着读和倒着读都是相同的。

现在我们来看看代码是如何检查这个链表是否为回文链表的:

  1. 初始化

    • 首先,我们调用 isPalindrome(head) 函数,其中 head 指向链表的头部。
    • frontPointer 全局变量被初始化为指向链表的头部,表示我们从链表的头部开始比较。
  2. 递归检查

    • 递归调用 recursivelyCheck(head) 函数,其中 currentNode 为当前节点,开始时指向链表的头部。
    • 我们进入递归函数,先判断当前节点是否为 NULL,如果不是则继续执行。
    • 递归调用 recursivelyCheck(currentNode->next),传入下一个节点,即 2 -> 3 -> 2 -> 1
    • 递归调用继续,直到 currentNode 指向链表的最后一个节点 1
    • 然后我们开始回溯,从链表的尾部向头部逐个比较节点的值,同时 frontPointer 从链表的头部向后移动。
    • currentNode 指向 1 时,我们开始比较最后一个节点的值 1frontPointer 指向的节点的值 1,它们相等,继续。
    • currentNode 指向 2frontPointer 指向链表的头部,比较节点的值 21,不相等,返回 false。
    • 回溯过程中,如果有不相等的节点值,则直接返回 false。

复杂度分析

时间复杂度:O(n),其中 n 指的是链表的大小。
空间复杂度:O(n),其中 nnn 指的是链表的大小。我们要理解计算机如何运行递归函数,在一个函数中调用一个函数时,计算机需要在进入被调用函数之前跟踪它在当前函数中的位置(以及任何局部变量的值),通过运行时存放在堆栈中来实现(堆栈帧)。在堆栈中存放好了数据后就可以进入被调用的函数。在完成被调用函数之后,他会弹出堆栈顶部元素,以恢复在进行函数调用之前所在的函数。在进行回文检查之前,递归函数将在堆栈中创建 n 个堆栈帧,计算机会逐个弹出进行处理。所以在使用递归时空间复杂度要考虑堆栈的使用情况。
这种方法不仅使用了 O(n) 的空间,且比第一种方法更差,因为在许多语言中,堆栈帧的开销很大(如 Python),并且最大的运行时堆栈深度为 1000(可以增加,但是有可能导致底层解释程序内存出错)。为每个节点创建堆栈帧极大的限制了算法能够处理的最大链表大小。

方法三:快慢指针

思路

避免使用 O(n) 额外空间的方法就是改变输入。

我们可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。比较完成后我们应该将链表恢复原样。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。

该方法虽然可以将空间复杂度降到 O(1),但是在并发环境下,该方法也有缺点。在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执行过程中链表会被修改。

算法

整个流程可以分为以下五个步骤:

  1. 找到前半部分链表的尾节点。
  2. 反转后半部分链表。
  3. 判断是否回文。
  4. 恢复链表。
  5. 返回结果。

执行步骤一,我们可以计算链表节点的数量,然后遍历链表找到前半部分的尾节点。

我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。

若链表有奇数个节点,则中间的节点应该看作是前半部分。

步骤二可以使用「反转链表」问题中的解决方法来反转链表的后半部分。

步骤三比较两个部分的值,当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点。

步骤四与步骤二使用的函数相同,再反转一次恢复链表本身。

代码

// 反转单链表
struct ListNode* reverseList(struct ListNode* head) {// 初始化前一个节点指针为 NULLstruct ListNode* prev = NULL;// 当前节点指针指向头节点struct ListNode* curr = head;// 遍历链表while (curr != NULL) {// 保存当前节点的下一个节点struct ListNode* nextTemp = curr->next;// 当前节点的 next 指针指向前一个节点curr->next = prev;// 更新 prev 指针为当前节点prev = curr;// 更新 curr 指针为下一个节点curr = nextTemp;}// 返回反转后的链表头节点return prev;
}// 找到链表的前半部分的尾节点
struct ListNode* endOfFirstHalf(struct ListNode* head) {// 初始化快慢指针都指向头节点struct ListNode* fast = head;struct ListNode* slow = head;// 快指针每次移动两步,慢指针每次移动一步,直到快指针到达链表末尾while (fast->next != NULL && fast->next->next != NULL) {fast = fast->next->next;slow = slow->next;}// 返回慢指针指向的节点,即前半部分链表的尾节点return slow;
}// 判断链表是否为回文链表
bool isPalindrome(struct ListNode* head) {// 如果链表为空,则是回文链表if (head == NULL) {return true;}// 找到前半部分链表的尾节点并反转后半部分链表struct ListNode* firstHalfEnd = endOfFirstHalf(head);struct ListNode* secondHalfStart = reverseList(firstHalfEnd->next);// 判断是否回文struct ListNode* p1 = head;struct ListNode* p2 = secondHalfStart;bool 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;
}

复杂度分析

时间复杂度:O(n),其中 n 指的是链表的大小。

空间复杂度:O(1)。我们只会修改原本链表中节点的指向,而在堆栈上的堆栈帧不超过 O(1)。

作者:力扣官方题解
链接:https://leetcode.cn/problems/palindrome-linked-list/solutions/457059/hui-wen-lian-biao-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Uniapp真机调试:手机端访问电脑端的后端接口解决

Uniapp真机调试&#xff1a;手机端访问电脑端的后端接口解决 1、前置操作 HBuilderX -> 运行 -> 运行到手机或模拟器 -> 运行到Android App基座 少了什么根据提示点击下载即可 使用数据线连接手机和电脑 手机端&#xff1a;打开开发者模式 -> USB调试打开手机端&…

Unity类银河恶魔城学习记录7-1 P67 Sword Throw Skill State源代码

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili Sword_Skill.cs using System.Collections; using System.Collections.Gen…

Kubernetes基础(十五)-k8s网络通信

1 k8s网络类型 2 Pod网络 2.1 同一pod内不同容器通信 Pod是Kubernetes中最小的可部署单元&#xff0c;它是一个或多个紧密关联的容器的组合&#xff0c;这些容器共享同一个网络命名空间和存储卷&#xff0c;因此Pod中的所有容器都共享相同的网络命名空间和IP地址——PodIP&a…

git flow与分支管理

git flow与分支管理 一、git flow是什么二、分支管理1、主分支Master2、开发分支Develop3、临时性分支功能分支预发布分支修补bug分支 三、分支管理最佳实践1、分支名义规划2、环境与分支3、分支图 四、git flow缺点 一、git flow是什么 Git 作为一个源码管理系统&#xff0c;…

Vulnhub靶机:hackable3

一、介绍 运行环境&#xff1a;Virtualbox 攻击机&#xff1a;kali&#xff08;10.0.2.15&#xff09; 靶机&#xff1a;hackable3&#xff08;10.0.2.53&#xff09; 目标&#xff1a;获取靶机root权限和flag 靶机下载地址&#xff1a;https://www.vulnhub.com/entry/hac…

基于springboot超市进销存系统源码和论文

随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;超市进销存系统也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&#xff0c;人工管理显然已无法应对时代的变化&#xff0c;而超…

【代码】Processing笔触手写板笔刷代码合集

代码来源于openprocessing&#xff0c;考虑到国内不是很好访问&#xff0c;我把我找到的比较好的搬运过来&#xff01; 合集 参考&#xff1a;https://openprocessing.org/sketch/793375 https://github.com/SourceOf0-HTML/processing-p5.js/tree/master 这个可以体验6种笔触…

sheng的学习笔记-网络爬虫scrapy框架

基础知识&#xff1a; scrapy介绍 何为框架&#xff0c;就相当于一个封装了很多功能的结构体&#xff0c;它帮我们把主要的结构给搭建好了&#xff0c;我们只需往骨架里添加内容就行。scrapy框架是一个为了爬取网站数据&#xff0c;提取数据的框架&#xff0c;我们熟知爬虫总…

TI毫米波雷达开发——High Accuracy Demo 串口数据接收及TLV协议解析 matlab 源码

TI毫米波雷达开发——串口数据接收及TLV协议解析 matlab 源码 前置基础源代码功能说明功能演示视频文件结构01.bin / 02.binParseData.mread_file_and_plot_object_location.mread_serial_port_and_plot_object_location.m函数解析configureSport(comportSnum)readUartCallback…

LMDeploy 大模型量化部署实践

在浦语的MDeploy大模型量化部署实践课程中&#xff0c;可能需要完成的任务包括&#xff1a; 大模型部署背景 2、LMDeploy简介 环境配置&#xff1a;这个部分你需要安装并设置相关的开发工具和库。这可能包括Python环境、LMDeploy库等等。你需要明确写出你使用的操作系统以及安装…

UML 2.5图形库

UML 2.5图形库 drawio是一款强大的图表绘制软件&#xff0c;支持在线云端版本以及windows, macOS, linux安装版。 如果想在线直接使用&#xff0c;则直接输入网址drawon.cn或者使用drawon(桌案), drawon.cn内部完整的集成了drawio的所有功能&#xff0c;并实现了云端存储&#…

Java编程练习之类的继承

1.创建银行卡类&#xff0c;并分别设计两个储蓄卡和信用卡子类。 import javax.swing.plaf.BorderUIResource;import java.util.Scanner;class Card {int Id; //银行卡&#xff1b;int password; //密码&#xff1b;double balance2000; //账户存款金额&#xff1b;String A…

【SpringBootStarter】自定义全局加解密组件

【SpringBootStarter】 目的 了解SpringBoot Starter相关概念以及开发流程实现自定义SpringBoot Starter(全局加解密)了解测试流程优化 最终引用的效果&#xff1a; <dependency><groupId>com.xbhog</groupId><artifactId>globalValidation-spring…

《MySQL 简易速速上手小册》第3章:性能优化策略(2024 最新版)

文章目录 3.1 查询优化技巧3.1.1 基础知识3.1.2 重点案例&#xff1a;电商平台商品搜索3.1.3 拓展案例 1&#xff1a;博客平台的文章检索3.1.4 拓展案例 2&#xff1a;用户登录查询优化 3.2 索引和查询性能3.2.1 基础知识3.2.2 重点案例&#xff1a;电商平台的订单历史查询3.2.…

Java中“==”和equals方法的区别

目录 一、“”举例 二、equals举例 区别如下&#xff1a; &#xff08;1&#xff09;“”既可以用在基本数据类型&#xff0c;也可以用在引用数据类型&#xff1b;如果用在基本数据类型上&#xff0c;那么比较时比较的是具体的值&#xff0c;如果用在引用数据类型&#xff0c…

React+Antd实现省、市区级联下拉多选组件(支持只选省不选市)

1、效果 是你要的效果&#xff0c;咱们继续往下看&#xff0c;搜索面板实现省市区下拉&#xff0c;原本有antd的Cascader组件&#xff0c;但是级联组件必须选到子节点&#xff0c;不能只选省&#xff0c;满足不了页面的需求 2、环境准备 1、react18 2、antd 4 3、功能实现 …

创建一个VUE项目(vue2和vue3)

背景&#xff1a;电脑已经安装完vue2和vue3环境 一台Mac同时安装vue2和vue3 https://blog.csdn.net/c103363/article/details/136059783 创建vue2项目 vue init webpack "项目名称"创建vue3项目 vue create "项目名称"

没更新的日子也在努力呀,布局2024!

文章目录 ⭐ 没更新的日子也在努力呀⭐ 近期的一个状态 - 已圆满⭐ 又到了2024的许愿时间了⭐ 开发者要如何去 "创富" ⭐ 没更新的日子也在努力呀 感觉很久没有更新视频了&#xff0c;好吧&#xff0c;其实真的很久没有更新短视频了。最近的一两个月真的太忙了&#…

Linux(Ubuntu) 环境搭建:Nginx

注&#xff1a;服务器默认以root用户登录 NGINX 官方网站地址&#xff1a;https://nginx.org/en/NGINX 官方安装文档地址&#xff1a;https://nginx.org/en/docs/install.html服务器的终端中输入以下指令&#xff1a; # 安装 Nginx apt-get install nginx # 查看版本信息 ngi…

Java:字符集、IO流 --黑马笔记

一、字符集 1.1 字符集的来历 我们知道计算机是美国人发明的&#xff0c;由于计算机能够处理的数据只能是0和1组成的二进制数据&#xff0c;为了让计算机能够处理字符&#xff0c;于是美国人就把他们会用到的每一个字符进行了编码&#xff08;所谓编码&#xff0c;就是为一个…