148. 排序链表

LeetCode 原题链接 题目

实现思路

链表不能像数组一样随机访问,所以更适合使用归并排序:

  • 使用快慢指针找到链表中点,将链表拆成左右两段。
  • 分别递归排序左右两段链表。
  • 将两个有序链表合并成一个有序链表。
  • 递归终止条件是链表为空,或者链表只有一个节点。

时间复杂度是 O(nlogn),空间复杂度是递归调用栈的 O(logn)

代码实现

// sortList 函数接收链表头节点 head,返回排序后的链表头节点
var sortList = function (head) {
  // 如果链表为空,或者链表只有一个节点,说明已经有序,直接返回
  if (head === null || head.next === null) {
    // 返回当前链表头节点,作为递归的终止结果
    return head;
  }

  // slow 是慢指针,用来寻找链表中点,初始指向头节点
  let slow = head;
  // fast 是快指针,每次走两步;从 head.next 开始可以让 slow 停在左半部分的尾节点
  let fast = head.next;

  // 当 fast 还能继续向后走时,说明 slow 还没有到达中间位置
  while (fast !== null && fast.next !== null) {
    // slow 每次只走一步
    slow = slow.next;
    // fast 每次走两步
    fast = fast.next.next;
  }

  // slow.next 是右半部分链表的头节点,需要先保存下来
  const rightHead = slow.next;
  // 将 slow.next 置为 null,把原链表断开成左右两个独立链表
  slow.next = null;

  // 递归排序左半部分链表,head 仍然是左半部分的头节点
  const left = sortList(head);
  // 递归排序右半部分链表,rightHead 是右半部分的头节点
  const right = sortList(rightHead);

  // 将两个已经有序的链表合并,并返回合并后的头节点
  return merge(left, right);
};

// merge 函数接收两个升序链表 list1 和 list2,返回合并后的升序链表
function merge(list1, list2) {
  // 创建虚拟头节点,方便统一处理结果链表的第一个节点
  const dummy = new ListNode(0);
  // cur 指向结果链表的尾节点,后续新接入的节点都挂在 cur.next 上
  let cur = dummy;

  // 当两个链表都还有节点时,持续比较两个链表当前节点的值
  while (list1 !== null && list2 !== null) {
    // 如果 list1 当前节点的值更小或相等,就把 list1 当前节点接到结果链表后面
    if (list1.val <= list2.val) {
      // cur.next 指向 list1 当前节点,完成节点拼接
      cur.next = list1;
      // list1 指针后移,准备比较 list1 的下一个节点
      list1 = list1.next;
    } else {
      // 否则说明 list2 当前节点更小,就把 list2 当前节点接到结果链表后面
      // cur.next 指向 list2 当前节点,完成节点拼接
      cur.next = list2;
      // list2 指针后移,准备比较 list2 的下一个节点
      list2 = list2.next;
    }

    // cur 后移到结果链表的最新尾节点
    cur = cur.next;
  }

  // 循环结束后,至少有一个链表已经为空;另一个链表剩余部分本身已经有序,直接接上即可
  cur.next = list1 === null ? list2 : list1;

  // dummy 是虚拟头节点,真正的排序结果从 dummy.next 开始
  return dummy.next;
}

关键点

  • 找中点时让 fasthead.next 开始,可以保证 slow 停在左半部分的尾节点,方便执行 slow.next = null 切断链表。
  • 这里不是单纯为了找到“中间节点”,而是为了找到“左半部分的尾节点”。如果 fasthead 开始,两个节点的链表中 slow 会停在最后一个节点,导致 rightHead = slow.nextnull,链表没有被真正拆开,递归会一直处理同一段链表。
  • 例如链表 4 -> 2,让 fast = head.next 时,循环不会执行,slow 停在 4,可以拆成 42 两段;这能保证递归规模持续变小。
  • 合并时直接复用原链表节点,不需要创建新的业务节点,只需要一个虚拟头节点辅助拼接。
  • 必须先保存 rightHead = slow.next,再断开 slow.next,否则右半部分会丢失。