【LeetCode】148. 排序链表

148. 排序链表(中等)



方法一:归并排序(递归法)

思路

  • 题目要求时间空间复杂度分别为 O(nlogn) 和 O(1) ,根据时间复杂度我们自然想到二分法 ,从而联想到归并排序

  • 对数组做归并排序的空间复杂度为 O(n) ,分别由新开辟数组 O(n) 和递归函数调用 O(logn) 组成,而根据链表特性:

    • 数组额外空间:链表可以通过修改引用来更改节点顺序,无需像数组一样开辟额外空间;
    • 递归额外空间:递归调用函数将带来 O(logn) 的空间复杂度,因此若希望达到 O(1) 空间复杂度,则不能使用递归。
  • 通过递归实现链表归并排序,有以下两个环节:

    • 分割 cut 环节: 找到当前链表 中点 ,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
      • 我们使用 fast,slow 快慢双指针法,「奇数个节点找到中点,偶数个节点找到中心左边的节点」。
      • 找到中点 slow 后,执行 slow->next = nullptr; 将链表切断。
      • 递归分割时,输入当前链表左端点 head 和中心节点 slow 的下一个节点 tmp(因为链表是从 slow 切断的)。
      • cut 递归终止条件 : 当 head->next == nullptr 时,说明只有一个节点了,直接返回此节点。
    • 合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。
      • 双指针法 合并,建立辅助 ListNode* h 作为头部。
      • 设置两指针 left, right 分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
        返回辅助ListNode* h 作为头部的下个节点 h->next
      • 时间复杂度 O(l + r),l, r 分别代表两个链表长度。
    • 当题目输入的 head == nullptr 时,直接返回 None。

代码

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 == nullptr || head->next == nullptr) {
            return head;
        }
        // 定义快慢指针找到分割中点 slow
        ListNode* fast = head->next;
        ListNode* slow = head;
        while(fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }
        // 右半边的头节点 tmp
        ListNode* tmp = slow->next;
        // 左右分割
        slow->next = nullptr;
        // 递归分割
        ListNode* left = sortList(head);
        ListNode* right = sortList(tmp);

        // 定义辅助头节点 h
        ListNode* h = new ListNode(0);
        ListNode* res = h; // res保存h的头节点

        // 合并
        while (left && right) {
            if (left->val < right->val) {
                h->next = left;
                left = left->next;
            }
            else {
                h->next = right;
                right = right->next;
            }
            h = h->next;
        }
        // 检查是left为空还是right为空
        h->next = left != nullptr ? left : right;
        return res->next;
    }
};

方法二:归并排序(从底至顶直接合并)

思路

  • 对于非递归的归并排序,需要使用迭代的方式替换 cut 环节:

    • cut 环节本质上是通过二分法得到链表最小节点单元,再通过多轮合并得到排序结果。
    • 每一轮合并 merge 操作针对的单元都有固定长度 intv ,例如:
      • 第一轮合并时 intv = 1,即将整个链表切分为多个长度为 1 的单元,并按顺序两两排序合并,合并完成的已排序单元长度为 2。
      • 第二轮合并时 intv = 2,即将整个链表切分为多个长度为 2 的单元,并按顺序两两排序合并,合并完成已排序单元长度为 4。
      • 以此类推,直到单元长度 intv >= 链表长度,代表已经排序完成。
    • 根据以上推论,我们可以仅根据 intv 计算每个单元边界,并完成链表的每轮排序合并,例如:
      • 当 intv = 1 时,将链表第 1 和第 2 节点排序合并,第 3 和第 4 节点排序合并,......。
      • 当 intv = 2 时,将链表第 1-2 和第 3-4 节点排序合并,第 5-6 和第 7-8 节点排序合并,......。
      • 当 intv = 4 时,将链表第 1-4 和第 5-8 节点排序合并,第 9-12 和第 13-16 节点排序合并,......。
  • 此方法时间复杂度 O(nlogn) ,空间复杂度 O(1) 。

  • 模拟上述的多轮排序合并:

    • 统计链表长度 length,用于通过判断 intv < length 判定是否完成排序;
    • 额外声明一个节点 res,作为头部后面接整个链表,用于:
      • intv *= 2 即切换到下一轮合并时,可通过 res->next 找到链表头部 h;
      • 执行排序合并时,需要一个辅助节点作为头部,而 res 则作为链表头部排序合并时的辅助头部 pre;后面的合并排序可以将上次合并排序的尾部 tail 用做辅助节点。
    • 在每轮 intv 下的合并流程:
      • 根据 intv 找到合并单元 1 和单元 2 的头部 h1, h2。由于链表长度可能不是 2^n,需要考虑边界条件
        • 在找 h2 过程中,如果链表剩余元素个数少于 intv ,则无需合并环节,直接 break,执行下一轮合并;
        • 若 h2 存在,但以 h2 为头部的剩余元素个数少于 intv,也执行合并环节,h2 单元的长度为 c2 = intv - i。
      • 合并长度为 c1, c2 的 h1, h2 链表,其中:
        • 合并完后,需要修改新的合并单元的尾部 pre 指针指向下一个合并单元头部 h 。(在寻找 h1, h2 环节中,h指针已经被移动到下一个单元头部
        • 合并单元尾部同时也作为下次合并的辅助头部 pre。
      • 当 h == None,代表此轮 intv 合并完成,跳出。
    • 每轮合并完成后将单元长度 ×2,切换到下轮合并:intv *= 2。

代码

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 == nullptr || head->next == nullptr) return head;
        int intv = 1, length = 0;
        // res保存已排序的结果链表头节点
        ListNode* res = new ListNode(0);
        res->next = head;
        // pre指向已排序的结果链表的末尾
        // h指向待排序链表的头节点
        ListNode* pre;
        ListNode* h;
        // 遍历链表,得到长度
        while (head) {
            length ++;
            head = head->next;
        }
        while (intv < length) {
            pre = res;
            h = res->next;
            // 当待排序链表头节点不为空,说明还需要归并
            while(h) {
                // tmp1保存合并单元1的头节点
                ListNode* tmp1 = h;
                int len1 = intv;
                while (len1 > 0 && h) {
                    len1 --;
                    h = h->next;
                } 
                if(len1 > 0) {
                    break;
                }
                // tmp2保存合并单元2的头节点
                ListNode* tmp2 = h;
                int len2 = intv; 
                while (len2 > 0 && h) {
                    len2 --;
                    h = h->next;
                }
                // c1 c2分别是合并单元剩余的节点(即合并单元各自的节点数)
                int c1 = intv, c2 = intv - len2;
                while (c1 > 0 && c2 > 0) {
                    if (tmp1->val > tmp2->val) {
                        pre->next = tmp2;
                        tmp2 = tmp2->next;
                        c2 --; // 减少一个节点
                    }
                    else {
                        pre->next = tmp1;
                        tmp1 = tmp1->next;
                        c1--;
                    }
                    pre = pre->next;
                }
                // 将c1剩余节点连接到已排序节点的末尾
                if(c1 > 0) {
                    pre->next = tmp1;
                }
                else {
                    pre->next = tmp2;
                }
                // 更新pre 使其指向已排序链表的末尾
                while (c1-- > 0 || c2-- > 0) {
                    pre = pre->next;
                }
                // 将已排序链表和待排序链表连接
                pre->next = h;
            }
            intv *= 2;
        }
        return res->next;
    }
};

参考资料

  1. Sort List (归并排序链表)
相关推荐
yuanbenshidiaos41 分钟前
C++----------函数的调用机制
java·c++·算法
唐叔在学习1 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA1 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo1 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc1 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
姚先生971 小时前
LeetCode 54. 螺旋矩阵 (C++实现)
c++·leetcode·矩阵
游是水里的游2 小时前
【算法day20】回溯:子集与全排列问题
算法
yoyobravery2 小时前
c语言大一期末复习
c语言·开发语言·算法
Jiude2 小时前
算法题题解记录——双变量问题的 “枚举右,维护左”
python·算法·面试
被AI抢饭碗的人3 小时前
算法题(13):异或变换
算法