排序链表
题目描述:
给你链表的头结点 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)
时间复杂度和常数级空间复杂度下,对链表进行排序吗?
思路分析:
使用归并排序
大体实现:
-
基本情况处理:首先检查链表是否为空或只有一个节点。如果是,那么它已经是有序的,直接返回该链表。
-
分割链表 :使用快慢指针技术找到链表的中点,并将链表从中点处分割成两个子链表。快指针
fast
每次移动两步,慢指针slow
每次移动一步,同时用一个prevPtr
指针来记录slow
的前一个节点,以便在找到中点后将链表分割。 -
递归排序 :对分割后的两个子链表递归地调用
sortList
方法进行排序。由于递归的性质,这两个子链表最终都会被分割成更小的子链表,直到每个子链表只包含一个节点(或为空),然后这些子链表会被合并成有序链表。 -
合并链表 :使用
merge
方法将两个已排序的子链表合并成一个有序链表。合并过程中,通过比较两个链表头节点的值,将较小的节点连接到结果链表的末尾,并移动相应的指针。当其中一个链表遍历完时,将另一个链表的剩余部分直接连接到结果链表的末尾。
细节解释
-
快慢指针 :快指针
fast
每次移动两步,慢指针slow
每次移动一步。当快指针到达链表末尾时,慢指针正好位于链表的中点(或中点的前一个节点,如果链表长度为奇数)。prevPtr
用于记录slow
的前一个节点,以便在找到中点后将链表分割。 -
链表分割 :通过将
prevPtr.next
设置为null
,可以将链表从中点处分割成两个子链表。prevPtr
是slow
的前一个节点,因此prevPtr.next = null
会将slow
及其后面的节点与前面的节点断开连接。(在使用快慢指针找到中点并分割链表时,需要确保链表长度至少为2,否则fast.next.next
可能会引发NullPointerException
。但在这个实现中,由于首先检查了链表是否为空或只有一个节点,所以这种情况不会发生。) -
递归调用 :对分割后的两个子链表(
head
和slow
)public class Solution { // 主函数,用于对链表进行排序 public ListNode sortList(ListNode head) { // 如果链表为空或只有一个节点,则它已经是有序的,直接返回 if (head == null || head.next == null) { return head; } // 使用快慢指针找到链表的中点 ListNode slow = head; // 慢指针,每次移动一步 ListNode fast = head; // 快指针,每次移动两步 ListNode prevPtr = null; // 用于记录slow的前一个节点,以便分割链表 while (fast != null && fast.next != null) { prevPtr = slow; slow = slow.next; fast = fast.next.next; } // 分割链表:将链表从中间断开,prevPtr.next原本指向slow,现在设置为null prevPtr.next = null; // 递归地对左右两部分进行排序 // 注意:这里的left是原链表的左半部分,而right是原链表的右半部分(从slow开始) ListNode left = sortList(head); // 对左半部分排序 ListNode right = sortList(slow); // 对右半部分排序 // 合并两个有序链表 // 返回合并后的链表头节点 return merge(left, right); } // 合并两个有序链表的辅助函数 private ListNode merge(ListNode l1, ListNode l2) { // 创建一个哑节点,方便处理链表头部 ListNode dummy = new ListNode(0); ListNode tail = dummy; // tail用于遍历并连接合并后的链表 // 遍历两个链表,按序连接节点 while (l1 != null && l2 != null) { if (l1.val < l2.val) { tail.next = l1; l1 = l1.next; } else { tail.next = l2; l2 = l2.next; } tail = tail.next; // 移动tail到当前连接的节点 } // 如果l1或l2还有剩余节点,直接将剩余部分连接到tail后面 if (l1 != null) { tail.next = l1; } if (l2 != null) { tail.next = l2; } // 返回合并后的链表(跳过哑节点) return dummy.next; } }
分别递归调用
sortList
方法进行排序。注意,这里的head
是原链表的头节点,而slow
是分割后右子链表的头节点。 -
合并链表 :使用
merge
方法将两个已排序的子链表合并成一个有序链表。合并过程中,通过比较两个链表头节点的值,将较小的节点连接到结果链表的末尾,并移动相应的指针。当其中一个链表遍历完时,另一个链表的剩余部分(如果有的话)将直接连接到结果链表的末尾。
代码实现:
public class Solution {
// 主函数,用于对链表进行排序
public ListNode sortList(ListNode head) {
// 如果链表为空或只有一个节点,则它已经是有序的,直接返回
if (head == null || head.next == null) {
return head;
}
// 使用快慢指针找到链表的中点
ListNode slow = head; // 慢指针,每次移动一步
ListNode fast = head; // 快指针,每次移动两步
ListNode prevPtr = null; // 用于记录slow的前一个节点,以便分割链表
while (fast != null && fast.next != null) {
prevPtr = slow;
slow = slow.next;
fast = fast.next.next;
}
// 分割链表:将链表从中间断开,prevPtr.next原本指向slow,现在设置为null
prevPtr.next = null;
// 递归地对左右两部分进行排序
// 注意:这里的left是原链表的左半部分,而right是原链表的右半部分(从slow开始)
ListNode left = sortList(head); // 对左半部分排序
ListNode right = sortList(slow); // 对右半部分排序
// 合并两个有序链表
// 返回合并后的链表头节点
return merge(left, right);
}
// 合并两个有序链表的辅助函数
private ListNode merge(ListNode l1, ListNode l2) {
// 创建一个哑节点,方便处理链表头部
ListNode dummy = new ListNode(0);
ListNode tail = dummy; // tail用于遍历并连接合并后的链表
// 遍历两个链表,按序连接节点
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next; // 移动tail到当前连接的节点
}
// 如果l1或l2还有剩余节点,直接将剩余部分连接到tail后面
if (l1 != null) {
tail.next = l1;
}
if (l2 != null) {
tail.next = l2;
}
// 返回合并后的链表(跳过哑节点)
return dummy.next;
}
}