链表排序居然比数组排序还简单?——leetcode 148 (1)

链表排序居然比数组排序还简单?——leetcode 148 (1)

O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。

示例 1:

输入: 4->2->1->3
输出: 1->2->3->4
示例 2:

输入: -1->5->3->4->0
输出: -1->0->3->4->5

我们之前学过了不少排序算法,其中比较常用的,而且时间复杂度是O(nlgn)的有快速排序和归并排序。但是这个题目还有一个要求,那就是只能使用常数级别的额外空间。那么这两种解法,哪种可以满足这个条件呢?我们先看归并排序。

写过归并排序的人都知道,我们必须使用额外的O(n)的空间,因为对于两个排好序的数组,想要把它们归并到一个数组里并且保持升序,需要和原来数组同样大小的空间。基于这一点,我一开始也觉得归并排序没有办法解决这次的问题。但是后来看了别人的解法,还没仔细看具体怎么实现这个算法,就看到了一句话:链表是不用额外空间的。我想了一下这句话的含义,就明白了这句话是什么意思。因为对于一个链表,我们只需要改变结点的后驱结点,也就是改变next指向的结点,就可以实现排序,而这种操作是不用额外空间的。

明白了这一点,我们就可以用归并排序解决这个问题。值得注意的是,对数组使用归并排序,是要获得这个数组的起始下标和末尾下标(或者数组长度)的。也就是说,归并排序的函数mergeSort有两个参数,而且如果想要得到排好序的数组,也是需要两个变量来表示的。

因为这一点,我一开始写链表的归并排序时,也是这么写的,mergeSort有两个参数,merge有三个参数(事实上应该是四个,但需要合并的两个排好序的数组都是相邻的,也就是说第一个数组的末尾就是第二个数组的起始,因此只需要三个参数就可以表示这两个数组),而且mergeSort要返回排好序的数组以便merge合并,所以mergeSort又要返回两个指针。而且还要考虑一大堆边界值的情况,这样写出来的代码就会异常繁琐:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution 
{
public:
    ListNode* sortList(ListNode* head) 
    {
        if (!head) { return head; }
        ListNode *tail = head;
        while (tail) { tail = tail->next; }
        pair<ListNode*, ListNode*> p = mergeSort(head, tail);
        return p.first;
    }

    pair<ListNode*, ListNode*> mergeSort(ListNode* head, ListNode* tail)
    {
        if (head->next == tail) { return make_pair(head, head); }
        if (head->next->next == tail) 
        {
            if (head->val > head->next->val)
            {
                swap(head->val, head->next->val);
            }
            return make_pair(head, head->next);
        }
        ListNode* mid = findMid(head, tail);
        pair<ListNode*, ListNode*> p1 = mergeSort(head, mid);
        pair<ListNode*, ListNode*> p2 = mergeSort(mid, tail);
        p1.second->next = p2.first;
        pair<ListNode*, ListNode*> p = 
        	merge(p1.first, p2.first, p2.second->next);
        return p;
    }

    ListNode* findMid(ListNode* head, ListNode* tail)
    {
        ListNode* l1 = head, *l2 = head;
        while (l1 != tail && l1->next != tail)
        {
            l1 = l1->next->next;
            l2 = l2->next;
        }
        return l2;
    }
    
    pair<ListNode*, ListNode*> merge(ListNode* head, ListNode* mid, 
    	ListNode* tail)
    {
        ListNode* p = head;
        ListNode* cur = NULL, *l1 = NULL, *l2 = NULL;
        if (head->val < mid->val)
        {
            l1 = head->next;
            l2 = mid;
            cur = head;
        } 
        else
        {
            l1 = head;
            l2 = mid->next;
            head = cur = mid;
        }
        while (l1 != mid || l2 != tail)
        {
            if (l1 == mid)
            {
                cur->next = l2;
                cur = l2;
                l2 = l2->next; 
            }
            else if (l2 == tail)
            {
                cur->next = l1;
                cur = l1;
                l1 = l1->next;
            }
            else
            {
                if (l1->val < l2->val)
                {
                    cur->next = l1;
                    cur = l1;
                    l1 = l1->next;
                }
                else
                {
                    cur->next = l2;
                    cur = l2;
                    l2 = l2->next;
                }
            }
        }
        cur->next = tail;
        return make_pair(head, cur);        
    }
};

这段代码是能过的,但是我现在甚至不想再去具体分析这段代码。究其原因,还是因为受到了数组的思维定势的影响,对于mergeSort,非要传起始和末尾两个参数不可。事实上,我们只需要起始结点就可以了。如果只有起始结点,就有一个问题:我们怎么把链表分成两半呢?想要搞清楚这个问题,就要解决一个问题:假如我们已经知道链表的起始和末尾结点,能否找到它的中间结点?可能有人会说,先遍历一遍链表,数一下结点个数,然后再从起始结点出发,走一半的结点数,就到达了中间结点。但事实上,我们之前做过一个环形链表的题,用到了快慢指针这种解法。事实上,找中间结点依旧可以使用这种思路。只要设快指针每次向前两个单位,慢指针每次向前一个单位,同时出发,当快指针到达终点时,慢指针就到达了中间结点。

那么,现在我们不知道链表的末尾结点,还能否用快慢指针去找中间结点呢?其实在一个链表中,我们只考虑它的某一段,想找这一段的中间结点,然后进行归并排序,那么我们的确需要知道这一段到哪里结束。可是对于一段完整的链表来说,它的末尾结点自然就是NULL,这是无法改变的事实,因此也不用一个专门的指针来标记这个末尾结点。这个过程,通过findMid这个函数实现。

知道了这一点,我们就会想,能不能在排序的时候,把不参与排序的那部分链表切掉呢?也就是说,对一个链表进行归并排序,我们能不能把这个链表从中间切开,给每一段的末尾都添加一个NULL结点,然后两边各自排序,最后再连起来?

显然,这是可行的。首先,能切开吗?切开链表是完全可以的,找到mid之后直接切掉就可以了。那么,切开以后,能排序吗?之前我们已经提到,对于某段完整的链表来说,只需要知道起始结点,就可以进行归并排序了,因此切开链表之后,我们需要保留两段的起始结点。最后,切开并分别排序之后能重新接上吗?事实上,只需要知道每段的起始结点,就可以接上了。具体如何实现,请看代码的merge函数。以下,是改进后的实现代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution 
{
public:
    ListNode* sortList(ListNode* head) 
    {
        if (!head) { return NULL; }
        if (!head->next) { return head; }
        if (!head->next->next) 
        {
            if (head->val > head->next->val) 
            { 
                swap(head->val, head->next->val); 
            }
            return head;
        }
        ListNode* mid = findMid(head), *h = mid->next;
        	//事实上是以mid->next为后一段链表的头结点,不过无伤大雅
        mid->next = NULL;
        ListNode *h1 = sortList(head), *h2 = sortList(h);
        return merge(h1, h2);      
    }

    ListNode* findMid(ListNode* head)
    {   
        ListNode* l1 = head, *l2 = head;
        while (l1 && l1->next) 
        {
            l1 = l1->next->next;
            l2 = l2->next;
        }
        return l2;
    }
    
    ListNode* merge(ListNode* h1, ListNode* h2)
    {
        /*ListNode *cur = NULL;
        if (h1->val < h2->val) 
        {
            cur = h1;
            h1 = h1->next;
        }
        else
        {
            cur = h2;
            h2 = h2->next;
        }
        ListNode *head = cur;*/
        ListNode *head = new ListNode(0), *cur = head;
        	//新建一个起始节点,到时候再舍弃,可以省去上面的代码。
        while (h1 && h2)
        {
            if (h1->val < h2->val)
            {
                cur->next = h1;
                cur = h1;
                h1 = h1->next;
            }
            else
            {
                cur->next = h2;
                cur = h2;
                h2 = h2->next;
            }
        }
        if (!h1) { cur->next = h2; }
        else { cur->next = h1; }
        //return head;
        return head->next;
    }
};

以上就是用归并排序实现对链表排序。事实上,快速排序也是可行的,这个我们下期再说。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值