【算法--链表】147.对链表进行插入排序--通俗讲解

81 阅读7分钟

算法通俗讲解推荐阅读
【算法--链表】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

  1. 初始化

    • 创建虚拟头节点dummy,dummy.next指向4。
    • 已排序部分:dummy → 4(但4还未排序,所以实际上初始时已排序部分为空,未排序部分为整个链表)。
    • 设置lastSorted指向4(已排序部分的最后一个节点),curr指向2(当前待处理节点)。
  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)。
  3. 处理节点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)。
  4. 处理节点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,排序结束。
  5. 返回结果:返回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.重排链表--通俗讲解