算法题学习---单链表的排序

113 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

题目

描述

给定一个节点数为n的无序单链表,对其按升序排序。

数据范围:0<n≤100000

要求:空间复杂度 O(n),时间复杂度 O(nlogn)

示例1

输入:
[1,3,2,4,5]
返回值:
{1,2,3,4,5}

示例2

输入:
[-1,0,-2]
返回值:
{-2,-1,0}

分析

思路一

实际上刨除链表的形式,我们知道,这就是一个排序问题,首先我们能想到的比较简单的就是插入排序方式。

  • 将链表分为已排序的部分(开始时只有一个元素)和后面未排序的部分,遍历未排序的部分元素;
  • 在已排序的部分中从前往后遍历查找合适的位置(元素大小大于当前待排序的元素,或走到了已排序部分的末尾);
  • 将当前待排序元素插入到找出的合适位置,遍历直到末尾,整个链表已排序完毕。

上述方案中需要增加一个表头元素,空间复杂度为O(1),最差情况下时间复杂度为O(n^2)。

思路二

对于排序问题,归并排序的时间复杂度可以达到O(nlogn),我们就来看看如何将链表排序的问题分解成子问题。

首先我们要将链表分为前后两个部分,这里我们可以用到快慢指针,快指针每次走两步,慢指针走一步,这样可以找到链表的中点;然后对这两个部分分别做排序处理,最后将得到的结果使用两个有序链表排序的算法连接成一条链表。

特别地,子问题中,如果头指针为空或头指针的next为空,视为已排序,立即返回。

步骤如下:

  • step 1:首先判断链表为空或者只有一个元素,直接就是有序的。
  • step 2:准备三个指针,快指针right每次走两步,慢指针mid每次走一步,前序指针left每次跟在mid前一个位置。三个指针遍历链表,当快指针到达链表尾部的时候,慢指针mid刚好走了链表的一半,正好是中间位置。
  • step 3:从left位置将链表断开,刚好分成两个子问题开始递归。
  • step 4:将子问题得到的链表合并。

代码示例

版本一

使用插入排序,使用一个额外的空间O(1),时间复杂度O(n^2)

/**
 * struct ListNode {
 *  int val;
 *  struct ListNode *next;
 * };
 */

class Solution {
  public:
    /**
     *
     * @param head ListNode类 the head node
     * @return ListNode类
     */
    ListNode* sortInList(ListNode* head) {
        // write code here
        if (nullptr == head || nullptr == head->next) {
            return head;
        }

        ListNode* pReturn = new ListNode(-1);
        pReturn->next = head;

        // split the link list
        ListNode* pCur = head->next;
        head->next = nullptr;
        ListNode* pNext = nullptr;

        while (nullptr != pCur) {
            // find the location
            ListNode* pPreSearch = pReturn;
            ListNode* pSearch = pReturn->next;
            while (nullptr != pSearch) {
                if (pSearch->val > pCur->val) {
                    break;
                }
                pPreSearch = pSearch;
                pSearch = pSearch->next;
            }

            // link the node
            pNext = pCur->next;
            pCur->next = pPreSearch->next;
            pPreSearch->next = pCur;
            pCur = pNext;
        }
        
        return pReturn->next;
    }
};

版本二

使用归并排序的方式,时间复杂度O(nlogn),空间复杂度O(n)

  • 终止条件: 当子链表划分到为空或者只剩一个节点时,不再继续划分,往上合并。
  • 返回值: 每次返回两个排好序且合并好的子链表。
  • 本级任务: 找到这个链表的中间节点,从前面断开,分为左右两个子链表,进入子问题排序。
/**
 * struct ListNode {
 *  int val;
 *  struct ListNode *next;
 * };
 */

class Solution {
  public:
    /**
     *
     * @param head ListNode类 the head node
     * @return ListNode类
     */
    ListNode* sortInList(ListNode* head) {
        // write code here
        return MergeSort(head);
    }
    ListNode* MergeSort(ListNode* head){
        if (nullptr == head || nullptr == head->next) {
            return head;
        }

        ListNode *pQuick = head;
        ListNode *pSlow = head;
        ListNode *pPre = nullptr;

        while(pQuick && pQuick->next){
            pQuick = pQuick->next->next;
            pPre = pSlow;
            pSlow = pSlow->next;
        }
        pPre->next = nullptr;
        ListNode* pLeft = MergeSort(head);
        ListNode* pRight = MergeSort(pSlow);
        
        return Merge2List(pLeft, pRight);
    }

    ListNode* Merge2List(ListNode *head1,ListNode* head2){
        if(nullptr == head1){
            return head2;
        }
        if(nullptr == head2){
            return head1;
        }

        ListNode *pReturn = new ListNode(-1);
        ListNode *pCur = pReturn;

        while(nullptr != head1 && nullptr != head2){
            if(head1->val < head2->val){
                pCur->next = head1;
                head1 = head1->next;
            }else{
                pCur->next = head2;
                head2 = head2->next;
            }
            pCur = pCur->next;
        }

        pCur->next = nullptr == head1 ? head2 : head1;
        return pReturn->next;
    }
};