一、题目描述
给你链表的头结点 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),是一种效率较高的排序算法。
归并排序的基本思想是将链表分成两半,分别对这两半进行排序,然后将排序好的两部分合并在一起。在链表中实现这一思想相对简单,因为只需要改变节点的next指针即可完成排序。
以下是使用归并排序对链表进行排序的步骤:
- 找到链表的中点,可以使用快慢指针法。
- 将链表从中点断开,形成两个子链表。
- 对两个子链表分别进行递归排序。
- 将两个已排序的子链表合并在一起。
三、具体代码
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// Step 1. 分割链表
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode mid = slow.next;
slow.next = null;
// Step 2. 递归排序
ListNode left = sortList(head);
ListNode right = sortList(mid);
// Step 3. 合并链表
return merge(left, right);
}
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode current = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
if (l1 != null) {
current.next = l1;
} else {
current.next = l2;
}
return dummy.next;
}
}
四、时间复杂度和空间复杂度
1. 时间复杂度
- 分割链表:使用快慢指针找到中点的时间复杂度是O(n),其中n是链表的长度。
- 递归排序:将链表分割成两半,每半的长度大约是n/2,因此递归的深度是O(log n),每次递归调用需要对两个子链表进行合并,合并的时间复杂度是O(n),所以递归排序的总时间复杂度是O(n log n)。
- 合并链表:合并两个有序链表的时间复杂度是O(n),因为每个节点最多被访问一次。
- 综上所述,整个算法的时间复杂度是O(n log n)。
2. 空间复杂度
- 递归栈空间:由于归并排序是递归进行的,所以需要递归栈空间。递归的最大深度是O(log n),每个递归调用需要常数空间,所以递归栈空间的总空间复杂度是O(log n)。
- 合并链表:合并链表时,除了输入的链表节点外,只需要一个哑节点和几个指针,不需要额外的空间,因此合并链表的空间复杂度是O(1)。
- 综上所述,整个算法的空间复杂度是O(log n),主要取决于递归栈的空间消耗。
综合以上分析,归并排序链表的算法时间复杂度是O(n log n),空间复杂度是O(log n)。
五、总结知识点
1. 链表的基本操作:
- 节点构成:链表由节点组成,每个节点包含数据和指向下一个节点的引用。
- 创建与遍历:链表的创建通过连接节点实现,遍历则是通过依次访问节点的引用。
- 分割与合并:链表的分割通过调整节点的引用实现,合并则是将两个链表的节点按特定顺序连接。
2. 递归:
- 函数自调用:递归允许函数调用自身,用于解决可以分解为更小相似问题的大问题。
- 分治策略:在归并排序中,递归用于将链表分成更小的部分,分别排序后再合并。
3. 快慢指针:
- 中点查找:快慢指针技术用于找到链表的中点,快指针每次移动两步,慢指针移动一步。
- 链表分割:当快指针到达链表末尾时,慢指针所指即为链表的中点,从而实现链表的分割。
4. 归并排序:
- 分治算法:归并排序是一种分治算法,将数据分为两半,分别排序后再合并。
- 时间复杂度:归并排序的时间复杂度为O(n log n),适用于链表排序。
5. 合并有序链表:
- 比较与连接:合并两个有序链表时,比较节点值,将较小的节点连接到结果链表中。
- 链表拼接:当一个链表为空时,将另一个链表的剩余部分接到结果链表的末尾。
6. 哑节点:
- 辅助节点:哑节点作为辅助节点,用于简化链表操作的边界条件处理。
- 处理头节点:在合并链表时,使用哑节点作为结果链表的起始节点,避免处理头节点的特殊情况。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。