Code Top -- 10 链表排序

迭代归并,O(nlogn) + O(1)

Posted by OUC_LiuX on April 16, 2022

from CodeTop, Leetcode, 148. 排序链表
迭代归并。

题意:

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

解读:
要求时间复杂度 O(nlogn) 自然而然就想到了快排和归并。但是快排,需要找随机节点,链表没有下标很难操作,于是归并。要求空间复杂度 O(1),于是不能使用递归,要使用迭代。使用迭代,反而好做了。

  • 首先,头结点不一定是最小的,可能会变动,那这种情况下就需要一个虚拟头结点,其 next 指针指向真正的头结点,这样无论真正的头结点怎样变化,我只需要返回虚拟头的 next 即可。
  • 自底向上两两归并,设每次两两合并的段的长度为 i,则 i = 1 开始,每次变化到原来的两倍,直到 i >= n,说明当前两两合并的段的长度已经等于或超过了总的链表长度,也即,整个链表合并完成。
  • 每一轮(基本段长度相等的,称为一轮)的合并,每一对两两相邻的段合并结束后,其头结点也可能变化,于是也需要使用一个虚拟头结点,其 next 指针指向真正的头结点。这个虚拟头结点会随着每两两合并的段的后移而后移,且始终其 next 节点指向当前需要两两合并的段的头结点。
    比如,cur 最初等于虚拟头结点,指向 1,2 段的头节点(也就是 1 的头)。当第 1, 2 段完成合并后,cur 应当在完成合并的1,2 段的末尾,并且其 next 指针指向 3,4 段的头(也即是 3 的头)。
  • 在每一轮(基本段长度相等的,称为一轮)的合并中,可能有不止一对两两相邻的段需要被合并。设起点(也就是相邻两段的第一段的起点)为 j,显然起点每次应当后移 2*i ,走到下一个需要两两合并的段的头结点位置。直到 j + i 大于 n (j 从零开始,j + i 大于 n 代表着当前段在段长度为 i 的情况下已经是孤零零一段,后面没有段可以合并了)。
    当然,j 的作用,也仅限于计数计长度。
  • 新建两个指针 first, second 指向两个段的头结点,新建两个变量 f, s 代表两个指针走了多少步(从零开始),开始归并:
    1. 显然,两两合并可进行的条件是指针都存在,且两个指针走的步数都小于 i。
    2. 两个指针指向相邻两段的节点,选取值较小的节点接到 cur 后面,相应的指针后移一位,步数加一。
    3. 两两合并后,应当由一个指针没有走完相应步数,不管具体是哪个指针,两个 while 收个尾,将未走完相应步数的一段接到 cur 后面。
  • 结束,cur 指针应当指向合并段的最后节点,那么显然还应当使其 next 指向后面两段的头;后面两段的头,显然被 second 指向。于是 cur -> next = second。

代码及详细注释:

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (!head || !head -> next) return head;

        //每次先归并好其中一小段,之后对两小段之间进行归并           
        int n = 0;
        for(auto p = head; p ; p = p->next) n ++;

        ListNode* dummy = new ListNode(-1, head); 
        // 因为 head 可能会变,因为原始的 head->val 可能不是最小的,就会被移到后面          

        // 要明白下面第1个和 第2个for循环是什么,第1个for循环是 纵向层与层之间的迭代,         
        // 第2个for循环是 横向 每层 从左往右 每次 2i段遍历            
        for(int i = 1; i < n; i *= 2 ){ 
            //每次归并段的长度,每次长度依次为1,2,4,8...n/2               
            //小于n是因为等于n时说明所有元素均归并完毕,大于n时同理            
            auto cur = dummy; 
            // 因为下面for循环第一次进入时(即每一层第一段头结点 是从第一个结点head开始的,            
            // 这个第一个结点head也是会变的),要保证 first = head, 所以这里要将           
            // cur = dummy, 因为dummy->next =head, 能保证 first = cur->next = head             
            // 根据把链表头结点 索引当成0 或 1 有两种写法:           
            // 写法一: j = 0; j + i < n;                 
            // 写法二:j = 1; j + i <= n;         
            for(int j = 0; j + i < n; j += 2 * i){
            //j代表每一段的开始,每次将两段有序段归并为一个大的有序段,故而每次+2i         
            //必须保证每段中间序号是小于链表长度的,显然,如果大于表长,就没有元素可以归并了             
                auto first = cur->next, second = first;
                // p表示2i段第一段的起始点,cur是上一排好序的2i链表段的尾结点          
                // (这个起始点会变,不能用p=head),q表示2i段第二段的起始点,之后开始归并即可                  
                for(int k = 0; k < i; k ++) second = second->next;

                //f,s用于计数第一段和第二段归并的节点个数,由于当链表长度非2的整数倍时           
                // 表长会小于i,故而需要加上first && second的边界判断             
                 int f = 0, s = 0;
                 while(f < i && s < i && first && second){  
                // 加 && second 是因为 第二段长度 可能不满 i              
                // 这里可以不用 && first, 因为上面 j+i<=n 保证了,有第二段,          
                // 所以第一段肯定存在 且 长度为i
                     if(first->val <= second->val) 
                        cur = cur->next = first,first = first->next,f ++; 
                        // 等号的赋值顺序为从右到左,故而该句意思为             
                        // cur->next = first,cur = cur->next,即将小的一点插入cur链表中             
                     else cur = cur->next = second,second = second->next,s++;
                 }


                //归并排序基本套路            
                 while(f < i && first) 
                    cur = cur->next = first,first = first->next,f ++; 
                    // 这里可以不加 && first,原理同上            
                 while(s < i && second) 
                    cur = cur->next = second,second = second->next,s ++; 
                    // 跳出循环时,second已经是 j+2i 下一2i链表段的新的头结点         
                 cur->next = second; 
                 // 记得把排好序的链表尾 链接 到 j+2i 下一2i链表 的表头,         
                 // 循环完毕后 second 为下一2i链表表头          
            }
        } 
        return dummy->next;
    }
};