从零学算法148

12 篇文章 0 订阅
7 篇文章 0 订阅

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 * 104] 内
-105 <= Node.val <= 105
进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

  • 根据这个时间复杂度能想到,不断二分的排序,那就是用归并排序了,归并的原理也就是不断递归直到单个有序(只有一个节点)后返回,然后返回上一层合并两个有序的(得到 2 个有序的节点),再返回上一层合并两个有序的(得到 4 个有序的节点),直到返回到最开始合并左右两部分链表得到完全有序的链表,先写个伪代码模版
  •   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){
      }
    
  • 这里需要考虑的主要有两点:链表边界情况的考虑以及中点的获取。
  • 这里我们用左闭右开的区间来归并,那么当起点的下一个节点为终点时也就表示我们可用的只剩下起点这一个节点,此时可以结束最内层的递归返回上一层了
  • 而中点的获取,我们采用快慢指针的方式来获取。
  •   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;
      }
    
  • 类似的思路们也是用递归实现归并排序,但是这里区间的划分用了不同的处理,我们在取到中点后直接在中点处断开,将一个链表真正分成两个链表,这两个链表也就是左右链表,此时也就不存在中点了,可以直接调用主函数得到排序后的链表去合并,因为入参和主函数一致了都为头节点
  •   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(logn),想要空间复杂度为 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,所以我们可以先写出伪代码如下
  •   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 始终为尾部
      	  }
      }
    
  • 最终代码如下
  •   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;
      }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值