详细介绍:对于返回倒数第 k 个节点、链表的回文结构、链表相交题目的解析

news/2025/11/9 18:18:24/文章来源:https://www.cnblogs.com/yangykaifa/p/19204639

开篇介绍:

hello 大家,我又来了,本篇博客依旧是为大家带来三道题的解析,那么我们废话少说,直接进入正题,还是老样子,我们先看题目链接:

面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)https://leetcode.cn/problems/kth-node-from-end-of-list-lcci/description/链表的回文结构_牛客题霸_牛客网https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId=182&&tqId=34762&sourceUrl=https%3A%2F%2Fwww.nowcoder.com%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AC%2594%25E9%259D%25A2%25E8%25AF%2595%25E7%25AF%2587%26topicId%3D295面试题 02.07. 链表相交 - 力扣(LeetCode)https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/description/接下来,我们直接进入题目解析:

面试题 02.02. 返回倒数第 k 个节点:

这道题,其实难度不大,我们可以直接秒,我们先看题目:

题意分析:

这道题要求我们找出单向链表中倒数第 k 个节点,并返回该节点的值。

关键信息提取

  • 数据结构:单向链表,每个节点包含一个值 val 和指向下一个节点的指针 next
  • 目标:找到链表中倒数第 k 个节点的值。
  • 条件:给定的 k 是有效的,无需处理 k 无效(如 k 大于链表长度等)的情况。
  • 示例辅助理解:输入链表 1->2->3->4->5k = 2,输出为 4,因为倒数第 2 个节点是值为 4 的节点。

解析:

对于这道题,咱也不讲什么乱七八糟的解法,我们直接用传奇解法:快慢指针,解决本题,大家有不了解快慢指针的,可以看一下这篇博客:对于单链表相关经典算法题:206. 反转链表及876. 链表的中间结点的解析-CSDN博客

只不过在本题中,我们的快指针不是每次走两步。

因为题目要找链表的倒数第k个节点,那么也就是说,我们先看图:

要返回倒一个节点的数据,那么这又和快慢指针有什么关系呢,根据前面所学的知识,我们知道,fast指针在为NULL的时候,终止循环,也就是说fast指针运动到了图中的NULL所示位置,那么我们不妨看看此时fast指针距离倒数第一个节点的距离为多少,巧了,正好是1,即k,那么按照惯例来说,我们的slow指针也就要在那是运动到了倒数第一个节点,那么究竟要怎么实现呢?

其实和上面说的类似,我们要让slow指针在fast指针运行到NULL的时候就运行到倒数第k个节点,那么,换句话来说,此时slow指针和fast指针的距离不正好是k吗诸位,再换句话来说就是,我们要让slow指针和fast指针保持k个节点的距离,如此,才能正好达到我们上述所说。

那么,我们又知道,在初始化的时候,我们是把fast和slow指针同时设置在头结点,那么为了让slow指针和fast指针保持k个节点的距离,我们就得在slow指针不移动的情况下,让fast指针先行k个节点,这一步很简单,我们直接借助while循环:

// 双指针法寻找倒数第k个节点:快指针先移动k步
sl* fast = head;  // 快指针,用于先行探路
sl* slow = head;  // 慢指针,最终指向倒数第k个节点
// sl* cur = head;  // 此变量在当前逻辑中未使用,可注释或删除
// 快指针先行移动k步
while (k > 0) {fast = fast->next;  // 快指针向前移动k--;                // 计数减1,直到移动k步
}

但是在这里我想给大家强调一下,大家可能会觉得说相隔k个节点就代表说两个节点之间要间隔k个节点,其实这是错的,准确来说,相隔k个节点是代表说两个节点之间要间隔k-1个节点,为什么呢?那是因为我们实际上的相隔k个节点指的是前面的那个节点要走几步才能到后面的那一个节点,就那k=2来说,它的意思是说,前面的那个节点要走2步才能到后面的那一个节点,而是不是当这两个节点之间间隔一个节点的时候,能够达到这个效果(即begin=begin->nex->next),要是中间间隔这着两个节点,那就要走4步,那么就不行,这里希望大家注意一下,这个理解很重要。

接下来,我们就可以让slow指针和fast指针同时一起移动相同的步数了,如此一来,当fast指针运动到NULL的时候,slow指针正好就停在倒数第k个节点,完整代码如下:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
typedef struct ListNode sl;
int kthToLast(struct ListNode* head, int k) {sl* fast=head;sl* slow=head;sl* cur=head;while(k){fast=fast->next;k--;}//出来循环之后,fast正好在距离头结点k个节点的距离的节点的前一个节点while(fast!=NULL){fast=fast->next;slow=slow->next;}return slow->val;//返回的是节点的数据
}

这个方法的原理就是,假设链表一共有n个节点,然后要返回倒数第k个节点,那么就是说,我们要让slow指针走到倒数第k个节点,换句话说就是,我们要让slow指针走n-k步,那么当我们让fast先走k步,那么此时fast距离链表的尾节点(这里是指NULL指针),

即如上图,距离是3步,也就是n-k步,那么当fast指针走到NULL时,那我们的slow指针也是走了n-k步,而n-k,不正好就对应着倒数第k个节点吗,原理就是这样。这个还是考验大家的数学功底和思维。

再给大家提供一个例子,帮助大家进一步理解:

假设我们有链表:1 -> 2 -> 3 -> 4 -> 5,要查找倒数第 2 个节点(即值为 4 的节点)

执行步骤:

  1. 初始化指针:fast = headslow = head(都指向节点 1

  2. 第一个循环(快指针先行 k 步):

    • k=2,进入循环:fast 移动到节点 2,k=1
    • k=1,进入循环:fast 移动到节点 3,k=0
    • 循环结束,fast 指向节点 3
  3. 第二个循环(快慢指针同时移动):

    • fast 不为空,fast 移动到节点 4slow 移动到节点 2
    • fast 不为空,fast 移动到节点 5slow 移动到节点 3
    • fast 不为空,fast 移动到 NULLslow 移动到节点 4
    • 循环结束,slow 指向节点 4
  4. 返回 slow->val 即 4,正确找到倒数第 2 个节点

另一个例子:链表 3 -> 7 -> 9 -> 5,查找倒数第 3 个节点

  • 第一个循环后,fast 指向 9
  • 第二个循环后,slow 指向 7(倒数第 3 个节点)
  • 函数返回 7

如此,我们本题就大功告成。

REAL483 链表的回文结构:

这道题是牛客网里的,牛客网也算是我们的老相识了,我们先看题目:

题意分析:

这道题要求我们设计一个算法,判断给定的单向链表是否为回文结构。

关键信息提取

  • 数据结构:单向链表。
  • 算法要求:时间复杂度为 O(n),额外空间复杂度为 O(1)。
  • 任务:判断链表是否为回文结构,即链表正序和逆序遍历得到的序列相同。
  • 输入输出:给定链表的头指针 A,返回一个布尔值(true 表示是回文结构,false 表示不是)。
  • 测试样例辅助理解:输入链表 1->2->2->1,输出 true,因为正序和逆序遍历得到的序列都是 1,2,2,1,是回文结构。

解析:

做这道题之前,我们不妨先来了解了解,什么算是回文链表

回文链表的核心定义是:从前往后遍历链表得到的节点值序列,和从后往前遍历得到的节点值序列完全一致。下面通过更详细的例子来理解:

一、是回文链表的例子

例子 1:简单偶数长度回文

链表结构:1 -> 2 -> 2 -> 1

  • 正序遍历(从表头到表尾):依次访问节点值 1221,序列为 [1, 2, 2, 1]
  • 逆序遍历(从表尾到表头):依次访问节点值 1221,序列为 [1, 2, 2, 1]
  • 结论:正序和逆序序列完全相同,是回文链表。
例子 2:简单奇数长度回文

链表结构:3 -> 5 -> 8 -> 5 -> 3

  • 正序遍历:节点值序列 [3, 5, 8, 5, 3]
  • 逆序遍历:节点值序列 [3, 5, 8, 5, 3]
  • 结论:正序和逆序序列完全相同,是回文链表(中间节点 8 是 “对称轴”,两侧对称)。
例子 3:单节点链表

链表结构:只有一个节点,值为 4

  • 正序遍历:序列 [4]
  • 逆序遍历:序列 [4]
  • 结论:是回文链表(单个元素自身对称)。
例子 4:重复元素的回文

链表结构:2 -> 2 -> 2 -> 2

  • 正序遍历:序列 [2, 2, 2, 2]
  • 逆序遍历:序列 [2, 2, 2, 2]
  • 结论:是回文链表(所有元素相同,天然对称)。

二、不是回文链表的例子

例子 1:1 -> 2 -> 3 -> 1 -> 2
  • 正序遍历:节点值依次为 12312,序列 [1, 2, 3, 1, 2]
  • 逆序遍历:节点值依次为 21321,序列 [2, 1, 3, 2, 1]
  • 结论:正序和逆序序列不同(比如第一个元素 1 vs 2,最后一个元素 2 vs 1),不是回文链表。
例子 2:非对称偶数长度

链表结构:1 -> 3 -> 2 -> 4

  • 正序遍历:序列 [1, 3, 2, 4]
  • 逆序遍历:序列 [4, 2, 3, 1]
  • 结论:序列完全不同,不是回文链表。
例子 3:非对称奇数长度

链表结构:5 -> 6 -> 7 -> 8 -> 9

  • 正序遍历:序列 [5, 6, 7, 8, 9]
  • 逆序遍历:序列 [9, 8, 7, 6, 5]
  • 结论:序列完全相反,不是回文链表。

总结来说,回文链表的本质是 “对称”—— 就像回文串(如 “level”“雷达”)一样,从前往后和从后往前读完全一致,即中间劈开,两边能不能对称罢了

知道了什么算是回文链表之后,我们才能进行用代码去判断一个链表是不是回文链表,方法有很多,我在这里就提供一种方法供大家参考。

我们拿回文链表 1 2 3 2 1,作为例子来进行分析,首先,我们经过上述,知道了回文链表的核心其实就是中间的一个节点,无论是奇数链表还是偶数链表,那么我们先抓着中间节点不放一下。

我们可以观察中间节点后的节点和中间节点前的节点的数据,究竟有什么关系,即1 2和2 1,我们能找到什么关系呢?

诶,聪明的你很快就能发现,当我们将中间节点后的2 1两个节点进行翻转一下变为1 2,那它是不是就和中间节点前面的节点变得一模一样了。

是的,上述就是我们解决这个问题的关键所在。

我们要找到中间节点,然后要去将中间节点后的节点进行反转,然后再与中间节点前面的节点进行一一的比较,一但有出现不一样的,就代表该链表不是回文链表,例如 1 2 3 1 2,翻转过后变为1 2 2 1 3,此时的第二个2是链表的中间节点,然后进行对比,第一个1和中间节点的2一对比,不相等,pass。

而1 2 3 2 1,翻转过后:1 2 1 2 3,一一对比,OK没问题,完全一样,过关。

但是我们例子是这样,却还是有一些点需要知道,

  • 第一,我们翻转是要把从中间节点到最后一个节点进行翻转,前面的节点是不动的,如1 2 3 2 1 翻转后变为1 2 1 2 3
  • 第二:我们要知道,由于我们是不用创建新链表的方法去翻转链表,所以即使翻转了之后,例如 1 2 1 2 3,此时的第一个2它的next指针还是指向3的,而3的next指针指向NULL,第二个1的next指针指向第二个2,第二个2的next指针指向3,即从原本的1->2->3->2->1变为

你要是问我为什么不把中间节点后的节点进行反转,不去动中间节点,那我的回答是:其实也可以这样子的,只是我做的时候去将中间节点一起反转了。

但是,说是这么说,其实我还是不推荐啦,因为链表长度为奇数的时候,或许还可行,但是要是为偶数的时候,比如 1 2 2 1,那么此时找到的中间节点,我们找到的是第二个2,那这个时候要是我们把中间节点后的节点进行翻转的话,那是不是就变成了1 2 2 1,没变化~,就是如此悲伤,而要是中间节点也跟着反转的话,就是 1 2 1 2,正好满足我们前面所述,所以,我还是推荐从中间节点开始往后进行反转,而后再按照顺序将第一个节点和新链表的中间节点一一比较,逐渐递增。

那么我们要怎么找到中间节点和进行翻转呢?诶,我之前写喽:对于单链表相关经典算法题:206. 反转链表及876. 链表的中间结点的解析-CSDN博客

在这里我们要用快慢指针和三指针迭代法哦。

到了这里,我们离结束本题就不远了,只差比较的处理。

我们知道,我们肯定要借助循环进行比较,那么循环终止的条件要是什么呢?

我们是将新链表的第一个节点和第中间个节点进行比较,所以我们的终止条件,也肯定和这两个有关。

大家看图,其实很简单,我们依旧看例子:翻转后变为1 2 1 2 3,进行比较:1和1相同,head和mid各自进入下一个进行比较,2和2相同,OK,两个再进入下一个,此时的head在第一个2,它的下一个是3.而mid在第二个2,它的下一个也是3,OK,再比较,又相等,再往下走,变为空了,比较无意义,结束比较,所以,我们的循环条件就是当head和mid指针都不为0的时候,进行循环。

偶数时的情况:

步骤 3:双指针比较(前半段 vs 反转后的后半段)

  • 前半段头指针:head(指向 1)。
  • 反转后半段头指针:mid(指向 1)。

循环条件:head != NULL && mid != NULL(两个指针都未走到末尾时比较)。

比较轮次head 指向的值prev 指向的值是否相等后续操作
第 1 轮11head = head->next(指向 2);mid = mid->next(指向 2
第 2 轮22head = head->next(指向 2);mid = p】mid->next(指向 NULL
循环结束head 指向 2prev 指向 NULL-所有对应位置都相等,判定为回文

由此,我们便可以将此题斩于马下。下面看完整代码:

/** 回文链表判断的C语言实现* 回文链表定义:正序遍历与逆序遍历得到的节点值序列完全相同的链表* 例如:1->2->2->1 是回文链表;1->2->3 不是回文链表*/
// 链表节点结构体定义
// 每个节点包含一个整数值和指向下一个节点的指针
struct ListNode {int val;               // 节点存储的数值struct ListNode *next; // 指向下一个节点的指针
};
// 为结构体创建别名sl,简化代码书写
typedef struct ListNode sl;
/*** 创建新的链表节点* @param val 节点要存储的数值* @return 成功返回新节点的指针,失败返回NULL*/
sl* createNode(int val) {// 分配内存空间,大小为一个ListNode结构体sl* node = (sl*)malloc(sizeof(sl));// 检查内存分配是否成功if (node == NULL) {return NULL; // 内存分配失败,返回空指针}node->val = val;   // 设置节点的数值node->next = NULL; // 新节点默认不指向任何节点return node;       // 返回创建的节点
}
/*** 寻找链表的中间节点(快慢指针法)* 算法原理:快指针每次移动2步,慢指针每次移动1步* 当快指针到达链表末尾时,慢指针恰好指向中间节点* @param phead 链表的头节点指针* @return 中间节点的指针*/
sl* midsl(sl* phead) {// 初始化快慢指针,都指向头节点sl* fast = phead;  // 快指针:每次走2步sl* slow = phead;  // 慢指针:每次走1步// 循环条件:快指针不为空且快指针的下一个节点也不为空// 保证fast->next->next不会访问空指针while (fast && fast->next) {fast = fast->next->next;  // 快指针移动2步slow = slow->next;        // 慢指针移动1步}// 当循环结束时,slow指向中间节点// 对于偶数长度链表:指向后半段的第一个节点// 对于奇数长度链表:指向正中间的节点return slow;
}
/*** 反转链表(三指针迭代法)* 算法原理:通过三个指针逐步反转节点的指向关系* @param mid 要反转的链表的头节点(原链表的中间节点)* @return 反转后新链表的头节点*/
sl* reversesl(sl* mid) {sl* n1 = NULL;       // 指向当前节点的前一个节点(初始为NULL)sl* n2 = mid;        // 指向当前正在处理的节点(初始为中间节点)// 循环直到当前节点为空(遍历完所有节点)while (n2) {sl* n3 = n2->next;  // 保存当前节点的下一个节点(防止断链)n2->next = n1;      // 反转当前节点的指向:指向它的前一个节点n1 = n2;            // 前指针向后移动:指向当前节点n2 = n3;            // 当前指针向后移动:指向下一个节点}// 循环结束后,n1指向原链表的最后一个节点,即反转后新链表的头节点return n1;
}
/*** 判断链表是否为回文结构* 算法步骤:* 1. 找到链表的中间节点* 2. 反转中间节点之后的后半段链表* 3. 比较前半段链表与反转后的后半段链表* @param A 链表的头节点指针* @return 是回文链表返回true,否则返回false*/
bool chkPalindrome(sl* A) {// 边界条件处理:空链表或只有一个节点的链表都是回文链表if (A == NULL || A->next == NULL) {return true;}// 步骤1:找到链表的中间节点sl* mid = midsl(A);// 步骤2:反转中间节点之后的后半段链表,得到新的头节点sl* newsl = reversesl(mid);// 步骤3:比较前半段与反转后的后半段// 循环条件:两个指针都未到达各自的末尾while (A && newsl) {// 若对应位置的节点值不相等,说明不是回文链表if (A->val != newsl->val) {return false;}// 两个指针分别向后移动一步,继续比较下一个节点A = A->next;newsl = newsl->next;}// 所有对应节点值都相等,是回文链表return true;
}

搞定。

面试题 02.07. 链表相交:

这道题,wos,绝对是单链表的经典中的经典的题目。我们先看题目:

题意分析:

这道题要求找出两个单链表相交的起始节点,若不相交则返回 null,且要保证函数返回后链表结构不变,还需设计时间复杂度 O(n)、空间复杂度 O(1) 的解决方案。

关键信息提取

  • 数据结构:两个单链表,无环。
  • 任务:找到两链表相交的起始节点,无交点则返回 null
  • 约束条件:返回结果后链表需保持原始结构;时间复杂度 O(n),空间复杂度 O(1)。
  • 示例辅助理解:如示例 1 中两链表在值为 8 的节点相交;示例 3 中两链表无交点,返回 null

解析:

对于这道题,我们同样要进行画图:

就拿题目给的例子吧,根据题目要求,我们首先要判断,这两个链表是不是相交的,说到相交,很多人觉得可能是这样子的相交:

但是是这样子的嘛,其实不是的,如果是这么相交的话,那链表不就变成了这样子:

所以,相交只能是这样子的:

这一点希望大家注意。

那么我们要如何判断两个链表是不是相交的呢?

其实很简单,我们可以看到

两个链表从相交开始到最后一个节点,都是相同的,那么这就给了我们可乘之机。

我们可以分别遍历两个链表,遍历到两个链表的尾节点,然后看一下两个尾节点的地址是否相同,相同就代表两个链表的最后一个节点是同一个节点,这里注意,一定要用地址进行比较,要是用节点中存储的数据的话,可能会有巧合,至于是什么巧合呢?大家不妨自己想想,我在这里不进行赘述。

那么在判断两个链表是否为相交链表之后,题目还要求我们返回出两个链表相交的第一个节点,那么这一步,我们又要怎么解决呢?

按照上图来看,其实就是要我们把c1节点找到,那么我们要怎么找到呢?

这就需要两个指针了,我们让两个指针分别处于两个链表,然后去对比,不同就一起进入下一个节点,一旦两个指针对应的节点相同了,就代表这个节点是两个链表相交的第一个节点。

但是这就有一个要求,那就是两个节点各自所处的链表的位置要是一样的,比如这样子:

这样子才能随着各自的一起移动(即cura走一步,curb也得跟着走一步)去找到相交的第一个节点。

如果是不在距离相交的第一个节点相同的节点上,比如这个:

那么当cura运动到c1的时候,curb才移动到b3,那么这两个就会一直错过~~~。

所以呢,我对上面总结一下就是,我们要让cura和curb处于相同的起跑线,因为一般来说,两个链表肯定一长一短,我们要是让cura和curb都从头结点出发,那肯定会嘎嘣脆,不行。

所以针对较短的,我们就让它从头结点出发,对于较长的,我们就不能让它从头结点出发,而是要移动它,让它这个链表的指针和较短链表的头结点处于同一起跑线上,即如下图:

那么我们要实现这一步呢?

大家看上图,我们想要让较长和较短的链表的各自指针处于同一起跑线上,我们就得让较长的那个链表的指针移动(较长较短链表节点的数量差)步,这就是关键。

那么你要是问我我是怎么得出的,很简单,画图,我们先得到了

这一副图,然后再去死命找关系即可。

所以,我们在一开始的遍历时,就要去各自统计两个链表的节点数量,然后再判断a和b哪个链表比较长,用长的那个减去短的那个,然后再让长的链表的指针从长链表的头结点移动(差)步,然后再让较短链表的指针从其头结点出发,这么一来,两个指针就在同一起跑线上了。

随后再进行比较并移动指针,当两个指针对应的节点的地址相同,就代表找到了两个链表相交的第一个节点,这个时候我们把两个指针中的随意一个返回即可。

具体代码如下:

typedef struct ListNode sl;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {sl* cura=headA;sl* curb=headB;int counta=1;int countb=1;while(cura){cura=cura->next;counta++;}while(curb){curb=curb->next;countb++;}//找到尾节点,比较尾节点的地址是否相同,相同就代表有相交//要记得用地址比较,不能用值,不难可能会发生巧合if(curb!=cura)//直接用指针比较{return NULL;}else{if(counta>countb){int cha=counta-countb;while(cha){headA=headA->next;cha--;}while(headA!=headB){headA=headA->next;headB=headB->next;}return headA;}else{int cha=countb-counta;while(cha){headB=headB->next;cha--;}while(headA!=headB){headA=headA->next;headB=headB->next;}return headA;}}
}

但是对于上面这么长一串代码,大家有没有感觉麻烦,那么,为了简洁性,我在这里给大家提供一个假设法:

其实这个方法思路很简单,就是我们假设是A链表长,并创建新节点longsl去接收A链表的头结点,假设B链表短,再创建新节点shortsl去接收B链表的头结点。

然后再借用if语句判断一下,如果真的是B链表的长度大于A链表,我们就将longsl和shortsl调换,简简单单,而后我们再借助abs函数去接收两个链表长度之差,接着让我们所创建的longsl指针(此时它就是长链表(A长他就是A的头结点,B长它就是B的头结点))移动(差)步,如此一来,longsl和shortsl便处于同一起跑线(即距离相交的第一个节点相同),由此,我们就可以大幅减少代码长度,具体如下:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
typedef struct ListNode sl;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {sl* cura=headA;sl* curb=headB;int counta=1;int countb=1;while(cura){cura=cura->next;counta++;}while(curb){curb=curb->next;countb++;}//找到尾节点,比较尾节点的地址是否相同,相同就代表有相交//要记得用地址比较,不能用值,不难可能会发生巧合// if(curb!=cura)//直接用指针比较// {//     return NULL;// }// else// {//     if(counta>countb)//     {//         int cha=counta-countb;//         while(cha)//         {//             headA=headA->next;//             cha--;//         }//         while(headA!=headB)//         {//             headA=headA->next;//             headB=headB->next;//         }//         return headA;//     }//     else//     {//         int cha=countb-counta;//         while(cha)//         {//             headB=headB->next;//             cha--;//         }//         while(headA!=headB)//         {//             headA=headA->next;//             headB=headB->next;//         }//         return headA;//     }// }//假设法int gap=abs(counta-countb);sl* longsl=headA;sl* shortsl=headB;if(countb>counta){longsl=headB;shortsl=headA;}while(gap--){longsl=longsl->next;}while(longsl!=shortsl){longsl=longsl->next;shortsl=shortsl->next;}return longsl;
}

依旧是简简单单,那么到这里,我们本题也就结束喽。

结语:

当我们逐行拆解完这三道单链表经典题目,会发现看似独立的问题背后,其实藏着一套贯穿始终的 “解题逻辑”—— 从对链表遍历节奏的把控(快慢指针),到对链表结构的改造(局部反转),再到对两个链表关系的精准分析(长度对齐与交点定位),每一步都是对单链表 “指针指向” 这一核心特性的深度运用。这些题目或许不是算法领域中最难的,但却是理解 “如何用最少的资源解决问题” 的绝佳案例,尤其是在时间复杂度要求 O (n)、空间复杂度要求 O (1) 的约束下,更能倒逼我们跳出 “用额外空间存储数据” 的惯性思维,转而聚焦于链表本身的结构特性与指针操作的灵活性。

先看 “返回倒数第 k 个节点”,这道题的关键在于 “如何让一个指针精准停在倒数第 k 个位置”。我们没有选择先遍历链表统计长度、再回溯 k 步(虽然可行,但不够优雅),而是用快慢指针的 “距离差” 实现了 “一次遍历即定位”—— 让快指针先跑 k 步,再让两个指针同步前进,当快指针触达链表末尾时,慢指针恰好落在目标节点上。这种思路的巧妙之处在于,它把 “倒数第 k 个” 的抽象需求,转化为 “两个指针保持 k 步距离” 的具象操作,既避免了额外的长度统计步骤,也让代码逻辑更简洁。相信大家在后续遇到 “定位链表特定位置” 的问题时,比如找中间节点、找倒数第 n 个节点,都能想到用类似的 “指针距离控制” 思路解决。

再到 “判断链表的回文结构”,这道题则是对 “链表操作组合” 的考验。回文的核心是 “前后对称”,但单链表的单向性决定了我们无法直接从后往前遍历,于是我们拆解出 “找中间节点→反转后半段→前后对比” 三个步骤:先用快慢指针找到中间节点,把链表分成 “前半段” 和 “后半段”;再用三指针法反转后半段,让原本单向的后半段变成 “反向可遍历” 的结构;最后用两个指针分别从原链表头部和反转后的后半段头部同步前进,逐一对比节点值。整个过程没有用任何额外的数组或栈存储数据,完全依赖原地指针操作,既满足了空间复杂度 O (1) 的要求,也让我们更熟悉链表反转的细节 —— 比如反转时如何用 n1、n2、n3 三个指针避免断链,反转后哪个指针才是新的头节点。更重要的是,这道题教会我们:当遇到 “无法直接双向遍历” 的限制时,“改造链表局部结构” 是一种高效的破局思路。

最后是 “链表相交”,这道题的难点在于 “如何找到两个链表的第一个公共节点”。首先我们要明确一个关键前提:若两个链表相交,那么从相交节点开始到末尾的所有节点都是共享的,也就是说它们的尾节点必然是同一个(地址相同,而非值相同)。基于这个前提,我们先通过遍历统计两个链表的长度,判断尾节点是否一致(不一致则直接返回 null);再通过 “长度差对齐” 让两个链表的指针站在 “同一起跑线”—— 让长链表的指针先移动长度差的步数,确保后续同步前进时,两个指针能同时触达相交节点(若存在)。后来我们还优化了代码,用 “假设法” 简化了长链表与短链表的判断逻辑,避免了大量的 if-else 分支。这道题的启示在于,解决 “两个链表关系” 的问题时,首先要找到它们的共性特征(如尾节点是否相同),再通过 “补全长度差”“同步遍历” 等操作,把复杂的双链表问题转化为单链表的遍历问题,这种 “化繁为简” 的思路在算法题中尤为重要。

回顾这三道题,我们会发现单链表的算法题本质上是 “指针操作的艺术”—— 如何用最少的指针,通过最合理的移动逻辑,实现目标需求。快慢指针、三指针反转、长度对齐,这些技巧不是孤立的,而是可以组合使用的:比如在更复杂的 “带环链表” 问题中,我们会用到快慢指针判断环的存在,再用类似 “链表相交” 的思路找环的入口。因此,大家在学习这些题目时,不要只记住代码本身,更要理解每一步操作的逻辑的本质 —— 比如为什么快慢指针能找到中间节点?为什么反转后半段后对比就能判断回文?为什么长度对齐后同步遍历能找到相交节点?只有把这些 “为什么” 想清楚,才能在遇到新问题时举一反三,灵活运用。

算法学习从来不是一蹴而就的,可能一开始会对指针的移动逻辑感到困惑,会忘记反转链表时的指针顺序,会在长度统计时少算一个节点 —— 但这些都是正常的。建议大家在学习时多画图:用笔画出链表的结构,标注每个指针的位置,一步步模拟代码的执行过程。比如在 “链表相交” 中,画两个长度不同的链表,标出它们的长度差,模拟长链表指针移动对齐的过程;在 “回文链表” 中,画出反转前后的后半段结构,看对比时指针如何同步移动。画图不仅能帮我们理清思路,还能让我们快速发现代码中的错误(比如反转后返回了错误的头节点,或者对比时循环条件写错)。

最后,希望大家能把这三道题的思路沉淀下来,作为单链表算法的 “基础库”。后续再遇到链表相关的题目时,不妨先回想这些题目的解题逻辑,看看是否能复用或改造已有思路。算法的提升没有捷径,唯有多思考、多练习、多总结,才能在面对复杂问题时游刃有余。相信只要坚持下去,大家都能在数据结构与算法的学习之路上,走得更稳、更远!

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

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

相关文章

实用指南:TensorFlow2 Python深度学习 - TensorFlow2框架入门 - 自动微分和梯度

实用指南:TensorFlow2 Python深度学习 - TensorFlow2框架入门 - 自动微分和梯度pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-…

浏览器Blockstack.org全名字段输入限制缺失漏洞分析

本文详细分析了Blockstack浏览器端全名字段缺乏输入长度限制导致的服务中断风险,包括漏洞发现过程、安全影响评估以及开发团队与安全研究员的讨论过程。漏洞报告 #304073 - browser.blockstack.org 全名字段字符串大小…

2025年维修厂家推荐排行榜单:行业权威解析

摘要 随着制冷行业在2025年的快速发展,维修厂家在保障设备高效运行中扮演关键角色。本文基于行业数据和用户口碑,解析2025年维修厂家排行榜单,重点推荐优质服务商,并提供详细表单供参考,帮助用户选择可靠的合作伙…

2025年维修厂家口碑排行榜:专业制冷服务首选

摘要 2025年维修厂家行业正迎来技术革新与市场需求增长,制冷设备维修服务成为关键领域。本文基于行业数据与用户反馈,推出维修厂家推荐排行榜单,旨在帮助用户选择靠谱的合作伙伴。榜单涵盖口碑、实力、服务等多维度…

行业内专业的维修厂家功能亮点

摘要 随着制冷行业的快速发展,2025年维修厂家市场呈现出技术升级和服务多元化的趋势。本文基于行业数据和用户口碑,解析维修厂家的核心功能亮点,并提供一份权威的维修厂家推荐排行榜单,供用户参考选择。表单排名综…

Dask-权威指南-全-

Dask 权威指南(全)原文:annas-archive.org/md5/4f64056c14690c5478291f8391f41fa7 译者:飞龙 协议:CC BY-NC-SA 4.0第一章:理解 Dask DataFrames 的架构 Dask DataFrames 允许您扩展您的 pandas 工作流。Dask Da…

DevOps-文化中的协作指南-全-

DevOps 文化中的协作指南(全)原文:zh.annas-archive.org/md5/747ac673186de3c38ee667bd2f54b035 译者:飞龙 协议:CC BY-NC-SA 4.0序言 本书面向已经建立起领导地位以及正在走向领导地位的人士。它专注于有效协作—…

WGCLOUD磁盘告警有没有恢复通知

有的 WGCLOUD磁盘告警后,如果我们处理后,系统也会发送恢复通知

疑似 CSP-SB、CSP-JB、NOSb 考题泄露

每日一题,防止变蠢[!] NOSb 考题。 [?] CSP-JB 考题。 [.] CSP-SB 考题。 这种代码难度很低的清新小题还是很有意思的。[?] \(\text{mex}\times \min\)。[?] 树的边双连通分量。[?] 对于 \(n\le 10^5\) 的点 \(11…

人工智能团队的技术工具

人工智能团队的技术工具

C++之开始学习C++(二) - Invinc

本文记录了初步学习C++时容易遗忘的一些知识。本文记录了初步学习C++时容易遗忘的一些知识。“没有”main() 的例外程序在 Windows 编程中,可以编写一个动态链接库 (DLL) 模块,这是其他 Windows程序可以使用的代码。…

如何禁止谷歌浏览器更新提示

在快捷方式的目标中加入 --disable-background-networking重启浏览器即可

Azure-Arc-支持的面向多云的-Kubernetes-指南-全-

Azure Arc 支持的面向多云的 Kubernetes 指南(全)原文:zh.annas-archive.org/md5/d59529b1ef4e2848b60fd4981536534c 译者:飞龙 协议:CC BY-NC-SA 4.0前言 在科技和云计算领域,包括多云、容器、Kubernetes 和 De…

拓扑 AC 2025 线上 NOIP 联测 #2

100 + 25 + 10 + 0 = 135, Rank 7/19.久违的罚坐。[2025线上NOIP联测第三阶段] 模拟赛 2 链接:link 题解:暂无 时间:4.5h (2025.11.08 13:00~17:30) 题目数:4 难度:A B C D\(\color{#FFC116} 黄\)*1300估分:100 …

04--CSS基础(3) - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

P14462 【MX-S10-T3】『FeOI-4』寻宝游戏

P14462 【MX-S10-T3】『FeOI-4』寻宝游戏 P14462 【MX-S10-T3】『FeOI-4』寻宝游戏 - 洛谷 (luogu.com.cn) 分类讨论。\(len\ge 3\)。 找到一个目标桶 \(x\),把剩下的都扔进去。 设剩下的桶之中,个数和为 \(sum\),最…

完整教程:FocusAny 发布v1.1.0 插件搜索过滤,FAD文件优化,插件显示MCP服务

完整教程:FocusAny 发布v1.1.0 插件搜索过滤,FAD文件优化,插件显示MCP服务pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-fam…

11.9 模拟赛 T3

题意:将 \(n\) 个线段分成恰好 \(m\) 组,每个线段需要且只能分进一组。求这 \(m\) 组线段合法的得分之和最大是多少。一组线段的得分定义为它们的交的长度(区间长度为右端点减左端点)。一个方案合法,当且仅当每组…

CSP2025游记

早上到考场发现那一层两个考场一共就看到两种校服。今年好像不是按姓名字典序排的 J组 挺水的 T1 简单切了 T2 简单切了 T3 想了一会,切了 T4 想了一会,以为自己切了 赛后发现没开滚动数组好像会爆空间($512 \times…