算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
通俗易懂讲解“对链表进行插入排序”算法题目
一、题目是啥?一句话说清
给定一个链表,使用插入排序算法对链表进行排序,要求每次取出一个节点,插入到已排序部分的正确位置。
示例:
- 输入:4 → 2 → 1 → 3
- 输出:1 → 2 → 3 → 4
二、解题核心
使用一个虚拟头节点来简化操作,维护一个已排序的链表部分,然后逐个取出未排序的节点,在已排序部分中找到合适的插入位置并插入。
这就像我们打扑克牌时,一张一张地拿牌,然后把每张牌插入到手中已排序牌的正确位置。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 虚拟头节点的使用
- 是什么:创建一个虚拟头节点,其next指向原链表的头节点,这样可以简化在头部插入的操作。
- 为什么重要:因为插入位置可能在链表头部,使用虚拟头节点可以避免处理特殊的头部插入情况,使代码更统一和简洁。
2. 已排序和未排序部分的分离
- 是什么:将链表分为已排序部分和未排序部分。初始时,已排序部分只包含虚拟头节点,未排序部分是整个链表。然后逐个处理未排序节点。
- 为什么重要:这样我们可以清晰地管理已排序和未排序的节点,确保每次只处理一个节点,并正确插入到已排序部分。
3. 插入位置的查找
- 是什么:对于每个未排序的节点,我们需要在已排序部分中从头开始查找,直到找到第一个大于当前节点值的节点,然后插入到该节点之前。
- 为什么重要:查找插入位置是插入排序的核心,需要正确遍历已排序部分,比较节点值,并处理指针操作,确保链表不断裂。
四、看图理解流程(通俗理解版本)
假设链表为:4 → 2 → 1 → 3
-
初始化:
- 创建虚拟头节点dummy,dummy.next指向4。
- 已排序部分:dummy → 4(但4还未排序,所以实际上初始时已排序部分为空,未排序部分为整个链表)。
- 设置lastSorted指向4(已排序部分的最后一个节点),curr指向2(当前待处理节点)。
-
处理节点2:
- 比较lastSorted的值(4)和curr的值(2),因为4 > 2,需要插入。
- 从dummy开始遍历,找到插入位置(dummy.next是4,大于2,所以插入到dummy和4之间)。
- 插入后:dummy → 2 → 4 → 1 → 3
- lastSorted仍指向4,curr指向1(lastSorted.next)。
-
处理节点1:
- 比较lastSorted的值(4)和curr的值(1),因为4 > 1,需要插入。
- 从dummy开始遍历,找到插入位置(dummy.next是2,大于1,所以插入到dummy和2之间)。
- 插入后:dummy → 1 → 2 → 4 → 3
- lastSorted仍指向4,curr指向3(lastSorted.next)。
-
处理节点3:
- 比较lastSorted的值(4)和curr的值(3),因为4 > 3,需要插入。
- 从dummy开始遍历,找到插入位置(遍历到2时,2 < 3,继续;到4时,4 > 3,所以插入到2和4之间)。
- 插入后:dummy → 1 → 2 → 3 → 4
- lastSorted指向4,curr为null,排序结束。
-
返回结果:返回dummy.next,即1 → 2 → 3 → 4。
五、C++ 代码实现(附详细注释)
#include <iostream>
using namespace std;
// 链表节点定义
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
ListNode* insertionSortList(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head; // 空链表或单节点链表直接返回
}
// 创建虚拟头节点,简化操作
ListNode* dummy = new ListNode(0);
dummy->next = head;
// lastSorted 指向已排序部分的最后一个节点,初始为head
ListNode* lastSorted = head;
// curr 指向当前待排序的节点,初始为head的下一个节点
ListNode* curr = head->next;
while (curr != nullptr) {
if (lastSorted->val <= curr->val) {
// 如果当前节点值大于等于已排序部分的最后一个节点,直接扩展已排序部分
lastSorted = lastSorted->next;
} else {
// 否则,需要找到插入位置
ListNode* prev = dummy; // 从虚拟头节点开始遍历
// 找到第一个大于curr值的节点的前一个节点
while (prev->next->val <= curr->val) {
prev = prev->next;
}
// 将curr插入到prev和prev->next之间
lastSorted->next = curr->next; // 将curr从原位置移除
curr->next = prev->next; // curr指向prev的下一个节点
prev->next = curr; // prev指向curr
}
// 移动curr到下一个待排序节点
curr = lastSorted->next;
}
return dummy->next;
}
};
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head != nullptr) {
cout << head->val << " ";
head = head->next;
}
cout << endl;
}
// 测试代码
int main() {
// 创建示例链表:4->2->1->3
ListNode* head = new ListNode(4);
head->next = new ListNode(2);
head->next->next = new ListNode(1);
head->next->next->next = new ListNode(3);
Solution solution;
ListNode* result = solution.insertionSortList(head);
printList(result); // 输出:1 2 3 4
return 0;
}
六、时间空间复杂度
- 时间复杂度:O(n²),其中n是链表长度。最坏情况下,每个节点都需要遍历整个已排序部分来找到插入位置,因此时间复杂度为O(n²)。
- 空间复杂度:O(1),只使用了常数额外空间(几个指针),不包括虚拟头节点(但虚拟头节点也是常数空间)。
七、注意事项
- 边界情况处理:如果链表为空或只有一个节点,直接返回,不需要排序。
- 虚拟头节点:使用虚拟头节点可以简化在头部插入的操作,避免特殊处理。
- 指针操作顺序:在插入节点时,需要先保存下一个节点,再调整指针,避免链表断裂。
- 性能考虑:插入排序对于链表来说效率较低,但对于小规模数据或部分有序数据可能有效。如果链表较长,可能需要更高效的排序算法(如归并排序)。
- 内存管理:在C++中,如果使用了动态分配的虚拟头节点,需要在适当的时候释放内存,但这里为了简化,没有在代码中释放(实际应用中需要注意)。
算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解