LeetCode148——Sort List——merge sort

这道题的题面非常简单,就是简单的对一个乱序的链表进行排序。
在这里插入图片描述
以往做的排序问题很多,但是大多数都是在数组中进行排序的。关于链表的排序做的比较少,这里专门开一贴来记录链表中的排序算法。注意这里所说的面向链表的排序算法特指game of pointers,也就是说这里不考虑有些人把数字取到vector里再sort一下放回链表的讨巧做法。

一、插入排序
这是我首先想到的算法,而且它确实非常适合链表这种数据结构。
插入排序需要保留一个有序区,所以这里我用了[Head, Tail]这个区间来指出有序区域的范围,那么我们要插入的下一个元素,理所当然的就是Tail+1这个位置的元素了,这里我记为Present,整个插入排序的代码如下:

ListNode* sortList(ListNode* head) {
        if(head == nullptr)
            return head;
        
        /*apply for a dummy node*/
        ListNode* Dummy = new ListNode(0, head);

        /*2 pointers*/
        ListNode* Tail = Dummy->next;           /*tail of sorted list*/
        ListNode* Present = Tail->next;			/*the next node to be inserted*/

        
        while(Present)
        {
            if(Tail->val <= Present->val)
                Tail = Tail->next;
            else
            {
                ListNode* Search = Dummy;
                while(Search->next->val <= Present->val)
                    Search = Search->next;
                    
                /*a set of pointer modification*/
                Tail->next = Present->next;
                Present->next = Search->next;
                Search->next = Present;
            }
            Present = Tail->next;
        }
        return Dummy->next;
    }

这段代码注意几个点:

1.遇到链表问题首先申请个Dummy结点总是没错的,即便它有些情况下没什么用,但是也不会带来额外的坏处:)
2. 循环结束的标志是要插入的结点为空结点
3. 首先考虑特殊情况,这样可以大大简化代码逻辑,比如上述代码中首先考虑如果要插入的节点的值大于尾结点的值,那这种情况下只需要将有序区向后移就可以了,并不需要额外的动作:

if(Tail->val <= Present->val)
	Tail = Tail->next;

4.否则,如果不是特殊情况,那么就的确需要插入操作了。这时候就得找到合适的插入位置,注意这里Search指针每一次都用它的下一个结点去和要插入的结点Present比较大小,这是因为我们修改链表时需要记住待插入位置的前一个结点

while(Search->next->val <= Present->val)
	Search = Search->next;

还有这里我们用了while循环,这是因为我们确定在前面的有序区一定可以找到要插入的位置
5.完成一次新结点的插入操作之后,无论如何,总要更新下一个要插入结点的指针,这个有时候容易忘。

Present = Tail->next;

以上就是面向链表的简单插入排序的代码思路,在此做个记录。
事实上,这个代码效率不高,复杂度是 O ( n 2 ) O(n^2) O(n2)这个量级的,结果就是:
在这里插入图片描述

二、归并排序
归并排序算法的时间复杂度是 O ( n log ⁡ n ) O(n\log{n}) O(nlogn),这个算法显然高效多了,但是一旦出现 l o g n logn logn,一定也会想到这会涉及到链表的划分。这里面根据划分的次序就可以分为自顶向下和自底向上

首先来看看自顶向下的方法,这种方法是递归的,每次将链表从其中点分开成为等长的两个部分,这样一层层切下去直到子链表的长度为1,在回归的时候再将它们merge(归并)起来。首先给出完整代码:

class Solution {
    /*the list is in the range of [Head, Tail)*/
    ListNode* mergeList(ListNode* L1, ListNode* L2)
    {
        ListNode* Dummy = new ListNode(0, nullptr);
        ListNode* Present = Dummy;

        while(L1 != nullptr && L2 != nullptr)
        {
            if(L1->val <= L2->val)
            {
                Present->next = L1;
                L1 = L1->next;
            }
            else
            {
                Present->next = L2;
                L2 = L2->next;
            }
            Present = Present->next;
        }

        if(L1 != nullptr)
            Present->next = L1;
        
        if(L2 != nullptr)
            Present->next = L2;

        return Dummy->next;
    }
    
    ListNode* splitList(ListNode* Head, ListNode* Tail)
    {   
        /*0 node*/
        if(Head == nullptr)
            return Head;

        /*1 node*/
        if(Head->next == Tail)
        {
            Head->next = nullptr;	/*extract the node from list*/
            return Head;
        }
        
		/*using fast-slow pointer to search the middle point*/
        ListNode* Slow = Head;
        ListNode* Fast = Head;
       while (Fast != Tail) {
            Slow = Slow->next;
            Fast = Fast->next;
            if (Fast != Tail) {
                Fast = Fast->next;
            }
        }
        
        return mergeList(splitList(Head, Slow), splitList(Slow, Tail));
    }

public:
    ListNode* sortList(ListNode* head)
    {
        return splitList(head, nullptr);
    }
};

首先需要指出的是,这里使用的是左闭右开的区间,所以链表的范围是[Head, Tail),然后我们使用快慢指针的算法去找链表的中点,这些都是常规方法。我当时在代码实现时,其实是对快慢指针这里有疑问的:

/*using fast-slow pointer to search the middle point*/
        ListNode* Slow = Head;
        ListNode* Fast = Head;
       while (Fast != Tail) {
            Slow = Slow->next;
            Fast = Fast->next;
            if (Fast != Tail) {
                Fast = Fast->next;
            }
        }

之前我写快慢指针时,代码是这样的,我一开始也是这样写的:

		ListNode* slow = head;
        ListNode* fast = head;
        while (fast->next != nullptr && fast->next->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;

但事实上这样子会死循环,原因得从这两种快慢指针算法的区别上来找。
事实上,本题中的快慢指针写法当遇到链表长度为偶数时,会找到中间靠后的中心元素,而之前的代码找到的是中间靠前的那个元素

4 -> [2] -> 1 -> 3	//原始写法
4 -> 2 -> [1] -> 3	//本题写法

原始写法下,再结合左闭右开区间的约定,链表自顶向下拆分的过程会在最后两个结点处陷入死循环:

1 -> 3				// 原始写法下切分到长度为2的小链表时,算法将陷入死循环

了解了这一点,其他的思路其实就是比较简单的,先拆分(N->1),再合并(1->N),用递归可以优雅地实现这种思路,时间复杂度是 O ( n log ⁡ n ) O(n\log{n}) O(nlogn)

再看自底向上的方式,这种方式就是迭代法了,我个人觉得递归这种写法并不让我特别喜欢,你可以用它来炫技,展现你高超的算法设计思维(但也就仅限于此)。否则,我还是更倾向于迭代,它的设计难度相对更低,也更适用于真正的工程场景,代码可维护性高,首先给出完整代码:

class Solution {

    /*merge 2 sorted list*/
    ListNode* mergeList(ListNode* ListA, ListNode* ListB)
    {
        /*apply for a dummy node*/
        ListNode* Dummy = new ListNode(0, nullptr);
        ListNode* Present = Dummy;

        while(ListA && ListB)
        {
            if(ListA->val <= ListB->val)
            {
                Present->next = ListA;
                ListA = ListA->next;
            }
            else
            {
                Present->next = ListB;
                ListB = ListB->next;
            }
            Present = Present->next;
        }

        /*deal with the remaining nodes*/
        if(ListA)
            Present->next = ListA;
        if(ListB)
            Present->next = ListB;
        return Dummy->next;
    }

    int getLength(ListNode* Head)
    {
        int Counter = 0;
        while(Head)
        {
            Counter += 1;
            Head = Head->next;
        }
        return Counter;
    }

public:
    ListNode* sortList(ListNode* head) 
    {
        /*if the list is null*/
        if(head == nullptr)
            return head;

        /*get the length of list*/
        int Length = getLength(head);

        /*apply for a dummy node*/
        ListNode* Dummy = new ListNode(0, head);

        /*sort the list in bottom-up method*/
        int SubLength;
        for(SubLength = 1 ; SubLength < Length ; SubLength <<= 1)
        {
            /*a set of pointers*/
            ListNode* Previous = Dummy;
            ListNode* SubHead = Dummy->next;
            ListNode* SubTail = SubHead;
            ListNode* SubList1 = nullptr;
            ListNode* SubList2 = nullptr;
            
            while(SubHead)
            {
                int Counter = 1;
                /*get the first sublist*/
                while(Counter < SubLength && SubTail && SubTail->next)     // the sublist's length can be shorter than subLength
                {
                    SubTail = SubTail->next;
                    Counter += 1;
                }

                /*record the next sublist's head*/
                SubList1 = SubHead;
                SubHead = SubTail->next;

                /*set the sublist's next to nullptr for list merge*/
                SubTail->next = nullptr;

                /*update the Tail pointer*/
                SubTail = SubHead;

                /*get the second sublist*/
                Counter = 1;
                while(Counter < SubLength && SubTail && SubTail->next)
                {
                    SubTail = SubTail->next;
                    Counter += 1;
                }
                SubList2 = SubHead;
                if(SubTail)
                {
                    SubHead = SubTail->next;
                    SubTail->next = nullptr;
                    SubTail = SubHead;                  /*ready for next round*/
                }
                    
                /*merge the 2 sublists*/
                ListNode* SortedSubList = mergeList(SubList1, SubList2);

                /*connect the sublist back to the original list*/
                Previous->next = SortedSubList;
                while(Previous->next)
                    Previous = Previous->next;      /*adjust previous pointer to the end of sorted list*/    
            }
        }
        
        return Dummy->next;
    }
};

基本思路就是按照子链表长度(SubLength)进行迭代,每一个长度下就数出来对应长度的子链表,一直数到链表结束,每相邻两个子链表归并一下,再插回到原先的链表中(Previous指针就是干这事儿的)。持续此过程,直到子链表长度为Length/2,这次归并完整个链表将完全有序,算法时间复杂度同样为 O ( n log ⁡ n ) O(n\log{n}) O(nlogn)

这篇文章梳理了关于链表排序的一些解法,其中使用归并法完成排序的两种方法是要掌握的重点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据,题目要求在O(n log n)的时间复杂度和常数级空间复杂度下对链表进行排序。根据提供的代码,可以使用归并排序来解决这个问题。代码中的sortList函数是递归函数,首先判断链表是否为空或者只有一个节点,如果是的话直接返回该链表。然后通过快慢指针找到链表的中点,将链表分成两半,再递归地对两个子链表进行排序。最后,使用merge函数将两个有序的子链表合并成一个有序的链表merge函数通过比较两个链表节点的值来确定节点的顺序,然后将节点逐个连接起来。整个过程可以保证链表最终被排序。因此,可以使用该方法解决leetcode 148的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [leetcode 148 排序链表 C语言](https://blog.csdn.net/qq_42007287/article/details/104730970)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [LeetCode148. Sort List 排序链表(C语言)](https://blog.csdn.net/wangqingchuan92/article/details/104037031)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [LeetCode 148. Sort List](https://blog.csdn.net/smmyy022/article/details/82937283)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值