IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> LeetCode148——Sort List——merge sort -> 正文阅读

[数据结构与算法]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)

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

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-05-21 19:13:41  更:2022-05-21 19:13:49 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 1:47:06-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码