98. 链表排序------归并排序

在 O(n log n) 时间复杂度和常数级的空间复杂度下给链表排序。

样例

给出 1->3->2->null,给它排序变成 1->2->3->null.

分析: 
  由于题目要求时间复杂度我O(nlgn),因此选择排序和插入排序可以排除。 
  在排序算法中,时间复杂度为O(nlgn)的主要有:归并排序、快速排序、堆排序。 
  其中堆排序的空间复杂度为(n),也不符合要求,因此也可以排除。 
  归并排序在对数组进行排序时,需要一个临时数组来存储所有元素,空间复杂度为O(n)。但是利用归并算法对单链表进行排序时,可以通过        next指针来记录元素的相对位置,因此时间复杂度也为O(1)。 
  因此可以考虑用快排和归并来实现单链表的排序。 

一、归并排序:

思想算法思想是首先用快慢指针的方法找到链表中间节点,然后递归地对两个子链表进行排序,把两个排好序的子链表合并成一条有序的链表。归并排序算是链表排序中的最好选择,保证了最好和最坏时间复杂度都是O(n log n),而且它在数组排序中广受诟病的空间复杂度在链表排序中也从O(N)降到了O(1)。合并函数和165题合并两个排序链表 

虽然归并排序比较占用内存,但是效率高且稳定。

代码:

1、归并排序递归方法

注意,Find Mid用了2种解法。或者是让Fast提前结束,或是让Fast先走一步,目的就是要取得中间节点的前一个。这样做的目的,主要是为了解决:1->2->null 这一种情况。如果不这么做,slow会返回2.这样我们没办法切割2个Node的这种情况。


/**
 * Definition of ListNode
 * class ListNode {
 * public:
 *     int val;
 *     ListNode *next;
 *     ListNode(int val) {
 *         this->val = val;
 *         this->next = NULL;
 *     }
 * }
 */


class Solution {
public:
    /*
     * @param head: The head of linked list.
     * @return: You should return the head of the sorted linked list, using constant space complexity.
     */
    ListNode * sortList(ListNode * head) {
        // write your code here
     if(head==NULL||head->next==NULL)
     return head;//这一句很重要,当递归分解完毕时需要其返回
     // step 1.将链表分成两个
     ListNode *pre=NULL,*slow=head,*fast=head;
     while(fast!=NULL&&fast->next!=NULL)
     {
         pre=slow;
         slow=slow->next;
         fast=fast->next->next;
     }
     pre->next=NULL;//将链表分成两部分
     // step 2. sort each half
     ListNode *l1=sortList(head);
     ListNode *l2=sortList(slow);
     // step 3. merge l1 and l2
     return merge(l1,l2);//回溯过程中,每个回溯均会执行一次return语句,因为return语句是sortList函数里//面的语句;
    }
    ListNode* merge(ListNode *l1,ListNode *l2)//其实这就是两个有序链表的合并
    {
        ListNode*p=new ListNode(0),*l=p;
        while(l1!=NULL&&l2!=NULL)
        {
            if(l1->val<l2->val)
            {
                p->next=l1;
                l1=l1->next;
            }
            else 
            {
            p->next=l2;
            l2=l2->next;
            }
            p=p->next;
        }
        if(l1==NULL)
        p->next=l2;
        if(l2==NULL)
        p->next=l1;
        return l->next;
    }
};
2、非递归的归并排序

(下面的内容来自博文http://www.cnblogs.com/bin3/articles/1858691.html)

面创新工场时被问到链表排序题。当时思路混乱,没有想出时间空间均较优的方法。后来再想,至少能用归并排序嘛,即使实现得不优美。这充分体现了我思维方法的一个不足,面对新问题有时会陷入东敲西打浅尝辄止的胡思乱想,而忽视了从基本方法出发稍加变通便能解决新问题的思路。

再一翻侯捷的《STL源码剖析》中介绍的SGI STL中list的sort函数的实现,修改其他无关细节之后的代码如下:

[cpp]  view plain  copy
  1. template<class T>  
  2. void list_sort(list<T>& lst)  
  3. {  
  4.     if (lst.size() > 1)  
  5.     {  
  6.         list<T> carry;  
  7.         list<T> counter[64];  
  8.         int fill = 0;  
  9.         while (!lst.empty())  
  10.         {  
  11.             carry.splice(carry.begin(), lst, lst.begin());  
  12.             int i = 0;  
  13.             while(i < fill && !counter[i].empty())  
  14.             {  
  15.                 counter[i].merge(carry);  
  16.                 carry.swap(counter[i++]);  
  17.             }  
  18.             carry.swap(counter[i]);  
  19.             if (i == fill) ++fill;  
  20.         }  
  21.         for (int i = 1; i < fill; ++i)  
  22.             counter[i].merge(counter[i-1]);  
  23.         lst.swap(counter[fill-1]);  
  24.     }  
  25. }  


侯捷的注释是本函数采用的是快速排序的方法。我硬着头皮看了多遍硬是没看懂。网上的几篇帖子也没有说明白。直到把中间结果输出才明白,侯捷的注释是错误的,这其实是归并排序的非递归实现

 

要理解这个实现,首先明确几个函数的作用:

 
    
void  list::splice( iterator pos, list &  lst, iterator del );

splice把lst中del所指元素删除并插入到当前list的pos位置上。

 
    
void  list::merge( list  & lst );

merge把lst的元素合并到当前list,参数lst的元素会被清空的。

再明确几个变量的作用:

counter[i]如不空,则存储2^i个已排好序的元素。

carry只是起中转作用。

fill标记非空counter数组元素的下标i的上界,初始时为0。

 

该实现可这样理解:

第9-20行是主循环,每次删除lst的首元素并将其放入counter数组列表的合适位置,直至lst为空。

第11行将lst首元素移动到carry中。

第12-18行从counter[0]开始,如当前处理的元素counter[i]非空,则归并carry与counter[i],将结果放到carry中并把counter[i]置空,以此类推处理后一个counter元素,直到当前处理的counter元素为空。这样的处理能一直保持counter[i]的特性,即如不空则存储2^i个已排好序的元素。

第19行在适当时候更新fill值。

第21-23行将counter数组的所有元素归并,并将最终的排序结果交回给lst。

 

可以看一个运行实例。假设lst包含元素“7 9 0 6 10 4 0 7 5 1 0”。以下输出为从左往右每把一个lst的元素放入counter后,counter数组的存储内容。

 

+ 7
[0] 7
 
+ 9
[0]
[1] 7 9
 
+ 0
[0] 0
[1] 7 9
 
+ 6
[0]
[1]
[2] 0 6 7 9
 
+ 10
[0] 10
[1]
[2] 0 6 7 9
 
+ 4
[0]
[1] 4 10
[2] 0 6 7 9
 
+ 0
[0] 0
[1] 4 10
[2] 0 6 7 9
 
+ 7
[0]
[1]
[2]
[3] 0 0 4 6 7 7 9 10
 
+ 5
[0] 5
[1]
[2]
[3] 0 0 4 6 7 7 9 10
 
+ 1
[0]
[1] 1 5
[2]
[3] 0 0 4 6 7 7 9 10
 
+ 0
[0] 0
[1] 1 5
[2]
[3] 0 0 4 6 7 7 9 10

最后归并counter中的所有元素得到最终的排序结果“0 0 0 1 4 5 6 7 7 9 10”。

 要方便记住上面的算法,可以用一个情形来进行记忆:counter[0]有一个元素1,counter[1]有两个元素2和3,fill为2,现在从lst中新加入一个元素0,则carry首先为0,然后i为0<fill并且counter[0]不为空,则counter[0]先merge carry然后再与carry交换,这个时候carry为0和1。然后i为1<fill并且counter[1]不为空则counter[1]先merge carry然后再与carry交换,这个时候carry为0,1,2,3,接着i为2==fill,则退出循环。这个时候,carry与counter[2]交换,则counter[2]为0,1,2,3。i与fill相等,则fill为3。最后不再有其他元素,则把所有元素合并到counter[2],再与lst进行交换。

这其实是一个链表上的归并排序的非递归实现。好处如下:

1.使用归并排序保证了最坏的时间复杂度为O(nlog(n))。2.利用链表结构使归并的过程既只需使用常数的额外空间,时间上又很高效。3.消除了递归实现的开销。

该实现将链表数据结构和归并排序算法的优势结合得天衣无缝,令人叹服。赞!

 总结:

      对于链表L, 若要将一个链表等分为两个链表,我们用如下方法

1、快慢指针法

可以使用快慢指针的方式,快的指针每次移动两步,慢的指针每次移动一步,直到快的指针到达链表尾部时,此时的慢指针就是要找的分界点。

ListNode *slow=head,*fast=head->next;

while(fast!=NULL&&fast->next!=NULL)

{slow=slow->next;

fast=fast->next->next;}

这样循环结束, slow为分界点,即第二段链表的首节点指针,或第一段链表的尾指针

 

  
 





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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值