【力扣 - 回文链表】

news2024/9/25 15:29:15

题目描述

给你一个单链表的头节点 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 用于存储链表节点的值,数组大小为 50001
    int vals[50001];
    // 定义一个变量 vals_num 用于记录数组中元素的个数,初始化为 0
    int vals_num = 0;

    // 遍历链表,将链表节点的值存储到数组 vals 中
    while (head != NULL) {
        // 将当前链表节点的值存储到数组 vals 中,并更新 vals_num
        vals[vals_num++] = head->val;
        // 移动到链表的下一个节点
        head = head->next;
    }

    // 使用双指针从数组 vals 的两端向中间遍历,比较对应位置的值是否相等
    // 注意这里只用了一个循环,本来想用双循环,但是双循环的话没有办法首尾对应
    for (int i = 0, j = vals_num - 1; i < j; ++i, --j) {
        // 如果对应位置的值不相等,则链表不是回文链表,返回 false
        if (vals[i] != vals[j]) {
            return false;
        }
    }
    // 如果双指针都遍历到了中间,且对应位置的值都相等,则链表是回文链表,返回 true
    return 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 null
        print_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 所指向的节点的值不相等,则链表不是回文,返回 false
        if (currentNode->val != frontPointer->val) {
            return false;
        }
        // 将 frontPointer 指向下一个节点,继续向后比较
        frontPointer = frontPointer->next;
    }
    // 如果链表遍历完成且没有发现不同,则链表是回文,返回 true
    return 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) {
    // 初始化前一个节点指针为 NULL
    struct 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.coloradmin.cn/o/1438968.html

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

相关文章

jsp教材管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 教材管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.0&…

网工内推 | 高级网工,IE认证优先,最高15K,五险一金

01 丰沃创新(北京)科技有限公司 招聘岗位&#xff1a;高级网络工程师 职责描述&#xff1a; 1. 主要负责移动营运商数据中心机房网络的维护工作&#xff1b; 2. 负责防火墙策略调整&#xff0c;负责交换机路由器等网络设备的配置&#xff1b; 3. 负责云专线的入网配置&#…

07:Kubectl 命令详解|K8S资源对象管理|K8S集群管理(重难点)

Kubectl 命令详解&#xff5c;K8S资源对象管理&#xff5c;K8S集群管理 kubectl管理命令kubectl get 查询资源常用的排错命令kubectl run 创建容器 POD原理pod的生命周期 k8s资源对象管理资源文件使用资源文件管理对象Pod资源文件deploy资源文件 集群调度的规则扩容与缩减集群更…

解析spritf和sscanf与模拟常用字符串函数strchr,strtok(二)

今天又来继续我们的字符串函数的文章&#xff0c;这也是最后一篇了。希望这两篇文章能让各位理解透字符串函数。 目录 strchr strtok sprintf和sscanf strchr strchr 是一个用于在字符串中查找特定字符首次出现位置的函数。以下是解析和模拟实现 strchr 函数的示例&…

缓存和分布式锁 笔记

概念 缓存的作用是减低对数据源的访问频率。从而提高我们系统的性能。缓存的流程图 缓存分类 本地缓存 把缓存数据存储在内存中(Map <String,Object>)&#xff0c;其实就是强引用&#xff0c;不会轻易被删除。 分布式缓存 数据冗余&#xff0c;效率不高 整合Redis &l…

计算机设计大赛 深度学习 YOLO 实现车牌识别算法

文章目录 0 前言1 课题介绍2 算法简介2.1网络架构 3 数据准备4 模型训练5 实现效果5.1 图片识别效果5.2视频识别效果 6 部分关键代码7 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于yolov5的深度学习车牌识别系统实现 该项目较…

新型RedAlert勒索病毒针对VMWare ESXi服务器

前言 RedAlert勒索病毒又称为N13V勒索病毒&#xff0c;是一款2022年新型的勒索病毒&#xff0c;最早于2022年7月被首次曝光&#xff0c;主要针对Windows和Linux VMWare ESXi服务器进行加密攻击&#xff0c;到目前为止该勒索病毒黑客组织在其暗网网站上公布了一名受害者&#x…

【芯片设计- RTL 数字逻辑设计入门 11.2 -- 状态机实现 移位运算与乘法 2】

文章目录 移位运算与乘法parameterparameter 特点parameter 基本语法parameter 示例局部参数局部参数示例 状态机代码实现VCS 仿真结果 文章 【芯片设计- RTL 数字逻辑设计入门 11.1 – 状态机实现 移位运算与乘法 1】 介绍了状态机&#xff0c;本篇文章主要就是使用状态机的方…

Vision Pro 5 月将在中国区发售;全球科技大厂 1 月已裁员 32000 人丨RTE 开发者日报 Vol.145

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…

Stable Diffusion 模型下载:Disney Pixar Cartoon Type A(迪士尼皮克斯动画片A类)

文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八案例九案例十 下载地址 模型介绍 目前还没有一个好的皮克斯迪士尼风格的卡通模型&#xff0c;所以我决定自己制作一个。这是将皮克斯风格模型与我自己的Loras合并在一起&#xff0c;创建一个通用的…

【Transformer(04/10) 】 Hugging Face手册-推理管道

一、说明 这里是Hugging Face手册第四部分&#xff0c;如何使用推理管道&#xff1b;即使您没有特定模式的经验或不熟悉模型背后的底层代码&#xff0c;您仍然可以使用它们通过 pipeline ()进行推理&#xff01; 二、推理管道 pipeline ()可以轻松使用Hub中的任何模型来推理任…

第三百一十三回

文章目录 1. 概念介绍2. 实现方法2.1 obscureText属性2.2 decoration属性 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何实现倒计时功能"相关的内容&#xff0c;本章回中将介绍如何实现密码输入框.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍…

实例分割论文阅读之:《Mask Transfiner for High-Quality Instance Segmentation》

1.摘要 两阶段和基于查询的实例分割方法取得了显著的效果。然而&#xff0c;它们的分段掩模仍然非常粗糙。在本文中&#xff0c;我们提出了一种高质量和高效的实例分割Mask Transfiner。我们的Mask Transfiner不是在规则的密集张量上操作&#xff0c;而是将图像区域分解并表示…

【Django】ORM关系映射

关系映射 在关系型数据库中&#xff0c;通常不会把所有数据都放在同一张表中&#xff0c;不易于扩展&#xff0c;常见的关系映射有&#xff1a; 一对一映射&#xff0c;如一个身份证对应一个人。 一对多映射&#xff0c;如一个班级可以有多个学生。 多对多映射&#xff0c;如…

基于SpringBoot和PostGIS的震中影响范围可视化实践

目录 前言 一、基础数据 1、地震基础信息 2、全国行政村 二、Java后台服务设计 1、实体类设计 2、Mapper类设计 3、控制器设计 三、前端展示 1、初始化图例 2、震中位置及影响范围标记 3、行政村点查询及标记 总结 前言 地震等自然灾害目前还是依然不能进行准确的预…

解密 ARMS 持续剖析:如何用一个全新视角洞察应用的性能瓶颈?

作者&#xff1a;饶子昊、杨龙 应用复杂度提升&#xff0c;根因定位困难重重 随着软件技术发展迭代&#xff0c;很多企业软件系统也逐步从单体应用向云原生微服务架构演进&#xff0c;一方面让应用实现高并发、易扩展、开发敏捷度高等效果&#xff0c;但另外一方面也让软件应…

每日一练:LeeCode-112、路径总和【二叉树+DFS+回溯】

本文是力扣LeeCode-112、路径总和 学习与理解过程&#xff0c;本文仅做学习之用&#xff0c;对本题感兴趣的小伙伴可以出门左拐LeeCode。 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有…

【闲来看源码】分析一下`ArrayUtils.contains()`这个方法的实现

【闲来看源码】分析一下ArrayUtils.contains()这个方法的实现 大家先来看源码 PreAuthorize("ss.hasPermi(system:user:remove)")Log(title "用户管理", businessType BusinessType.DELETE)DeleteMapping("/{userIds}")public AjaxResult remo…

flink反压及解决思路和实操

1. 反压原因 反压其实就是 task 处理不过来&#xff0c;算子的 sub-task 需要处理的数据量 > 能够处理的数据量&#xff0c;比如&#xff1a; 当前某个 sub-task 只能处理 1w qps 的数据&#xff0c;但实际上到来 2w qps 的数据&#xff0c;但是实际只能处理 1w 条&#…