【算法】876.链表的中间结点--通俗讲解

106 阅读6分钟

一、题目是啥?一句话说清

给定一个单链表,找到并返回链表的中间节点。如果有两个中间节点(链表长度为偶数),返回第二个中间节点。

示例:

  • 输入:1 → 2 → 3 → 4 → 5

  • 输出:节点3

  • 输入:1 → 2 → 3 → 4 → 5 → 6

  • 输出:节点4(第二个中间节点)

二、解题核心

使用快慢指针法:快指针每次走两步,慢指针每次走一步。当快指针到达链表末尾时,慢指针正好在中间位置。

这就像两个人赛跑,一个人跑得快(每次两步),一个人跑得慢(每次一步)。当快的人跑到终点时,慢的人正好在中间位置。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 快慢指针的速度差

  • 是什么:快指针每次移动两个节点,慢指针每次移动一个节点。
  • 为什么重要:这种速度差确保了当快指针到达末尾时,慢指针正好在中间位置。

2. 循环终止条件

  • 是什么:循环继续的条件是快指针不为空且快指针的下一个节点不为空。
  • 为什么重要:这确保了快指针可以安全地移动两步,避免空指针异常。

3. 处理偶数长度链表

  • 是什么:当链表长度为偶数时,有两个中间节点,题目要求返回第二个中间节点。
  • 为什么重要:快慢指针法自然满足这个要求,因为当快指针无法继续移动时,慢指针会停在第二个中间节点。

四、看图理解流程(通俗理解版本)

情况1:奇数长度链表 1 → 2 → 3 → 4 → 5

  1. 初始状态:快指针和慢指针都指向节点1。
  2. 第一步
    • 快指针移动两步:从1到3
    • 慢指针移动一步:从1到2
  3. 第二步
    • 快指针移动两步:从3到5
    • 慢指针移动一步:从2到3
  4. 结束:快指针到达末尾(5),慢指针在中间节点3。

情况2:偶数长度链表 1 → 2 → 3 → 4 → 5 → 6

  1. 初始状态:快指针和慢指针都指向节点1。
  2. 第一步
    • 快指针移动两步:从1到3
    • 慢指针移动一步:从1到2
  3. 第二步
    • 快指针移动两步:从3到5
    • 慢指针移动一步:从2到3
  4. 第三步
    • 快指针移动两步:从5到null(因为5后面是6,再后面是null)
    • 慢指针移动一步:从3到4
  5. 结束:快指针到达末尾(null),慢指针在第二个中间节点4。

五、C++ 代码实现(附详细注释)

#include <iostream>
using namespace std;

// 链表节点定义
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        // 快慢指针都从头节点开始
        ListNode* slow = head;
        ListNode* fast = head;
        
        // 循环条件:快指针不为空且快指针的下一个节点不为空
        // 这样确保快指针可以安全地移动两步
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;      // 慢指针移动一步
            fast = fast->next->next; // 快指针移动两步
        }
        
        // 循环结束时,慢指针指向中间节点
        return slow;
    }
};

// 辅助函数:打印链表
void printList(ListNode* head) {
    while (head != nullptr) {
        cout << head->val << " ";
        head = head->next;
    }
    cout << endl;
}

// 测试代码
int main() {
    // 测试用例1:奇数长度链表 1->2->3->4->5
    ListNode* head1 = new ListNode(1);
    head1->next = new ListNode(2);
    head1->next->next = new ListNode(3);
    head1->next->next->next = new ListNode(4);
    head1->next->next->next->next = new ListNode(5);
    
    Solution solution;
    ListNode* middle1 = solution.middleNode(head1);
    cout << "奇数长度链表中间节点: " << middle1->val << endl; // 输出:3
    
    // 测试用例2:偶数长度链表 1->2->3->4->5->6
    ListNode* head2 = new ListNode(1);
    head2->next = new ListNode(2);
    head2->next->next = new ListNode(3);
    head2->next->next->next = new ListNode(4);
    head2->next->next->next->next = new ListNode(5);
    head2->next->next->next->next->next = new ListNode(6);
    
    ListNode* middle2 = solution.middleNode(head2);
    cout << "偶数长度链表中间节点: " << middle2->val << endl; // 输出:4
    
    // 释放内存(简单示例)
    // 实际应用中需要更完整的释放
    return 0;
}

六、时间空间复杂度

  • 时间复杂度:O(n),其中n是链表长度。快指针遍历了整个链表(每次两步),但总时间仍然是O(n)。
  • 空间复杂度:O(1),只使用了两个指针,没有使用额外空间。

七、注意事项

  • 空链表处理:如果链表为空,函数会返回nullptr,这是正确的,因为空链表没有中间节点。
  • 单节点链表:如果链表只有一个节点,快指针无法移动(fast->next为null),直接返回头节点。
  • 指针移动顺序:先移动快指针,再移动慢指针,或者反过来都可以,因为每次循环都会移动两个指针。
  • 循环条件:条件fast != nullptr && fast->next != nullptr确保了快指针可以安全地移动两步。如果只检查fast != nullptr,当fast在最后一个节点时,fast->next->next可能会导致空指针异常。
  • 偶数长度处理:题目要求有两个中间节点时返回第二个,快慢指针法自然满足这个要求,不需要特殊处理。

算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解
【算法】【链表】148.排序链表--通俗讲解
【算法】【链表】160.相交链表--通俗讲解
【算法】【链表】203.移除链表元素--通俗讲解
【算法】【链表】206.反转链表--通俗讲解
【算法】234.回文链表--通俗讲解
【算法】【链表】237.删除链表中的节点--通俗讲解
【算法】【链表】328.奇偶链表--通俗讲解
【算法】【链表】给单链表加一--通俗讲解
【算法】【链表】382.链表随机节点--通俗讲解
【算法】426.将二叉搜索树转化为排序的双向链表--通俗讲解
【算法】430.扁平化多级双向链表--通俗讲解


关注公众号,获取更多底层机制/ 算法通俗讲解干货!