#
CS杂物 2026-03-26

归并排序 —— 从 LeetCode 148. 排序链表

By lbyxiaolizi 20 Views 21 MIN READ 0 Comments

在诸多排序算法中,归并排序(merge sort)无疑是我印象最为深刻的一种,这要源于在某位学长讲解算法让我尝试实现归并排序时,我卡在寻找中点的那一步毫无头绪时,得知还有快慢指针这种神器的震撼(谁能想到我是从归并排序接触到双指针的,天

归并排序是使用分治的思想,把待排序的集合分段直至不可再分,当集合中只有一个元素的时候,这个集合就自然成为了一个已排序好的集合,此时,我们在将其合并即可。

归并排序的时间复杂度是$O(n\log{n})$,空间复杂度是$O(n)$。

算法实现

数组的实现

在数组中分治是比较简单的,困难的一点主要在合并上。因为数组的内存是连续的,我们没法从中间开辟出一块内存,必须找一块新的内存存放。

//分治
void mergeSort(std::vector<int>& nums,int left,int right,std::vector<int>& temp){
    if(left>=right){
        return ;
    }
    int mid=left+(right-left)/2;
    
    mergeSort(nums,left,mid,temp);
    mergeSort(nums,mid+1,right,temp);
    
    merge(nums,left,mid,right,temp);
}

//合并
void merge(std::vector<int>& nums,int left,int mid,int right,std::vectoe<int>& temp){
    int i=left;
    int j=mid+1;
    int k=left;
    
    while(i<=mid && j<=right){
        if(nums[i]<=nums[j]){
            temp{k++}=nums[i++];
        }else{
            temo[k++]=nums[j++];
        }
        
    }
    while (i <= mid) {
            temp[k++] = nums[i++];
        }

        // 如果右半边还有剩下的,全部打包带走
        while (j <= right) {
            temp[k++] = nums[j++];
        }

        // 恢复原数组内容
        for (int p = left; p <= right; p++) {
            nums[p] = temp[p];
        }
}

链表的实现

链表在实现归并排序的时候最困难的一步是分治中的如何取到中点,所以我们在这里考虑使用快慢指针来解决这个问题。

什么是快慢指针?假设这里有一个链表1->2->3->4,我们现在给出两个指针,让慢指针slow指向1,快指针fast指向2,接着进行迭代,让快慢指针同时移动,快指针每次比慢指针多移动一步——当快指针移动到最后一个节点的时候,慢指针刚好移动到了中点!

好了,我们已经实现了找到中点,然后分成两端,在把他们分别丢给递归小盒,Done!

最后合并他们就完成了全部的归并排序了。

拿 LeetCode 的 148.排序链表 来举个例子

148. 排序链表 中等

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

示例 1:

img

输入:head = [4,2,1,3]
输出:[1,2,3,4]

示例 2:

img

输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目在范围 [0, 5 * 104]
  • -105 <= Node.val <= 105

进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

/**
 * 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;
        }

        ListNode* fast=head->next; //快指针指向第二个节点
        ListNode* slow=head; //慢指针指向第一个节点

        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* list1,ListNode* list2){
        ListNode dummy(-1); //创建一个虚拟头节点
        ListNode* curr=&dummy; //将dummy的引用赋值给curr

        while(list1!=nullptr && list2!=nullptr){
            if(list1->val < list2->val){
                // list1小的情况
                curr->next=list1; //给list1的节点挂到curr后面
                list1=list1->next; //步进
            }else{
                //list2小的情况
                curr->next=list2; //给list2的节点挂到curr后面
                list2=list2->next; //步进
            }
            curr=curr->next; //步进
        }
        curr->next=(list1!=nullptr)?list1:list2; //list1 里面还有剩下的节点吗?如果有,挂list1,否则挂list2
        return dummy.next;
    }
};

这份代码用自顶向下的递归归并排序实现了$O(n\log{n})$的时间复杂度和$O(\log{N})$的空间复杂度。

但是题目对我们提出了更高的要求,做到$O(1)$的空间复杂度。

这时候我们就要使用自底向上的迭代归并排序。

先遍历得到链表的长度,然后将链表拆成长度为1的子链表,他们本身已经排序好,两两合并;现在每个有序子链表的长度变成了2,再进行两两合并,每个有序子链表的长度就变成了4,……,以此类推,最后就可以得到排序好的链表。

但是在实现时需要多写一个split函数,较为复杂,下面是CPP的实现代码

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (head == nullptr) return nullptr;

        // 遍历得到链表长度
        int length = 0;
        ListNode* node = head;
        while (node != nullptr) {
            length++;
            node = node->next;
        }

        // 哨兵节点
        ListNode dummy(-1, head);

        // 外层循环:控制步长 step,每次翻倍 (1, 2, 4, 8...)
        for (int step = 1; step < length; step *= 2) {
            // prev 永远站在“已经合并好的部分”的尾巴上,负责把新合并的片段缝上去
            ListNode* prev = &dummy;
            // curr 是我们的探路兵,负责在前面切香肠
            ListNode* curr = dummy.next;

            // 内层循环:按当前 step 大小,一路往下切分并合并
            while (curr != nullptr) {
                // 第一刀:切出长度为 step 的 list1
                ListNode* head1 = curr;
                ListNode* head2 = split(head1, step);

                // 第二刀:切出长度为 step 的 list2
                // split 会把切完 list2 后剩下的链表头部返回给 curr,准备下一对的循环
                curr = split(head2, step); 

                // 把切下来的 list1 和 list2 合并
                ListNode* mergedHead = merge(head1, head2);

                // 缝合:把 prev 的尾巴连上刚刚合并好的新片段
                prev->next = mergedHead;

                // 把 prev 往前挪,挪到刚刚缝上去的这个新片段的最后面,准备接下一段
                while (prev->next != nullptr) {
                    prev = prev->next;
                }
            }
        }
        return dummy.next;
    }

private:
    // 切割链表
    // 从 head 开始切掉 n 个节点,切断联系,并返回剩下的链表头部
    ListNode* split(ListNode* head, int n) {
        if (head == nullptr) return nullptr;
        
        // 往前走 n-1 步,停在要切断的那个节点上
        // 注意:如果链表长度不够 n 了,走到尽头也要停下
        for (int i = 1; i < n && head->next != nullptr; i++) {
            head = head->next;
        }
        
        ListNode* nextChunk = head->next; // 存储剩余节点
        head->next = nullptr;             // 剪断
        return nextChunk;                 // 返回剩余节点
    }

    // 合并链表
    ListNode* merge(ListNode* list1, ListNode* list2) {
        ListNode dummy(-1);
        ListNode* curr = &dummy;
        while (list1 != nullptr && list2 != nullptr) {
            if (list1->val < list2->val) {
                curr->next = list1;
                list1 = list1->next;
            } else {
                curr->next = list2;
                list2 = list2->next;
            }
            curr = curr->next;
        }
        curr->next = (list1 != nullptr) ? list1 : list2;
        return dummy.next;
    }
};

本文由 lbyxiaolizi 原创

采用 CC BY-NC-SA 4.0 协议进行许可

转载请注明出处:https://blog.vh.gs/cs/merge-sort.html

TAGS: 算法 排序

相关推荐

  • 暂无相关推荐,看看别的吧。

0 评论

发表评论