从零学算法148

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

示例 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 * 10^4^] 内

-10^5^ <= Node.val <= 10^5^

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

  • 根据这个时间复杂度能想到,不断二分的排序,那就是用归并排序了,归并的原理也就是不断递归直到单个有序(只有一个节点)后返回,然后返回上一层合并两个有序的(得到 2 个有序的节点),再返回上一层合并两个有序的(得到 4 个有序的节点),直到返回到最开始合并左右两部分链表得到完全有序的链表,先写个伪代码模版
java 复制代码
  public ListNode sortList(ListNode head) {
      return merge(起点,终点);
  }
  ListNode merge(ListNode head,ListNode tail){
      if (只有一个节点) {
          return 该节点;
      }
      ListNode mid = 中点
      return mergeTwo(merge(head,mid),merge(mid,tail));
  }
  // 合并两个有序链表
  ListNode mergeTwo(ListNode a, ListNode b){
  }
  • 这里需要考虑的主要有两点:链表边界情况的考虑以及中点的获取。
  • 这里我们用左闭右开的区间来归并,那么当起点的下一个节点为终点时也就表示我们可用的只剩下起点这一个节点,此时可以结束最内层的递归返回上一层了
  • 而中点的获取,我们采用快慢指针的方式来获取。
java 复制代码
  public ListNode sortList(ListNode head) {
  	  // 排除无需排序的情况
      if(head==null || head.next==null)return head;
      // 左闭右开的区间
      return merge(head, null);
  }
  ListNode merge(ListNode head,ListNode tail){
  	  // 只剩一个可用节点 
      if (head.next == tail) {
          head.next = null;
          return head;
      }
      // 快指针每次走两步,慢指针每次走一步,所以快指针到尾部时慢指针位于中点
      ListNode slow=head, fast=head;
      while(fast!=tail && fast.next!=tail){
          slow=slow.next;
          fast=fast.next.next;
      }
      // 不断分治,最后会分解到得到单个节点才进行合并
      return mergeTwo(merge(head,slow),merge(slow,tail));
  }
  // 递归的合并两个有序链表
  ListNode mergeTwo(ListNode a, ListNode b){
      ListNode head = null;
      if(a==null)return b;
      if(b==null)return a;
      if(a.val<=b.val){
          a.next=mergeTwo(a.next,b);
          head=a;
      }else{
          b.next=mergeTwo(b.next,a);
          head=b;
      }
      return head;
  }
  • 类似的思路们也是用递归实现归并排序,但是这里区间的划分用了不同的处理,我们在取到中点后直接在中点处断开,将一个链表真正分成两个链表,这两个链表也就是左右链表,此时也就不存在中点了,可以直接调用主函数得到排序后的链表去合并,因为入参和主函数一致了都为头节点
java 复制代码
  public ListNode sortList(ListNode head) {
      if(head==null || head.next==null)return head;
      ListNode slow = head, fast = head.next;
      while(fast!=null && fast.next!=null){
          slow = slow.next;
          fast = fast.next.next;
      }
      // head 和 temp 相当于左右两个链表各自的头节点
      ListNode temp = slow.next;
      slow.next=null;
      // 调用自己得到排序后的链表
      ListNode left = sortList(head);
      ListNode right = sortList(temp);
      // 合并两个有序链表
      return mergeTwo(left,right);
  }
  // 非递归的合并两个有序链表
  public ListNode mergeTwo(ListNode a, ListNode b){
      ListNode res = new ListNode(0);
      ListNode head = res;
      while(a!=null && b!=null){
          if(a.val<=b.val){
              head.next=a;
              a=a.next;
          }else{
              head.next=b;
              b=b.next;
          }
          head=head.next;
      }
      head.next=a==null?b:a;
      return res.next;
  }
  • 以上两种解法由于都是使用递归的解法,所以空间复杂度都为 O(log^n^),想要空间复杂度为 O(1),可以采用自底向上的非递归解法,原文。

  • 例如 [4,3,1,7,8,9,2,11,5,6],在递归解法中我们递归到最底层会得到两个长度为 1 的链表(只有一个节点的链表必定有序)然后合并成长度为 2 的有序链表,再往上回溯,两两合并得到长度为 4 的有序链表...所以我们的思路就是先两个两个地 merge ,再四个四个地 merge...

  • 例如 [4,3,1,7,8,9,2,11,5,6]

    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)
    
  • 这里需要涉及到的其实就是两个操作,先切割(cut)出两个链表,再合并(merge)两个链表,cut 指的是比如最开始我们需要两个两个 merge,假设链表为 3->2->1->4->5->null,我们先切割出两个长度为 1 的链表 3->null2->null,剩下 1->4->5->null。merge 就比如将以上切割出的两个链表合并为 2->3->null,所以我们可以先写出伪代码如下

java 复制代码
  ListNode cur = dummy.next;
  LIstNode tail = dummy;
  for(int step = 1; step < length; step *= 2){
  	  while(cur != null){
  		   ListNode left = cur;//left:3->2->1->4->5->null
  		   ListNode right = cut(left,step);//left:3->null,right:2->1->4->5->null
  		   cur = cut(right,step);//left:3->null,right:2->null,cur:1->4->5->null
  		   tail.next = merge(left,right);// 合并后的链表一个个添加到尾部
  		   while(tail.next != null)tail = tail.next;//保持 tail 始终为尾部
  	  }
  }
  • 最终代码如下
java 复制代码
  public ListNode sortList(ListNode head) {
  	  ListNode dummy = new ListNode(0);
      dummy.next = head;
      int length = 0;
      ListNode temp = head;
      // 计算链表长度
      while(temp != null){
          temp=temp.next;
          ++length;
      }
      // 用 size 更准确,每次切出 size 个节点
      for(int size = 1; size < length; size *= 2){ 
      	  // cur 只用作遍历前一个新链表,最开始从 head 开始,所以最上面 dummy.next = head;
          ListNode cur = dummy.next;
          // tail 用来获取下一个新链表
          ListNode tail = dummy;
          while(cur != null){
              ListNode left = cur;
              ListNode right = cut(left, size);
              cur = cut(right, size);
              tail.next = mergeTwo(left, right);
              while(tail.next != null)tail = tail.next;
          }
      }
      return dummy.next;
  }
  ListNode cut(ListNode head,int n){
      ListNode p = head;
      // 注意只能走 n-1 步,比如要切出一个节点就只能走 0 步才能把第一个节点切出来
      while (p != null && --n > 0)p = p.next;
      // 不够切只能返回 null 了
      if(p==null)return null;
      ListNode next = p.next;
      // 断开形成新链表
      p.next = null;
      // 返回切掉 n 个节点后的新链表的头节点
      return next;
  }
  public ListNode mergeTwo(ListNode a, ListNode b){
      ListNode res = new ListNode(0);
      ListNode head = res;
      while(a!=null && b!=null){
          if(a.val<=b.val){
              head.next=a;
              a=a.next;
          }else{
              head.next=b;
              b=b.next;
          }
          head=head.next;
      }
      head.next=a==null?b:a;
      return res.next;
  }
相关推荐
-$_$-1 分钟前
【LeetCode 面试经典150题】详细题解之滑动窗口篇
算法·leetcode·面试
hjxxlsx5 分钟前
探索 C++ 自定义函数的深度与广度
开发语言·c++
Channing Lewis5 分钟前
算法工程化工程师
算法
罗政28 分钟前
PDF书籍《手写调用链监控APM系统-Java版》第12章 结束
java·开发语言·pdf
匹马夕阳29 分钟前
详细对比JS中XMLHttpRequest和fetch的使用
开发语言·javascript·ecmascript
月巴月巴白勺合鸟月半30 分钟前
一个特别的串口通讯
开发语言·串口通讯
乄北城以北乀36 分钟前
第1章 R语言中的并行处理入门
开发语言·分布式·r语言
全栈老实人_1 小时前
农家乐系统|Java|SSM|VUE| 前后端分离
java·开发语言·tomcat·maven
Tester_孙大壮1 小时前
Python爬虫技术科普
开发语言·爬虫·python
帅逼码农1 小时前
有限域、伽罗瓦域、扩域、素域、代数扩张、分裂域概念解释
算法·有限域·伽罗瓦域