【算法】【链表】148.排序链表--通俗讲解

66 阅读6分钟

算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解


通俗易懂讲解“排序链表”算法题目

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

给定一个链表,将其按升序排列并返回排序后的链表。

示例:

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

二、解题核心

使用归并排序算法,通过快慢指针找到链表中点,将链表分成两半,递归排序每半部分,然后合并两个有序链表。

这就像把一堆乱序的卡片分成两堆,分别排序,然后再把两堆有序的卡片合并成一堆有序的卡片。

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

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

1. 快慢指针找中点

  • 是什么:使用快慢指针技巧找到链表的中间节点,快指针每次走两步,慢指针每次走一步。
  • 为什么重要:这是分治策略的基础,通过找到中点可以将链表分成两个部分,分别进行排序,这是归并排序的核心思想。

2. 递归排序

  • 是什么:将链表分成两半后,递归地对每一半进行排序,直到链表长度为1或0(已经有序)。
  • 为什么重要:递归使得我们可以处理任意长度的链表,将大问题分解为小问题,是分治策略的实现方式。

3. 合并有序链表

  • 是什么:将两个已经排序的链表合并成一个有序链表,通过比较节点值,按顺序连接节点。
  • 为什么重要:这是归并排序的最后一步,也是关键步骤,需要正确比较和连接节点,确保合并后的链表有序。

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

假设链表为:4 → 2 → 1 → 3

  1. 找中点并分割

    • 快慢指针:慢指针从4开始,快指针从4开始。
    • 第一轮:慢指针走到2,快指针走到1。
    • 第二轮:慢指针走到1,快指针走到3(快指针无法再走两步,停止)。
    • 中点是节点2。将链表分成前半部分:4→2 和后半部分:1→3。
  2. 递归排序

    • 对前半部分4→2排序:
      • 找中点:慢指针从4开始,快指针从4开始。
      • 第一轮:慢指针走到2,快指针走到null(因为快指针走两步后为null)。
      • 分成4和2两个单节点链表。
      • 合并4和2:比较4和2,得到2→4。
    • 对后半部分1→3排序:
      • 找中点:慢指针从1开始,快指针从1开始。
      • 第一轮:慢指针走到3,快指针走到null。
      • 分成1和3两个单节点链表。
      • 合并1和3:比较1和3,得到1→3。
  3. 合并两个有序链表

    • 有两个有序链表:2→4 和 1→3。
    • 比较两个链表的头节点:2和1,1较小,取1。
    • 比较剩余部分:2→4 和 3,2和3,2较小,取2。
    • 比较剩余部分:4 和 3,3较小,取3。
    • 最后取4。
    • 合并结果:1→2→3→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* sortList(ListNode* head) {
        // 基线条件:空链表或单节点链表已经有序
        if (head == nullptr || head->next == nullptr) {
            return head;
        }
        
        // 使用快慢指针找到链表中点
        ListNode* slow = head;
        ListNode* fast = head->next;
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
        }
        
        // 将链表分成两半
        ListNode* mid = slow->next;
        slow->next = nullptr; // 切断链表
        
        // 递归排序两半
        ListNode* left = sortList(head);
        ListNode* right = sortList(mid);
        
        // 合并两个有序链表
        return merge(left, right);
    }
    
private:
    // 合并两个有序链表
    ListNode* merge(ListNode* l1, ListNode* l2) {
        ListNode dummy(0); // 虚拟头节点
        ListNode* tail = &dummy;
        
        while (l1 != nullptr && l2 != nullptr) {
            if (l1->val < l2->val) {
                tail->next = l1;
                l1 = l1->next;
            } else {
                tail->next = l2;
                l2 = l2->next;
            }
            tail = tail->next;
        }
        
        // 将剩余部分连接到尾部
        tail->next = (l1 != nullptr) ? l1 : l2;
        
        return dummy.next;
    }
};

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

// 测试代码
int main() {
    // 创建示例链表:4->2->1->3
    ListNode* head = new ListNode(4);
    head->next = new ListNode(2);
    head->next->next = new ListNode(1);
    head->next->next->next = new ListNode(3);
    
    Solution solution;
    ListNode* result = solution.sortList(head);
    
    printList(result); // 输出:1 2 3 4
    
    return 0;
}

六、时间空间复杂度

  • 时间复杂度:O(n log n),其中n是链表长度。归并排序的时间复杂度是O(n log n),每次分割需要O(n)时间,分割深度为O(log n)。
  • 空间复杂度:O(log n),主要来自递归调用栈的深度,最坏情况下递归深度为O(log n)。

七、注意事项

  • 递归基线条件:当链表为空或只有一个节点时,直接返回,因为已经有序。
  • 快慢指针找中点:注意快指针的起始位置,通常让快指针从head->next开始,这样可以确保当链表长度为偶数时,慢指针指向第一个中间节点。
  • 链表分割:在找到中点后,必须将中点的next设置为null,以真正将链表分成两半。
  • 合并有序链表:使用虚拟头节点可以简化合并操作,避免处理空链表的情况。
  • 内存管理:在C++中,需要注意内存管理,但本题不需要创建新节点,只是重新连接现有节点。
  • 性能考虑:归并排序是链表排序的最佳选择之一,因为它不需要随机访问,且时间复杂度较好。对于链表,归并排序通常比快速排序更适用。

算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解