Leetcode 24
1 题目
876. 链表的中间结点
给你单链表的头结点 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语言实现)
法一:快慢指针法
思路
- 慢指针每次走一步,快指针每次走两步
- 快指针为空或快指针的下一个指针为空时,slow指针指向的节点即为中间节点
- 注意:循环条件中 fast!=NULL 必须放在前面。
复杂度
- 时间复杂度: O(n) O(n) O(n)
- 空间复杂度: O(1) O(1) O(1)
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {ListNode* slow=head;ListNode* fast=head;while(fast!=NULL && fast->next!=NULL){slow=slow->next;fast=fast->next->next;}return slow;
}
【踩坑】:注意位与&和逻辑与&&的区别
这个函数用于查找单链表的中间节点,采用了 "快慢指针" 的经典算法。让我们详细分析其逻辑:
1. 算法核心思想
使用两个指针遍历链表:
- 慢指针 (slow):每次移动 1 步
- 快指针 (fast):每次移动 2 步
当快指针到达链表末尾时,慢指针恰好指向链表的中间节点。
2. 循环条件分析:fast != NULL && fast->next != NULL
这是一个逻辑与操作,需要两个条件同时满足才会执行循环体:
- 第一个条件
fast != NULL
:确保快指针没有超出链表范围 - 第二个条件
fast->next != NULL
:确保快指针的下一个节点存在
条件执行顺序
C 语言中逻辑与&&
运算符具有短路特性:
- 先判断第一个条件
fast != NULL
- 只有当第一个条件为真时,才会判断第二个条件
fast->next != NULL
- 如果第一个条件为假,直接跳过第二个条件的判断
这种执行顺序非常重要,它避免了对空指针进行->next
操作,防止程序崩溃。
3. 为什么不能用位与&
如果将逻辑与&&
换成位与&
,会导致严重问题:
- 逻辑错误:位与是对两个值的二进制位进行与操作,而不是判断两个条件是否同时成立
- 空指针访问:位与不具备短路特性,无论第一个条件是否为真,都会执行第二个条件的判断。当
fast
为NULL
时,执行fast->next
会导致空指针访问,程序崩溃
例如,当fast
为NULL
时:
- 使用
&&
:因第一个条件为假,不执行第二个条件,安全 - 使用
&
:会执行fast->next
,导致未定义行为(通常是程序崩溃)
4. 算法执行过程
- 对于偶数个节点:当快指针到达最后一个节点时,慢指针在中间两个节点的第二个节点
- 对于奇数个节点:当快指针到达末尾 (NULL) 时,慢指针在正中间节点
这种算法的时间复杂度是 O (n),空间复杂度是 O (1),是查找链表中间节点的最优解法。
法二:计数法
思路
- 第一次遍历链表记录节点总个数
- 第二次遍历到中间节点返回该节点
复杂度
- 时间复杂度: O(n) O(n) O(n)
- 空间复杂度: O(1) O(1) O(1)
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {//节点个数ListNode*pcur=head;int count=0;while(pcur){count++;pcur=pcur->next;}int mid=count/2;ListNode* midNode=head;while(mid--){midNode=midNode->next;}return midNode;
}
【踩坑】:循环条件
我写了一个错误的代码。
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {ListNode *cur = head;int count = 0;while(cur != NULL && cur -> next != NULL){count ++;cur = cur -> next;}int mid = count /2 ;ListNode *midNode = head;while(mid --){midNode = midNode -> next;}return midNode;}
这段代码的目的是找到链表的中间节点,但实现了逻辑错误,会导致在某些情况下返回错误的结果。我们来详细分析:
问题分析
代码采用了 "先计数再查找" 的思路,但计数逻辑存在缺陷:
-
计数错误
while(cur != NULL && cur -> next != NULL){count ++;cur = cur -> next; }
这个循环的终止条件有问题,导致
count
值不能正确反映链表的实际长度:- 对于 n 个节点的链表,实际会少统计 1 个节点
- 例如:3 个节点的链表只会计数 2 次
- 例如:4 个节点的链表只会计数 3 次
-
中间位置计算错误由于计数错误,导致
mid = count / 2
计算出的中间位置也是错误的:- 当链表有 3 个节点时,count 会得到 2,mid=1,最终返回第 2 个节点(正确)
- 当链表有 4 个节点时,count 会得到 3,mid=1,最终返回第 2 个节点(错误,应该返回第 3 个)
- 当链表有 5 个节点时,count 会得到 4,mid=2,最终返回第 3 个节点(正确)
- 当链表有 2 个节点时,count 会得到 1,mid=0,最终返回第 1 个节点(错误,应该返回第 2 个)
错误原因
根本问题在于循环的终止条件:cur != NULL && cur -> next != NULL
这个条件会在遍历到倒数第二个节点时就停止,导致最后一个节点没有被计数。
修正方案
应该先完整统计链表长度,再计算中间位置:
struct ListNode* middleNode(struct ListNode* head) {if (head == NULL) return NULL;ListNode *cur = head;int count = 0;// 修正1:正确统计所有节点while(cur != NULL) {count++;cur = cur->next;}// 修正2:正确计算中间位置int mid = count / 2;ListNode *midNode = head;while(mid--) {midNode = midNode->next;}return midNode;
}
为什么原代码会这样
原代码的作者可能混淆了两种遍历方式:
- 想统计节点总数,却用了类似 "快慢指针" 的终止条件
- 没有考虑到循环条件对计数结果的影响
- 没有测试偶数个节点的情况
这种错误在链表操作中很常见,尤其当涉及到边界条件处理时,需要仔细考虑各种长度的链表情况。