链表--排序链表

一、题目核心解析

1. 题目要求

给定单链表的头节点head,将链表按升序排列并返回排序后的链表。

  • 输入示例:head = [4,2,1,3] → 输出:[1,2,3,4]
  • 数据范围:链表节点数∈(0, 5×10⁴),节点值∈[-10⁵, 10⁵]
  • 进阶挑战:在O (n log n) 时间O (1) 空间内完成排序

2. 算法选型分析

链表的非连续存储特性,直接排除了数组中高效的随机访问类排序。结合复杂度要求,算法选型如下:

表格

排序算法 时间复杂度 空间复杂度 适配性
冒泡 / 插入排序 O(n²) O(1) ❌ 超时,无法处理大数据量
快速排序 平均 O (n log n) O(log n) ❌ 链表随机访问开销大,递归栈空间不满足进阶要求
堆排序 O(n log n) O(1) ❌ 链表实现复杂,无数组下标优势
归并排序 O(n log n) O(log n)/O(1) ✅ 最优解,完美适配链表分治与指针操作

归并排序的核心优势:分治思想天然适配链表,分割仅需快慢指针,合并仅调整指针无需额外空间,是唯一能同时满足时间与空间进阶要求的算法。

二、核心前置知识:链表操作基石

解决这道题前,需掌握两个核心基础操作,也是解题的 "工具包":

1. 快慢指针找中点(分割核心)

通过快慢指针将链表从中间拆分为左右两部分,为分治做准备。

  • 原理:慢指针slow每次走 1 步,快指针fast每次走 2 步,当fast到达链表尾时,slow指向中点。
  • 关键细节:快指针初始指向head->next,确保偶数长度链表时,slow指向前半段尾节点,便于精准分割。

2. 合并两个有序链表(合并核心)

将两个已升序排列的链表合并为一个新的升序链表,核心是双指针 + 虚拟头节点,无需创建新节点,仅调整指针指向。

  • 虚拟头节点dummy:简化边界处理,无需单独判断空链表,统一拼接逻辑。

三、解法一:自顶向下递归归并(清晰易上手)

1. 算法思路

遵循分治 "分割 - 治理 - 合并" 的核心逻辑,递归实现:

  1. 分割:用快慢指针找到链表中点,将链表拆分为左、右两部分;
  2. 治理:递归排序左、右两个子链表,直到子链表长度为 1(天然有序);
  3. 合并:将两个有序子链表合并为一个有序链表,回溯得到最终结果。

2. 完整 C++ 代码

cpp

运行

复制代码
/**
 * Definition for singly-linked list.
 * 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 || !head->next) {
            return head;
        }
        // 1. 快慢指针找中点,分割链表
        ListNode* mid = findMid(head);
        ListNode* rightHead = mid->next;
        mid->next = nullptr; // 切断左、右子链表,避免循环引用
        
        // 2. 递归排序左、右子链表
        ListNode* leftSorted = sortList(head);
        ListNode* rightSorted = sortList(rightHead);
        
        // 3. 合并两个有序链表
        return mergeTwoLists(leftSorted, rightSorted);
    }

private:
    // 快慢指针找链表中点(前半段尾节点)
    ListNode* findMid(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = head->next; // 关键:偶数长度时指向中点前一位
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }

    // 合并两个有序链表(核心工具函数)
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        // 虚拟头节点:简化边界处理
        ListNode dummy(0);
        ListNode* cur = &dummy;
        
        // 双指针遍历,按值拼接
        while (l1 && l2) {
            if (l1->val <= l2->val) {
                cur->next = l1;
                l1 = l1->next;
            } else {
                cur->next = l2;
                l2 = l2->next;
            }
            cur = cur->next;
        }
        
        // 拼接剩余节点
        cur->next = l1 ? l1 : l2;
        return dummy.next;
    }
};

3. 复杂度分析

  • 时间复杂度:O (n log n)。分割递归深度为 O (log n),每层合并操作遍历 O (n) 个节点,总复杂度为 O (n log n)。
  • 空间复杂度:O (log n)。递归调用栈的深度为链表长度的对数级,不满足进阶 O (1) 要求,但代码清晰易理解,适合入门与面试口述。

四、解法二:自底向上迭代归并(O (1) 空间最优解)

1. 算法思路

为满足进阶 O (1) 空间要求,用迭代替代递归,核心是 "从小到大逐步合并有序子链表":

  1. 初始化 :计算链表长度,定义子链表长度subLen = 1(初始仅单节点有序);
  2. 迭代合并 :按subLen切分链表为多个有序子链表,两两合并,每次合并后subLen翻倍(1→2→4→...);
  3. 终止条件 :当subLen >= 链表长度时,所有子链表合并完成,得到最终有序链表。

2. 完整 C++ 代码

cpp

运行

复制代码
class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (!head || !head->next) return head;
        
        // 1. 计算链表长度
        int len = 0;
        ListNode* cur = head;
        while (cur) {
            len++;
            cur = cur->next;
        }
        
        // 2. 初始化虚拟头节点(统一处理头节点变化)
        ListNode dummy(0);
        dummy.next = head;
        ListNode* prev = &dummy; // 记录已合并部分的尾节点
        ListNode* curr = dummy.next; // 待处理的链表头
        
        // 3. 自底向上迭代合并
        for (int subLen = 1; subLen < len; subLen *= 2) {
            prev = &dummy;
            curr = dummy.next;
            while (curr) {
                // 切分第一个长度为subLen的子链表
                ListNode* l1 = curr;
                ListNode* l2 = cut(l1, subLen); // 切分后返回第二个子链表头
                
                // 切分第二个长度为subLen的子链表,curr指向剩余链表头
                curr = cut(l2, subLen);
                
                // 合并两个有序子链表,拼接至已合并部分尾部
                prev->next = mergeTwoLists(l1, l2);
                
                // 移动prev到合并后链表的尾部
                while (prev->next) {
                    prev = prev->next;
                }
            }
        }
        return dummy.next;
    }

private:
    // 核心工具:切分链表,返回剩余链表头,同时截断前n个节点
    ListNode* cut(ListNode* head, int n) {
        ListNode* p = head;
        // 移动n-1步,到达第n个节点
        while (--n > 0 && p) {
            p = p->next;
        }
        if (!p) return nullptr; // 不足n个节点,返回空
        
        ListNode* nextHead = p->next;
        p->next = nullptr; // 截断前n个节点
        return nextHead;
    }

    // 合并两个有序链表(与递归解法通用)
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode dummy(0);
        ListNode* cur = &dummy;
        while (l1 && l2) {
            if (l1->val <= l2->val) {
                cur->next = l1;
                l1 = l1->next;
            } else {
                cur->next = l2;
                l2 = l2->next;
            }
            cur = cur->next;
        }
        cur->next = l1 ? l1 : l2;
        return dummy.next;
    }
};

3. 复杂度分析

  • 时间复杂度:O (n log n)。外层循环控制子链表长度翻倍(O (log n) 次),内层循环每次遍历整个链表(O (n)),总复杂度 O (n log n)。
  • 空间复杂度:O (1)。仅使用常数个指针变量(dummy/prev/curr等),无递归栈开销,完美满足进阶要求,是面试最优解。

五、解题避坑与关键细节

  1. 截断链表必须置空 :分割子链表时,务必将截断位置的next置为nullptr,否则会形成循环引用,导致递归 / 迭代死循环。
  2. 快慢指针初始位置 :找中点时,快指针初始指向head->next而非head,否则偶数长度链表会分割不均(如4→2→1→3会分割为4→21→3,若快指针初始为head则分割为42→1→3)。
  3. 虚拟头节点的必要性:合并链表时使用虚拟头节点,可避免单独处理空链表、头节点值较小等边界情况,统一拼接逻辑。
  4. 迭代法的 subLen 翻倍:子链表长度必须按 2 的幂次递增(1→2→4→...),否则无法保证所有子链表有序,最终合并结果错误。

六、总结与拓展

LeetCode 148「排序链表」的核心是归并排序在链表上的适配,两种解法各有侧重:

  • 递归归并:代码逻辑清晰,契合分治思想,适合理解算法本质,面试中可快速口述思路;
  • 迭代归并:空间复杂度最优,是进阶要求的标准解法,适合追求极致性能的实战场景。
相关推荐
IT猿手2 小时前
基于动态三维环境下的Q-Learning算法无人机自主避障路径规划研究,MATLAB代码
算法·matlab·无人机·动态路径规划·多无人机动态避障路径规划
逸Y 仙X2 小时前
文章十:ElasticSearch索引字段高级属性
java·大数据·elasticsearch·搜索引擎·全文检索
美式请加冰2 小时前
栈的介绍和使用(算法)
数据结构·算法·leetcode
不染尘.2 小时前
排序算法详解2
数据结构·c++·算法·排序算法
cm6543202 小时前
C++代码切片分析
开发语言·c++·算法
冯RI375II694872 小时前
食品FDA认证:确保食品周边产品安全的标准
大数据
重生之我是Java开发战士2 小时前
【递归、搜索与回溯】FloodFill算法:图像渲染,岛屿数量,岛屿的最大面积,被围绕的区域,太平洋大西洋水流问题,扫雷游戏,衣橱整理
算法·leetcode·深度优先
YUANQIANG20242 小时前
PPO算法典型思路
算法·机器学习
twc8292 小时前
大模型评估指标简要说明
算法·大模型·bleu