1 题目
其实是做过的
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:

输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 。
示例 2:

输入:head = [1,2,3,4,5,6]
输出:[4,5,6]
解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。
提示:
- 链表的结点数范围是
[1, 100] 1 <= Node.val <= 100
2 代码实现(c)
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
ListNode *fast = head ;
ListNode *slow = head ;
while(fast != NULL && fast -> next!= NULL ){
fast = fast -> next -> next ;
slow = slow -> next;
}
return slow;
}
【我发现一个很奇怪的事情,居然不需要考虑奇偶性?妙就妙在while约束条件是很好的,两个非空时循环进行。】
这个寻找链表中间节点的算法确实巧妙地通过循环条件处理了奇偶节点数的情况,无需额外判断。
具体来说:
- 当链表节点数为奇数时,fast 指针最终会指向最后一个节点(fast->next 为 NULL),此时 slow 恰好指向正中间节点
- 当链表节点数为偶数时,fast 指针最终会指向 NULL(fast 为 NULL),此时 slow 恰好指向中间两个节点中的后一个(符合题目常见需求)
这种 "快慢指针" 的思路通过让 fast 指针每次走两步,slow 指针每次走一步,天然地利用了两者的速度差,在 fast 到达链表末尾时,slow 刚好处于中间位置。循环条件fast != NULL && fast->next != NULL完美覆盖了两种情况的终止条件,确实非常精妙。
3 题目
给定一个头节点为 head 的链表用于记录一系列核心肌群训练项目编号,请查找并返回倒数第 cnt 个训练项目编号。
示例 1:
输入:head = [2,4,7,8], cnt = 1
输出:8
提示:
1 <= head.length <= 1000 <= head[i] <= 1001 <= cnt <= head.length
4 代码实现(c)
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* trainingPlan(struct ListNode* head, int cnt) {
ListNode *fast = head;
ListNode *slow = head;
for(int i = 0 ; i < cnt ; i ++){
fast = fast -> next ;
if(fast == NULL ){
return NULL ;
}
}
while(fast != NULL ){
fast = fast -> next ;
slow = slow -> next ;
}
return slow;
}
遇到了错误。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* trainingPlan(struct ListNode* head, int cnt) {
ListNode *p = (ListNode*)malloc(sizeof(ListNode));
p -> next = head ;
ListNode *fast = p;
ListNode *slow = p;
while(fast != NULL && fast -> next != NULL ){
for(int i = 0 ; i < cnt ; i ++){
fast = fast -> next ;
}
fast = fast -> next ;
slow = slow -> next ;
}
p = slow ;
return p;
}
1 循环嵌套逻辑有问题
2 哑结点不适用
3 边界判断不对
在链表问题中使用 "哑节点"(dummy node)的核心作用是简化边界情况处理,尤其是当涉及到头节点可能被修改或删除时。不过你这段代码中对哑节点的使用似乎不太恰当,反而可能引入问题,我们来具体分析:
为什么通常需要哑节点?
以 "寻找倒数第cnt个节点" 为例,常见的边界场景是:
- 当链表长度刚好等于
cnt时,倒数第cnt个节点就是头节点(如链表1->2->3,cnt=3,结果是1)。 - 如果不使用哑节点,当
fast先移动cnt步后可能直接指向NULL,此时slow需要从head开始移动,逻辑上没问题,但处理头节点时容易出错。 - 更典型的场景是 "删除倒数第
cnt个节点":如果要删除的是头节点,直接操作head会很麻烦,而哑节点可以统一处理(让slow->next指向要删除的节点,无论是否是头节点)。
你这段代码的问题
你的代码中虽然定义了哑节点p,但逻辑上并没有利用到它的优势,反而可能导致错误:
- 循环条件错误 :外层
while(fast != NULL && fast->next != NULL)依然沿用了 "找中间节点" 的逻辑,不适合 "找倒数第cnt个节点",会导致指针移动步数异常。 - fast 移动逻辑错误 :内层
for循环移动cnt步后又额外移动 1 步,相当于每次让fast比slow多移cnt+1步,这与 "倒数第cnt个节点" 的距离要求(fast比slow多移cnt步)不符。 - 哑节点未起作用 :最终返回
slow(即p->next的某个节点),但哑节点的存在反而可能让初始指针位置计算混乱。
正确使用哑节点的示例(针对倒数第cnt个节点)
如果要用哑节点,可以这样写(更适合删除场景,查询场景其实可以不用):
cpp
struct ListNode* trainingPlan(struct ListNode* head, int cnt) {
ListNode *dummy = (ListNode*)malloc(sizeof(ListNode));
dummy->next = head; // 哑节点指向头节点,统一边界处理
ListNode *fast = dummy;
ListNode *slow = dummy;
// fast先移动cnt步(此时fast与slow的距离为cnt)
for (int i = 0; i < cnt; i++) {
if (fast == NULL) return NULL; // 处理cnt超出链表长度的情况
fast = fast->next;
}
// 同时移动fast和slow,直到fast到达尾节点(fast->next == NULL)
while (fast->next != NULL) {
fast = fast->next;
slow = slow->next;
}
// slow->next就是倒数第cnt个节点
return slow->next;
}
这里哑节点的作用是:当cnt等于链表长度时,slow会停在哑节点,slow->next刚好是头节点,避免了单独处理 "删除头节点" 的特殊情况。
总结
- 哑节点的核心价值是统一处理头节点和其他节点的逻辑,尤其在插入 / 删除操作中。
- 单纯的 "查询倒数第
cnt个节点" 可以不用哑节点,但用了也能简化逻辑(如上面的示例)。 - 你的代码问题不在哑节点本身,而在指针移动的逻辑和循环条件,需要按照 "先让 fast 多走
cnt步,再同步移动" 的思路修改。