一、题目描述
给定单个链表的头 head
,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。
插入排序 算法的步骤:
- 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
- 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
- 重复直到所有输入数据插入完为止。
下面是插入排序算法的一个图形示例。部分排序的列表(黑色)最初只包含列表中的第一个元素。每次迭代时,从输入数据中删除一个元素(红色),并就地插入已排序的列表中。
对链表进行插入排序。
示例 1:
输入: head = [4,2,1,3] 输出: [1,2,3,4]
示例 2:
输入: head = [-1,5,3,4,0] 输出: [-1,0,3,4,5]
提示:
- 列表中的节点数在
[1, 5000]
范围内 -5000 <= Node.val <= 5000
二、解题思路
插入排序的基本思想是,维护一个有序的序列,将未排序的元素插入到有序序列的正确位置。对于链表来说,插入排序的时间复杂度是O(n^2),但空间复杂度是O(1),因为它只需要常数级别的额外空间。
以下是针对链表进行插入排序的步骤:
- 创建一个虚拟头节点
dummy
,它的next
指针指向链表的头部head
。这样做是为了方便在链表头部插入节点。 - 初始化两个指针,
lastSorted
指向链表的最后一个已排序节点,current
指向待排序的节点,初始时lastSorted
为dummy
,current
为head
。 - 遍历链表,对于每个待排序的节点
current
: a. 从链表头部开始遍历已排序的节点,找到current
应该插入的位置。 b. 将current
从原位置摘除。 c. 将current
插入到正确的位置。 d. 更新lastSorted
为current
。 - 继续步骤3,直到
current
为null
,此时链表已经排序完成。
三、具体代码
class Solution {public ListNode insertionSortList(ListNode head) {if (head == null) return null;ListNode dummy = new ListNode(0); // 创建一个虚拟头节点ListNode lastSorted = head; // 最后一个已排序的节点ListNode current = head.next; // 当前待排序的节点// 存储链表剩余部分dummy.next = head;while (current != null) {if (lastSorted.val <= current.val) {// 如果当前节点大于等于最后一个已排序的节点,直接后移lastSorted = lastSorted.next;} else {// 否则,从链表头开始找插入位置ListNode prev = dummy;while (prev.next.val <= current.val) {prev = prev.next;}// 将current节点从原位置摘除lastSorted.next = current.next;// 将current节点插入到正确的位置current.next = prev.next;prev.next = current;}// 移动current到下一个待排序的节点current = lastSorted.next;}return dummy.next; // 返回虚拟头节点的下一个节点,即排序后的链表头}
}
四、时间复杂度和空间复杂度
1. 时间复杂度
- 遍历整个链表需要O(n)的时间,其中n是链表的长度。
- 对于链表中的每个节点,最坏的情况下可能需要遍历已经排序的部分来找到插入的位置,这也需要O(n)的时间。
- 因此,对于每个节点,最坏的情况是O(n)的时间复杂度,总的时间复杂度是O(n^2),因为需要遍历每个节点。
在最好的情况下,即链表已经是有序的,每个节点只需要O(1)的时间来检查是否需要移动,总的时间复杂度是O(n)。
2. 空间复杂度
- 该算法只使用了几个额外的指针(dummy, lastSorted, current, prev),不管链表有多大,这些额外的空间都是常数级别的。
- 因此,空间复杂度是O(1),即常数空间复杂度。
综上所述,该代码的时间复杂度在最坏情况下是O(n^2),在最好情况下是O(n),空间复杂度是O(1)。
五、总结知识点
1. 链表(Linked List):
- 链表是一种常见的数据结构,它由一系列节点组成,每个节点包含数据和一个或多个指向其他节点的引用(链接)。
- 链表的特点是动态地分配内存,不需要连续的内存空间,可以有效地插入和删除节点。
2. 虚拟头节点(Dummy Node):
- 虚拟头节点是在链表实际头部之前添加的一个额外节点,它的值通常不重要。
- 使用虚拟头节点可以简化链表操作,尤其是在插入和删除操作中,可以避免处理头部节点的特殊情况。
3. 插入排序(Insertion Sort):
- 插入排序是一种简单的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
- 插入排序的时间复杂度通常是O(n^2),但在部分有序的数据中表现较好。
4. 循环(Loop):
- 代码中使用循环来遍历链表,这是处理链表问题时的常见做法。
- 通过循环,可以逐个访问链表中的每个节点,并进行相应的操作。
5. 指针操作(Pointer Manipulation):
- 在链表的操作中,频繁使用指针来改变节点之间的链接关系。
- 代码中涉及到的指针操作包括改变节点的
next
引用,以实现节点的插入和删除。
6. 条件语句(Conditional Statements):
- 代码中使用条件语句(
if-else
)来决定是否需要将当前节点插入到已排序部分的正确位置。
7. 变量赋值(Variable Assignment):
- 在链表操作中,经常需要更新节点的引用,这涉及到变量的赋值操作。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。