148. 排序链表

题目描述:

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

示例 1:


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


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

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

提示:

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

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sort-list
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:链表排序不难,难的是 O(n log n) 时间复杂度和常数级空间复杂度,这两个限制条件一加,基本就不怎么会写了。

        数组的排序, O(n log n) 时间复杂度我能想到快排,但是快排要随时访问某个坐标啊,这是链表,显然不可能的。思来想去,还是不会,看了题解。。。

        题解是归并排序,好吧,我几乎没怎么写过归并排序,也忘了它的时间复杂度。这道题算是帮我温习了一下。归并排序,时间复杂度为O(n log n)。

        而且归并排序分为两种,从上往下的归并排序和从下往上的归并排序。从上往下显然是用递归了,从下往上就是普通的迭代。题解中说明了这道题从上往下的话,空间复杂度是不满足的,而且一般情况下,递归是不怎么受欢迎的(面试中)。因此,就选用了从下往上。

        从下往上归并排序,就要定一个步长step,从1开始,每一轮归并排序增大2倍,直到整个链表完成归并排序。每一轮归并排序中,都要将相邻两个链表进行排序,并且不使用额外空间。

        题解的做法让我受益匪浅:建立一个虚拟头节点virtualhead,它的下一个节点virtualhead->next指向头节点head。每一轮归并排序,设定两个指针,pre和cur。pre位于要合并的两个链表的前面,当两个链表合并后,挂在pre后面即可。cur则是遍历整个链表的指针。cur初始情况是链表的头节点,也是这一轮归并排序中,第一组要合并的两个链表中的左链表的起始点。然后cur后移step个节点,到达第一组要合并的两个链表中的左链表的终止点。然后记录下下一个节点,也就是要合并的两个链表中的右链表的起始点。最关键的是,在左链表的终止点处,断开左链表与原链表的链接,设置cur->next=NULL;然后cur指向右链表起始点,接着后移step个节点,到达第一组要合并的两个链表中的右链表的终止点。然后记录下下一个节点,也就是下一组要合并的两个链表中的左链表的起始点。然后在右链表的终止点处,断开右链表与原链表的链接,设置cur->next=NULL;然后合并左链表和右链表,并把合并后的结果挂到pre后面,然后pre沿着链表移动,一直移动到链表尾节点,等待链接后续合并的链表(因为刚才在右边表终止点处,断开了与原链表的连接)。然后开始下一组相邻链表的合并,直到本轮归并排序结束。

        还有一个很重要的点,这道题让我学会了如何在不使用额外空间的情况下,合并两个有序链表。之前的做法是,新建一个链表,比较两个链表当前节点的大小,以小的链表为val,建立新节点,挂到新链表上去。这样可以完成链表的排序,但是会申请O(n)的空间。不申请额外空间的做法是,比较左链表和右链表的头部,如果左链表头部小,则以左链表头部为头节点,递归调用该函数,将左链表的剩余节点和右链表合并,将合并后的链表挂到左链表头部。如果右链表头部小,则是一样的处理方法。

如果觉得不怎么清晰,还是请看题解吧:

https://leetcode-cn.com/problems/sort-list/solution/pai-xu-lian-biao-by-leetcode-solution/力扣

 

代码如下:

/**
 * 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==NULL) return NULL;
        int length=0;
        // temphead存储头节点,用来计算链表长度
        ListNode *temphead=head;
        while(temphead!=NULL)
        {
            length++;
            temphead=temphead->next;
        }
        // 归并排序的步数,从1开始,每次增大2倍
        int step=1;
        // 虚拟头节点,指向排序后的链表
        ListNode *virtualhead=new ListNode(0);
        // pre指针指向两个相邻链表排序后的结果
        ListNode *pre=virtualhead;
        // 初始情况下,虚拟头节点的下个节点为头节点
        virtualhead->next=head;
        // cur指针用来遍历本次需要归并排序的两个相邻链表
        ListNode *cur=head;
        // 步长小于长度时,循环
        for(;step<length;step=step*2)
        {
            // cur指针不为空,说明本次归并排序尚未结束
            while(cur!=NULL)
            {
                // left为归并排序两个相邻链表中的左边那个头节点
                ListNode *left=cur;
                // 移动step步长,为了寻找到右链表的起始点,如果cur为空则提前终止
                for(int i=1;i<step&&cur!=NULL;i++)
                    cur=cur->next;
                // right为归并排序两个相邻链表中的右边那个头节点,初始为空
                ListNode *right=NULL;
                // cur不为空时且cur不是最后一个节点
                if(cur!=NULL&&cur->next!=NULL)
                {
                    // right等于cur的下个节点
                    right=cur->next;
                    // 要归并排序的左链表,从原链表中断开
                    cur->next=NULL;
                }
                // cur指向right
                cur=right;
                // 移动step步长,为了寻找到右链表的结束点,如果cur为空则提前终止
                for(int i=1;i<step&&cur!=NULL;i++)
                    cur=cur->next;
                // temp代表右链表结束点后的下一个节点,即下一轮归并排序的左链表起始点
                ListNode *temp=NULL;
                // 如果cur不为空,且cur不是最后一个节点
                if(cur!=NULL&&cur->next!=NULL)
                {
                    // temp指向cur下一个节点
                    temp=cur->next;
                    // 要归并排序的右链表,从原链表中断开
                    cur->next=NULL;
                }
                // cur指向下一轮归并排序的左链表起始点
                cur=temp;
                // 合并本轮归并排序的左链表和右链表,并把头节点指针连接到pre的后面
                pre->next=merge(left,right);
                // pre移动到归并排序后的链表的最后一个节点,等待链接下一轮归并排序后的结果
                while(pre->next!=NULL)
                    pre=pre->next;
            }
            // 本轮归并排序结束后,把pre重新指向虚拟头节点,等待下一轮归并排序
            pre=virtualhead;
            // car指向虚拟头节点的下一位,即下一轮归并排序的起始节点,等待下一轮归并排序
            cur=virtualhead->next;
        }
        // 返回虚拟头节点的下一个节点,即为归并排序后链表真正的头节点
        return virtualhead->next;
    }
    ListNode *merge(ListNode* left,ListNode *right)
    {
        // 左链表为空,直接返回右链表
        if(left==NULL) return right;
        // 右链表为空,直接返回左链表
        if(right==NULL) return left;
        // 左链表头节点比右链表头节点小
        if(left->val<right->val)
        {
            // 左链表头节点为第一个节点,左链表剩余节点和右链表都要链接在该节点后面
            left->next=merge(left->next,right);
            // 返回合并后的头节点
            return left;
        }
        // 左链表头节点比右链表头节点大
        else
        {
            // 右链表头节点为第一个节点,右链表剩余节点和左链表都要链接在该节点后面
            right->next=merge(left,right->next);
            // 返回合并后的头节点
            return right; 
        }
        // 默认返回NULL
        return NULL;
    }
};

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值