精选力扣500题 第43题 LeetCode 148. 排序链表c++/java详细题解

Posted 林深时不见鹿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精选力扣500题 第43题 LeetCode 148. 排序链表c++/java详细题解相关的知识,希望对你有一定的参考价值。

1、题目

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

进阶:

  • 你可以在 O ( n l o g n ) O(nlogn) O(nlogn) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

示例 1:

输入:head = [4,2,1,3]
输出:[1,2,3,4]

示例 2:

输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目在范围 [0, 5 * 104]
  • -105 <= Node.val <= 105

2、思路

(归并排序) 时间: O ( n l o g n ) O(nlogn) O(nlogn) 空间: O ( 1 ) O(1) O(1)

自顶向下递归形式的归并排序,由于递归需要使用系统栈,递归的最大深度是 l o g n logn logn,所以需要额外 O ( l o g n ) O(logn) O(logn) 的空间。
所以我们需要使用自底向上非递归形式的归并排序算法。
基本思路是这样的,总共迭代 l o g n logn logn次:

  1. 第一次,将整个区间分成连续的若干段,每段长度是2: [ a 0 , a 1 ] , [ a 2 , a 3 ] , … [ a n − 1 , a n − 1 ] [ a 0 , a 1 ] , [a0,a1],[a2,a3],…[an−1,an−1][a0,a1], [a0,a1],[a2,a3],[an1,an1][a0,a1],然后将每一段内排好序,小数在前,大数在后;
  2. 第二次,将整个区间分成连续的若干段,每段长度是4: [ a 0 , … , a 3 ] , [ a 4 , … , a 7 ] , … [ a n − 4 , … , a n − 1 ] [ a 0 , … , a 3 ] [a0,…,a3],[a4,…,a7],…[an−4,…,an−1][a0,…,a3] [a0,,a3],[a4,,a7],[an4,,an1][a0,,a3],然后将每一段内排好序,这次排序可以利用之前的结果,相当于将左右两个有序的半区间合并,可以通过一次线性扫描来完成;
  3. 依此类推,直到每段小区间的长度大于等于 n n n 为止;

另外,当 n n n 不是2的整次幂时,每次迭代只有最后一个区间会比较特殊,长度会小一些,遍历到指针为空时需要提前结束。
在这里插入图片描述

举个例子:

根据图片可知,从底部往上逐渐进行排序,先将长度是1的链表进行两两排序合并,再形成新的链表head,再在新的链表的基础上将长度是2的链表进行两两排序合并,再形成新的链表head … 直到将长度是n / 2的链表进行两两排序合并

step=1: (3->4) -> (1->7) -> (8->9) -> (2->11) -> (5->6)
step=2: (1->3->4->7) -> (2->8->9->11) -> (5->6)
step=4: (1->2->3->4->7->8->9->11) ->5->6
step=8: (1->2->3->4->5->6->7->8->9->11)

具体操作,当将长度是i的链表两两排序合并时,新建一个虚拟头结点dummy[j,j + i - 1][j + i, j + 2 * i - 1]两个链表进行合并,在当前组中,p指向的是当前合并的左边的链表,q指向的是当前合并的右边的链表,o指向的是下一组的开始位置,将左链表和右链表进行合并,加入到dummy的链表中,操作完所有组后,返回dummy.next链表给i * 2的长度处理

注意的是:需要通过lr记录当前组左链表和右链表使用了多少个元素,用的个数不能超过i,即使长度不是 2n 也可以同样的操作

时间复杂度分析: 整个链表总共遍历 l o g n logn logn 次,每次遍历的复杂度是 O ( n ) O(n) O(n),所以总时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)

空间复杂度分析: 整个算法没有递归,迭代时只会使用常数个额外变量,所以额外空间复杂度是 O ( 1 ) O(1) O(1).

3、c++代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* sortList(ListNode* head) {
        int n = 0;
        for(auto p = head; p; p = p->next) n++;
        auto dummy = new ListNode(-1); //虚拟头节点
		dummy->next = head;
        //每次归并段的长度,每次长度依次为1,2,4,8...n/2, 小于n是因为等于n时说明所有元素均归并完毕,大于n时同理
        for(int i = 1; i < n; i *= 2)
        {
            auto cur = dummy ;
            for(int j = 1; j + i <= n; j += 2*i ){ //j代表每一段的开始,每次将两段有序段归并为一个大的有序段,故而每次+2i		   //必须保证每段中间序号是小于链表长度的,显然,如果大于表长,就没有元素可以归并了
                auto p = cur->next, q = p;//p表示第一段的起始点,q表示第二段的起始点,之后开始归并即可
                for(int k = 0; k < i; k++) q = q->next;
            	//l,r用于计数第一段和第二段归并的节点个数,由于当链表长度非2的整数倍时表长会小于i,故而需要加上p && q的边界判断
                int l = 0, r = 0;
                while(l < i && r < i && p && q) //二路归并
                {
                    if(p->val <= q->val)  cur = cur->next = p, p = p->next, l++;
                    else cur = cur->next = q, q = q->next, r++;
                }
            
                while(l < i && p) cur = cur->next = p, p = p->next ,l++;
                while(r < i && q) cur = cur->next = q, q = q->next ,r++;
                cur->next = q;//记得把排好序的链表尾链接到下一链表的表头,循环完毕后q为下一链表表头
            }
        }
        return dummy->next;
    }
};

4、java代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
		int n = 0;
        for(ListNode p = head; p!=null; p = p.next) n++;
        ListNode dummy = new ListNode(-1); //虚拟头节点
		dummy.next = head;
        //每次归并段的长度,每次长度依次为1,2,4,8...n/2, 小于n是因为等于n时说明所有元素均归并完毕,大于n时同理
        for(int i = 1; i < n; i *= 2)
        {
            ListNode cur = dummy ;
            for(int j = 1; j+i <= n; j += 2*i ){ //j代表每一段的开始,每次将两段有序段归并为一个大的有序段,故而每次+2i		   //必须保证每段中间序号是小于链表长度的,显然,如果大于表长,就没有元素可以归并了
            ListNode p = cur.next; 
            ListNode q = p;//p表示第一段的起始点,q表示第二段的起始点,之后开始归并即可
            for(int k = 0; k < i; k++) q = q.next;
            //l,r用于计数第一段和第二段归并的节点个数,由于当链表长度非2的整数倍时表长会小于i,故而需要加上p && q的边界判断
            int l = 0, r = 0;
            while(l < i && r < i && p!=null && q!=null) //二路归并
            {
                if(p.val <= q.val) 
                {
                    cur = cur.next = p;
                    p = p.next;
                    l++;
                }    
                else
                {
                    cur = cur.next = q;
                    q = q.next;
                   	 r++;
                	} 
            	}
            
            	while(l < i && p!=null) {cur = cur.next = p;p = p.next ;l++;}
            	while(r < i && q!=null) { cur = cur.next = q; q = q.next ;r++;}
            	cur.next = q;
        	}
        }
        return dummy.next;
    }
}

原题链接: 148. 排序链表
在这里插入图片描述

以上是关于精选力扣500题 第43题 LeetCode 148. 排序链表c++/java详细题解的主要内容,如果未能解决你的问题,请参考以下文章

精选力扣500题 第38题 LeetCode 300.长递增子序列c++/java详细题解

精选力扣500题 第35题 LeetCode 94. 二叉树的中序遍历c++ / java 详细题解

精选力扣500题 第55题 LeetCode 144. 二叉树的前序遍历c++/java详细题解

精选力扣500题 第52题 LeetCode 98. 验证二叉搜索树c++/java详细题解

精选力扣500题 第14题 LeetCode 92. 反转链表 IIc++详细题解

精选力扣500题 第22题 LeetCode 88. 合并两个有序数组c++详细题解