10、链表的排序:冒泡排序在链表上的实现、插入排序在链表上的实现

排序,是数据结构里绕不开的话题。数组排序我们都很熟悉了,但链表排序呢?

说实话,我刚入行那会儿,也觉得链表排序不就是把数组那套搬过来吗?结果一上手就发现——没那么简单。链表没有随机访问能力,你不能直接 list[i] 去取值。这就逼着我们去思考:指针操作到底该怎么玩?

今天我们就来聊聊两种最基础的排序算法在链表上的实现:冒泡排序插入排序

核心观点:链表排序的本质不是交换数据,而是调整指针。谁把指针玩明白了,谁就掌握了链表的精髓。

10.1 冒泡排序在链表上的实现

冒泡排序的思路很简单:从头到尾,两两比较,把大的往后“冒”。

在数组里,我们靠下标来遍历。在链表里,我们靠指针来移动。我习惯用两个指针:一个指向当前节点,一个指向它的下一个节点。

10.1.1 基本思路

  • 外层循环控制“趟数”,一共需要 n-1
  • 内层循环负责两两比较,把大的节点往后移
  • 每一趟结束后,最大的节点就“沉”到了链表尾部

你可能会问:链表怎么知道长度?嗯,要么提前遍历一遍算出来,要么用 while 循环判断是否到达尾部。我个人更倾向于后者——少一次遍历,少一点麻烦。

10.1.2 代码实现

// 链表节点定义
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 冒泡排序(只交换数据域)
void bubbleSort(Node* head) {
    if (head == NULL || head->next == NULL) return;

    int swapped;
    Node* ptr;
    Node* last = NULL;  // 指向已排序的尾部

    do {
        swapped = 0;
        ptr = head;

        while (ptr->next != last) {
            if (ptr->data > ptr->next->data) {
                // 交换数据
                int temp = ptr->data;
                ptr->data = ptr->next->data;
                ptr->next->data = temp;
                swapped = 1;
            }
            ptr = ptr->next;
        }
        last = ptr;  // 更新已排序尾部
    } while (swapped);
}

我的经验:这里我选择只交换数据域,而不是调整指针。为什么?因为简单、直观、不容易出错。如果你追求极致性能,可以尝试交换节点指针——但相信我,调试起来会让你怀疑人生。

10.1.3 冒泡排序的复杂度

指标
最好情况 O(n) — 已经有序,只需一趟
最坏情况 O(n²) — 完全逆序
平均情况 O(n²)
空间复杂度 O(1) — 原地排序

说白了,冒泡排序在链表上并不高效。但我为什么还要讲?因为它简单、好理解,适合用来理解“指针遍历”的基本操作。

10.2 插入排序在链表上的实现

插入排序的思路是:把链表分成两部分——已排序区和未排序区。每次从未排序区取一个节点,插入到已排序区的正确位置。

这个思路在链表上其实比数组更自然。为什么?因为链表插入不需要移动元素,只需要改几个指针就行。

10.2.1 基本思路

  • 维护一个“已排序链表”的头指针,初始为 NULL
  • 遍历原链表,每次取出一个节点
  • 在已排序链表中找到合适的位置,插入进去

我曾经在项目中用这个思路处理一个实时数据流——数据不断进来,需要保持有序。插入排序在链表上简直就是为这种场景量身定做的。

10.2.2 代码实现

// 插入排序(调整指针)
Node* insertionSort(Node* head) {
    if (head == NULL || head->next == NULL) return head;

    Node* sorted = NULL;  // 已排序链表头
    Node* curr = head;

    while (curr != NULL) {
        Node* next = curr->next;  // 保存下一个节点

        // 在 sorted 中找到插入位置
        if (sorted == NULL || sorted->data >= curr->data) {
            // 插入到头部
            curr->next = sorted;
            sorted = curr;
        } else {
            // 在 sorted 中查找插入位置
            Node* search = sorted;
            while (search->next != NULL && search->next->data < curr->data) {
                search = search->next;
            }
            curr->next = search->next;
            search->next = curr;
        }

        curr = next;  // 继续处理下一个节点
    }

    return sorted;
}

避坑指南:我曾经在写这段代码时,忘记保存 curr->next,结果插入操作把 curr 的 next 改了,导致原链表断掉,后面的节点全丢了。记住:先保存,再操作

10.2.3 插入排序的复杂度

指标
最好情况 O(n) — 已经有序,每次插入到尾部
最坏情况 O(n²) — 逆序,每次插入到头部
平均情况 O(n²)
空间复杂度 O(1) — 原地排序

你想想看,插入排序在链表上的优势是什么?它不需要像数组那样频繁移动数据,只需要改指针。所以实际运行效率往往比冒泡排序好不少。

10.3 两种排序的对比

对比项 冒泡排序 插入排序
实现难度 简单 中等
指针操作 少(只交换数据) 多(调整指针)
稳定性 稳定 稳定
适用场景 教学、小规模数据 数据流、部分有序数据
实际效率 较低 较高

我的建议:如果你只是写个作业或练手,冒泡排序就够了。但如果你在写生产代码,尤其是数据量不确定时,插入排序是更靠谱的选择。

10.4 知识体系图

下面这张图帮你理清链表排序的核心逻辑:

链表排序核心逻辑 链表排序 冒泡排序 插入排序 交换数据域 两两比较 调整指针 分已排序/未排序 核心:指针操作 > 数据交换

嗯,这张图把两种排序的核心区别画得很清楚。冒泡排序重在“比较和交换”,插入排序重在“拆分和插入”。

10.5 避坑总结

最后,我把自己踩过的坑总结一下,希望能帮你少走弯路:

  • 忘记保存 next 指针:插入排序时,一定要先保存 curr->next,否则链表会断
  • 死循环:冒泡排序的 last 指针更新不对,会导致内层循环永远跑不完
  • 空链表和单节点:这两种情况一定要提前处理,否则代码会崩溃
  • 稳定性问题:如果你交换的是节点指针而不是数据,稳定性可能会被破坏

一个小技巧:调试链表排序时,我习惯写一个 printList() 函数,每趟排序后都打印一次。这样能直观看到数据是怎么“冒泡”或“插入”的。

好了,链表排序就讲到这里。这两种排序虽然简单,但它们是理解更高级排序算法(比如归并排序、快速排序)的基础。把指针操作练熟了,后面学什么都快。


公众号:蓝海资料掘金营,微信deep3321