🚩传送门:牛客题目

题目

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

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

示例 :
[NC]70. 单链表的排序 - 图1

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

解题思路:手动模拟插入

最容易想到的思路
备注:一般链表为了方便操作,都会创建一个 dummyHead 头结点
将未排序列表的每个结点依次取出,然后插入到已排序的列表中使之有序,插入的过程为:
利用 pNode 从新链表的 dummyHead 检查至 Tail ,直至遇到 waitSortNode.val <= pNode.next.val ,便将
waitSortNode 插入至 pNode 后面 。(此时 pNode 指向的结点正好为待插入位置的上一个结点)

复杂度分析

时间复杂度: [NC]70. 单链表的排序 - 图2 ,待排序的 n 个结点,每个结点都需要遍历 n 次。

空间复杂度:[NC]70. 单链表的排序 - 图3,没有使用额外空间

我的代码

  1. public class Solution {
  2. public ListNode sortInList (ListNode head) {
  3. if(head==null)return head;
  4. // 创建新的头结点,便于操作
  5. ListNode dummyHead =new ListNode(Integer.MIN_VALUE);
  6. while(head!=null){//只要head还有结点
  7. ListNode waitSortNode=head;
  8. head=head.next;
  9. ListNode pNode=dummyHead;
  10. while(pNode.next!=null&&waitSortNode.val>pNode.next.val){
  11. pNode=pNode.next;
  12. }
  13. waitSortNode.next=pNode.next;
  14. pNode.next=waitSortNode;
  15. }
  16. return dummyHead.next;
  17. }
  18. }

解题思路:优先队列 -> 小根堆

创建一个小根堆,将每个链表结点放入小根堆中,依次吐出,每次吐出的都是 ListNode.val 值最小的结点,利用 Tail 将吐出的结点依次插入 新链表的尾部 即可实现链表排序。

复杂度分析

时间复杂度: [NC]70. 单链表的排序 - 图4 ,优先队列内部结构是堆,堆排序时间复杂度是 [NC]70. 单链表的排序 - 图5

空间复杂度:[NC]70. 单链表的排序 - 图6,队列内部需要存储 n 个指针变量。

我的代码

  1. class Solution {
  2. public ListNode sortList(ListNode head) {
  3. if(head==null)return head;
  4. //1.创建小根堆
  5. Queue<ListNode>queue=new PriorityQueue<ListNode>((val1, val2)->val1.val-val2.val);
  6. //2.所有结点入堆
  7. while(head!=null){
  8. queue.add(head);
  9. head=head.next;
  10. }
  11. ListNode Head=null;
  12. Head=queue.poll();
  13. ListNode Tail=Head;
  14. //3.依次吐出堆中结点,追加至新链表的尾部
  15. while(!queue.isEmpty()){
  16. Tail.next=queue.poll();
  17. Tail=Tail.next;
  18. }
  19. Tail.next=null;
  20. return Head;
  21. }
  22. }

解题思路:归并排序

题目要求时间空间复杂度分别为 [NC]70. 单链表的排序 - 图7[NC]70. 单链表的排序 - 图8 ,根据时间复杂度我们自然想到二分法,从而联想到归并排序

对数组做归并排序的空间复杂度为 [NC]70. 单链表的排序 - 图9 ,分别由新开辟数组 [NC]70. 单链表的排序 - 图10 和递归函数调用 [NC]70. 单链表的排序 - 图11 组成,
而根据链表特性:链表可以通过修改引用来更改节点顺序,无需像数组一样开辟额外空间,因此是 [NC]70. 单链表的排序 - 图12

通过递归实现链表归并排序,有以下两个环节:
分割 cut 环节: 找到当前链表中点,并从中点将链表断开,分别递归左右两个链表。

  • 我们使用 fast,slow 快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。

    找到中点 slow 后,执行 slow.next = null 将链表切断。

  • 分别递归左右两个链表,左端点为原链表的 head ,右端点为中心节点 slow 的下一个节点 head2

  • cut 递归终止条件: 只有一个节点时,直接返回此节点。

合并 merge 环节: 二路归并,将两个排序链表合并为一个排序链表。
[NC]70. 单链表的排序 - 图13

复杂度分析

时间复杂度: [NC]70. 单链表的排序 - 图14 ,使用的是归并排序。

空间复杂度:[NC]70. 单链表的排序 - 图15,常数固定空间

我的代码

  1. class Solution {
  2. public static ListNode sortList(ListNode head) {
  3. //1.空结点或单结点,直接返回
  4. if (head == null || head.next == null)
  5. return head;
  6. //2.双指针找中点
  7. ListNode fast = head.next,slow = head;
  8. while (fast != null && fast.next != null) {
  9. slow = slow.next;
  10. fast = fast.next.next;
  11. }
  12. ListNode head1 = head; //左区间端点:head
  13. ListNode head2 = slow.next; //右区间端点:slow.next
  14. slow.next = null; //断开,左区间为[head,slow],右区间为[head2,null)
  15. ListNode list1 = sortList(head1); //左区间归并后的有序的新链表list1
  16. ListNode list2 = sortList(head2); //右区间归并后的有序的新链表list
  17. //3.二路归并
  18. ListNode dummyHead = new ListNode(0); //方便归并的头部结点
  19. ListNode pCur= dummyHead; //二路归并的哨兵指针
  20. while (list1 != null && list2 != null) {
  21. if (list1.val < list2.val) { //值小的结点添加至新链表
  22. pCur.next = list1;
  23. list1 = list1.next;
  24. } else {
  25. pCur.next = list2;
  26. list2 = list2.next;
  27. }
  28. pCur = pCur.next;
  29. }
  30. pCur.next = list1 != null ? list1 : list2;//将非空一方添加进新链表
  31. return dummyHead.next;
  32. }
  33. }

解题思路:归并排序(从底至顶直接合并)

个人认为这个方法太复杂没必要
对于非递归的归并排序,需要使用迭代的方式替换cut环节:
我们知道,cut 环节本质上是通过二分法得到链表最小节点单元,再通过多轮合并得到排序结果。

  • 每一轮合并 merge 操作针对的单元都有固定长度 intv,例如:
  • 第一轮合并时 intv = 1,即将整个链表切分为多个长度为 1 的单元,并按顺序两两排序合并,合并完成的已排序单元长度为 2
  • 第二轮合并时 intv = 2,即将整个链表切分为多个长度为 2 的单元,并按顺序两两排序合并,合并完成已排序单元长度为 4
  • 以此类推,直到单元长度 intv >= 链表长度,代表已经排序完成。

本质来说就是:使用迭代的方式替换递归切割环节

[NC]70. 单链表的排序 - 图16

复杂度分析

时间复杂度: [NC]70. 单链表的排序 - 图17 ,使用的是归并排序。

空间复杂度:[NC]70. 单链表的排序 - 图18,常数固定空间

我的代码

  1. class Solution {
  2. public static ListNode sortList(ListNode head) {
  3. if (head == null||head.next==null) return head;
  4. int length = 0;
  5. ListNode node = head;
  6. //1.统计链表长度
  7. while (node != null) {
  8. length++;
  9. node = node.next;
  10. }
  11. ListNode dummyHead = new ListNode(0, head); //创建有序链表的头结点
  12. for (int subLength = 1; subLength < length; subLength <<= 1) {
  13. ListNode prev = dummyHead, curr = dummyHead.next; //prev保存for一次循环中的有序部分
  14. //2.切割
  15. while (curr != null) {
  16. //#1.统计第一个长度为subLength的链表,端点为head1
  17. ListNode head1 = curr;
  18. for (int i = 1; i < subLength && curr.next != null; i++) {
  19. curr = curr.next;
  20. }
  21. //#2.统计第二个长度为subLength的链表,端点为head2
  22. ListNode head2 = curr.next;
  23. curr.next = null;
  24. curr = head2;
  25. for (int i = 1; i < subLength && curr != null && curr.next != null; i++) {
  26. curr = curr.next;
  27. }
  28. ListNode next = null;
  29. //pre存储已操作部分,cur存储后面未操作部分
  30. if (curr != null) {
  31. next = curr;
  32. curr = curr.next;
  33. next.next=null;
  34. }
  35. //3.合并操作
  36. ListNode merged = merge(head1, head2);
  37. //#3.将已合并部分与pre已操作部分连接,pre后移指向已操作部分的最后一个结点,等待接下来的合并部分的到来
  38. prev.next = merged;
  39. while (prev.next != null) {
  40. prev = prev.next;
  41. }
  42. }
  43. }
  44. return dummyHead.next;
  45. }
  46. public static ListNode merge(ListNode list1, ListNode list2) {
  47. ListNode dummyHead = new ListNode(0); //方便归并的头部结点
  48. ListNode pCur= dummyHead; //二路归并的哨兵指针
  49. while (list1 != null && list2 != null) {
  50. //将值小的添加至新链表
  51. if (list1.val < list2.val) {
  52. pCur.next = list1;
  53. list1 = list1.next;
  54. } else {
  55. pCur.next = list2;
  56. list2 = list2.next;
  57. }
  58. pCur = pCur.next;
  59. }
  60. //将非空一方添加进新链表
  61. pCur.next = list1 != null ? list1 : list2;
  62. return dummyHead.next;
  63. }
  64. }