从零学算法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;
  }
相关推荐
魔道不误砍柴功几秒前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
pianmian12 分钟前
python数据结构基础(7)
数据结构·算法
闲晨4 分钟前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程31 分钟前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk1 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
好奇龙猫2 小时前
【学习AI-相关路程-mnist手写数字分类-win-硬件:windows-自我学习AI-实验步骤-全连接神经网络(BPnetwork)-操作流程(3) 】
人工智能·算法
sp_fyf_20243 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-01
人工智能·深度学习·神经网络·算法·机器学习·语言模型·数据挖掘