Q1-LeetCode148- 排序链表

117 阅读5分钟

实现思路

1- 归并排序- 自顶向下思维技巧:

1 归并排序-普遍性:

  • 把数据分成两半:找到中点并分割==> 中点分割
  • 对每一半 递归排序==> 中序递归- 子序列递归排序
  • 合并两个有序序列==> 合并两个有序子序列

2.1 链表归并排序特殊性1- 链表中点切割==> 快慢指针

  • 不完全平均的分割 并不影响算法的正确性,因为 递归会持续分割 直到不能再分
  • 即 从正确性角度:任意位置切割都可以
  • 但是从性能角度:中点附近切割→ O(n log n),随机位置切割→ 最坏是 O(n^2)

2- 自底向上排序

1 自底向上sort = step * (cut + merge):

  • 分别以 1/2/4/8/k个节点为 1组车组 + 对车组(a, b)两两配对 排序合并

2.1 获取 第1组车厢 头节点a1

2.2 通过cut: 斩断a1所有尾结点 + 返回第2组车厢 头节点b1

2.3 通过cut: 斩断b1所有尾结点 + 返回下一队车厢(a2, b2)中 a2

2.4 更新cur, 让它指向 下一队待处理车厢的头节点a2

  • 由于 后续merge 需要斩断尾结点才能正确处理节点,后续还要能 处理下一队车厢
  • 所以只能在这里 更新cur的指向

2.5 merge: 合并+排序(a1 + b1) 这一队车厢,返回排好序的 这对车厢的头节点x

2.6 更新pre,以保证pre指向为 下一队车厢(a2, b2)中 a2的前一个节点

3- 快排中序实现

1 快排-普遍性:

  • 确定pivot/x: 找到随机基准点
  • partition-1: 创建/划分确定 左右分区
  • partition-2: 向 左右分区内 放入对应成员
  • partition-3: 放置pivot/x 到正确位置
  • 中序递归左右子区间
  • 返回排序后的 结果

2 链表快排-特殊性:

  • 无法随机访问元素,选择pivot通常使用 头节点/链表中点
  • 通过next指针操作而不是交换,来正确排序 链表节点

参考文档

01 方法1&2代码参考

02 方法2图示参考

03 快排实现参考

代码实现

1 方法1: 自顶向下归并排序,时间复杂度 O(n * logn) 空间复杂度O(logn)

function sortList(head) {
  if (!head || !head.next) return head;
  // 寻找中点并分割
  // 易错点1: fast要是head.next,而不能是head, 否则会死循环
  // 原因: fast如果是head, 在偶数长度的链表中(如1->2->null),slow会指向2导致死循环
  let slow = head, fast = head.next, mid = null;
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next; 
  }
  mid = slow.next;
  slow.next = null;
  // 中序递归- 子序列递归排序
  const l1 = sortList(head);
  const l2 = sortList(mid);
  // 合并有序子序列
  return merge(l1, l2);
}

function merge(l1, l2) {
  let dummy = new ListNode(-1);
  let pre = dummy;
  while (l1 && l2) {
    if (l1.val <= l2.val) {
      pre.next = l1;
      l1 = l1.next;
    } else {
      pre.next = l2;
      l2 = l2.next;
    }
    pre = pre.next;
  }
  pre.next = l1 ? l1 : l2;
  return dummy.next;
}

2 方法2: 自底向上归并排序,时间复杂度 O(n * logn) 空间复杂度O(1)

function sortList(head: ListNode | null): ListNode | null {
  // 处理特殊情况
  if (!head || !head.next) return head

  //S1 设置虚拟头节点
  let dummy = new ListNode(-1, head)

  //S2 获取链表长度,用于明确 归并的截止长度
  let len = 0;
  let cur = head;
  while (cur) {
    len++;
    cur = cur.next;
  }

  //S3 分别以 1/2/4/8/k个节点为 1节车厢 + 对车厢(a, b)两两配对,进行排序
  for (let step = 1; step < len; step *= 2) {
    let pre = dummy; // pre固定指向 每轮循环中的 每对配对车厢(a,b)中的 a1的前一个节点
    // 易错点1: cur不能指向head 
    let cur = dummy.next; // cur固定指向每轮循环中的 每对配对车厢(a,b)中的 a头节点
    // 易错点2: 每节车厢需要保证cur有值
    while (cur) {       
      let a1 = cur;           // 获取a车厢的 头节点a1
      let b1 = cut(a1, step); // 根据a1 + step, 得到b车厢的 头节点b1
      cur = cut(b1, step);   // 更新cur为下一对车厢的头结点a2, 同时隐式切断了b1的尾节点
      // 更新pre, 以保证pre指向为 下一对车厢(a2, b2)中 a2的前一个节点
      pre.next = merge(a1, b1);
      while (pre.next) {
        pre = pre.next;
      }
    }
  }
  return dummy.next;
};


// 对以head为头结点的链表,截断前n个节点,返回截断后的新头节点
function cut(head: ListNode, n: number) {
  while (head && --n > 0) head = head.next;
  const bHead = head ? head.next : null;
  if (head) head.next = null;
  return bHead;
}
    
// 合并 l1和l2 2个有序链表
function merge(l1: ListNode, l2: ListNode) {
  let dummy = new ListNode(-1)
  // 易错点: 为了返回头节点,需要要有新对象pre,用于后移指针
  let pre = dummy
  while (l1 && l2) {
    if (l1.val < l2.val) {
      pre.next = l1
      l1 = l1.next
    } else {
      pre.next = l2
      l2 = l2.next
    }
    pre = pre.next
  }
  pre.next = l1 ? l1 : l2
  return dummy.next
}

3 方法3: 快排中序递归,时间复杂度 O(n * logn) 空间复杂度O(logn)

function sortList(head) {
  if (!head || !head.next) return head
  // 1 确定pivot
  let x = head.val
  // 2.1 partition- 设定左右区间
  let ltH = new ListNode(-1), gtH = new ListNode(-1)
  let ltP = ltH, gtP = gtH, cur = head.next
  // 2.2 partiton- 分区左右区间
  while (cur) {
    if (cur.val < x) {
      ltP.next = cur
      ltP = ltP.next
    } else {
      // 这里有一个注意点: 等于x的节点,需要放在右边区
      // 如果放在左边:如果都是等于x的节点,那么在递归sortList左右子区间时,会导致死循环
      gtP.next = cur
      gtP = gtP.next
    }
    cur = cur.next
  }
   // 2.3 partiton- 放置pivot到正确位置
   ltP.next = null
   gtP.next = null  
   head.next = null
   ltP.next = head
   // 3 递归partition 左右区间
   ltH = sortList(ltH.next)
   gtH = sortList(gtH.next)
   // 4 返回排序后的链表头节点
   head.next = gtH
   return ltH
}