链表实现O(n log n)时间复杂度的排序——归并排序详解

个人主页:

<zephyr05>

个人专栏:

<Linux> <C/C++> <竞赛专栏> <数据结构>

座右铭:

等风来不如追风去,追寻的过程就是人生的意义。


目录

为什么选择归并排序?

递归归并排序实现

核心思想

寻找中点并分割

合并两个有序链表

完整递归代码

迭代归并排序(自底向上)

思路

实现要点

复杂度分析

性能与稳定性

常见陷阱与优化

[1. 快慢指针初始化](#1. 快慢指针初始化)

[2. 内存泄漏](#2. 内存泄漏)

[3. 递归深度](#3. 递归深度)

[4. 优化合并](#4. 优化合并)

测试与验证

总结


在链表数据结构上实现高效排序是一个经典问题。不同于数组,链表不支持随机访问,这使得许多常见排序算法(如快速排序、堆排序)难以直接应用或效率低下。本文将深入探讨如何使用C++在单链表上实现O(n log n) 时间复杂度的排序,重点介绍归并排序(Merge Sort),并提供递归与迭代两种实现,分析其复杂度、优缺点及注意事项。


为什么选择归并排序?

对于数组,快速排序通常是首选,因为它原地排序且缓存友好。但对于单链表,情况截然不同:

  • 快速排序:需要频繁访问中间元素进行分区,而链表无法随机访问,分区操作需要遍历,导致时间复杂度退化至O(n²)。

  • 堆排序:需要堆数据结构,链表难以高效实现。

  • 归并排序

    • 天然适合链表,因为合并操作只需改变指针,无需额外内存(除了递归栈)。

    • 稳定排序(相等元素保持原有相对顺序)。

    • 时间稳定为O(n log n),不受数据分布影响。

因此,归并排序是链表排序的不二之选。


递归归并排序实现

核心思想

  1. :使用快慢指针找到链表中间节点,将链表一分为二。

  2. :递归对两个子链表进行排序。

  3. :合并两个已排序的子链表。

寻找中点并分割

快慢指针技巧:

  • slow每次走一步,fast每次走两步。

  • fast到达末尾时,slow恰好在中点前(或中点,取决于初始位置)。

  • 需要将左半部分末尾的next置为nullptr,切断链表。

cpp 复制代码
ListNode* findMiddleAndSplit(ListNode* head) {
    if (!head || !head->next) return nullptr;
    ListNode* slow = head;
    ListNode* fast = head->next;  // 让fast先走一步,使得偶数长度时slow停在左半末尾
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    ListNode* mid = slow->next;   // 右半部分头
    slow->next = nullptr;         // 切断
    return mid;
}

合并两个有序链表

使用**哑节点(dummy node)**简化头节点处理。

cpp 复制代码
ListNode* merge(ListNode* l1, ListNode* l2) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    while (l1 && l2) {
        if (l1->val <= l2->val) {
            tail->next = l1;
            l1 = l1->next;
        } else {
            tail->next = l2;
            l2 = l2->next;
        }
        tail = tail->next;
    }
    tail->next = l1 ? l1 : l2;
    return dummy.next;
}

完整递归代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

ListNode* merge(ListNode* l1, ListNode* l2) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    while (l1 && l2) {
        if (l1->val <= l2->val) {
            tail->next = l1;
            l1 = l1->next;
        } else {
            tail->next = l2;
            l2 = l2->next;
        }
        tail = tail->next;
    }
    tail->next = l1 ? l1 : l2;
    return dummy.next;
}

ListNode* findMiddleAndSplit(ListNode* head) {
    if (!head || !head->next) return nullptr;
    ListNode* slow = head;
    ListNode* fast = head->next;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    ListNode* mid = slow->next;
    slow->next = nullptr;
    return mid;
}

ListNode* mergeSort(ListNode* head) {
    if (!head || !head->next) return head;
    ListNode* right = findMiddleAndSplit(head);
    ListNode* leftSorted = mergeSort(head);
    ListNode* rightSorted = mergeSort(right);
    return merge(leftSorted, rightSorted);
}

// 辅助函数
ListNode* createList(const vector<int>& vals) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    for (int v : vals) {
        tail->next = new ListNode(v);
        tail = tail->next;
    }
    return dummy.next;
}

void printList(ListNode* head) {
    while (head) {
        cout << head->val << " ";
        head = head->next;
    }
    cout << endl;
}

int main() {
    vector<int> arr = {4, 2, 1, 3, 5, 9, 6};
    ListNode* head = createList(arr);
    cout << "排序前: ";
    printList(head);
    head = mergeSort(head);
    cout << "排序后: ";
    printList(head);
    return 0;
}

迭代归并排序(自底向上)

递归实现虽然简洁,但递归深度为O(log n),对于极大规模链表(如百万级)可能栈溢出。迭代版本使用自底向上策略,空间复杂度降为O(1)。

思路

  • 初始步长step = 1,将链表划分为长度为step的若干段,两两合并。

  • 每次合并后step *= 2,直到step >= 链表长度

  • 需要维护三个指针:head(最终头)、tail(当前已合并部分的尾)、cur(遍历原链表的指针)。

实现要点

  • 每次合并两个长度均为step(最后一个可能不足)的子链表。

  • 合并后连接到tail后面,并更新tail

  • 使用哑节点简化头指针管理。

cpp 复制代码
ListNode* mergeSortIterative(ListNode* head) {
    if (!head || !head->next) return head;

    // 计算链表长度
    int length = 0;
    ListNode* cur = head;
    while (cur) { ++length; cur = cur->next; }

    ListNode dummy(0);
    dummy.next = head;

    for (int step = 1; step < length; step <<= 1) {
        ListNode* prev = &dummy;
        ListNode* curNode = dummy.next;

        while (curNode) {
            // 找到左子链表头(curNode)
            ListNode* left = curNode;
            // 找到左子链表尾,并计算左子链表长度(最多step)
            int leftLen = 0;
            for (; leftLen < step && curNode; ++leftLen) {
                curNode = curNode->next;
            }
            // 如果左子链表长度不足step,说明没有右子链表,直接结束
            if (leftLen < step) break;

            // 右子链表头(curNode当前指向)
            ListNode* right = curNode;
            // 计算右子链表实际长度(可能小于step)
            int rightLen = 0;
            for (; rightLen < step && curNode; ++rightLen) {
                curNode = curNode->next;
            }

            // 保存下一个待处理的起始节点
            ListNode* nextStart = curNode;

            // 合并left和right,长度为leftLen+rightLen
            ListNode* merged = mergeTwoLists(left, right, leftLen, rightLen);
            // 将合并后的链表接到prev后面
            prev->next = merged;
            // 移动prev到合并后的尾部
            while (prev->next) prev = prev->next;

            // 继续处理剩余部分
            curNode = nextStart;
        }
    }
    return dummy.next;
}

// 辅助合并:已知left和right的长度,方便控制
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2, int len1, int len2) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    while (len1 > 0 && len2 > 0) {
        if (l1->val <= l2->val) {
            tail->next = l1;
            l1 = l1->next;
            --len1;
        } else {
            tail->next = l2;
            l2 = l2->next;
            --len2;
        }
        tail = tail->next;
    }
    while (len1 > 0) { tail->next = l1; l1 = l1->next; --len1; tail = tail->next; }
    while (len2 > 0) { tail->next = l2; l2 = l2->next; --len2; tail = tail->next; }
    tail->next = nullptr; // 防止残留
    return dummy.next;
}

注意 :迭代版本实现较复杂,需仔细处理指针,尤其是prev的更新。使用长度计数器而非直接判断nullptr可以避免连接混乱。


复杂度分析

版本 时间复杂度 空间复杂度
递归归并 O(n log n) O(log n) 递归栈
迭代归并 O(n log n) O(1)
  • 时间:每层合并遍历所有节点,共log n层,总O(n log n)。

  • 空间:递归版本递归深度log n,迭代版本常数级。


性能与稳定性

  • 稳定性:归并排序是稳定的,因为合并时当值相等时优先取左子链表元素,保持原顺序。

  • 适用性:对于大规模链表,迭代版本避免栈溢出风险,且效率较高(无函数调用开销)。但迭代版本代码较难维护,可根据实际场景选择。


常见陷阱与优化

1. 快慢指针初始化

fast必须从head->next开始,否则对于偶数长度链表,slow会停在中点偏右,导致分割不均甚至死循环。

2. 内存泄漏

示例代码未释放new的节点,实际项目中应注意使用智能指针或手动delete

3. 递归深度

若链表长度为10^6,递归深度约20,安全。但若链表极其庞大(如10^8),递归深度约27,仍可接受,但可改用迭代。


测试与验证

使用随机数组测试递归与迭代版本,结果应一致。示例测试:

cpp 复制代码
int main() {
    vector<int> arr = {5, 3, 8, 1, 9, 2, 7, 4, 6};
    ListNode* head = createList(arr);
    head = mergeSort(head);      // 或 mergeSortIterative
    printList(head);             // 输出 1 2 3 4 5 6 7 8 9
    return 0;
}

总结

  • 链表排序的最优选择是归并排序,时间复杂度O(n log n),递归实现简洁易懂,迭代实现空间最优。

  • 关键技巧:快慢指针分割、哑节点合并、注意链表切断。

  • 实际开发中,若链表长度较小(如<100),可考虑插入排序(O(n²))因其常数小,但本题要求O(n log n),归并是最佳。

  • 掌握这两种实现,足以应对绝大多数链表排序需求。


如果本文对你有帮助,欢迎点赞、收藏、分享。如有疑问,请在评论区留言,我会尽力解答。

博客首发于 CSDN,转载请注明出处。

相关推荐
Zane19941 小时前
JRE 去哪了?一次讲清楚 JDK、JRE、JVM 到底是什么关系?
java·后端
喜欢睡觉1 小时前
双指针判断子序列
算法
PPPPickup1 小时前
基础知识差缺补漏-面试版持续更新
java·开发语言
深入云栈1 小时前
Nacos 2.x 源码深度解析 (二):通信协议迭代 —— HTTP长轮询到gRPC演进
java
贵慜_Derek1 小时前
vLLM-07|MegaMoE 与 FusedMoE:路由相同,算 expert 完全不同
人工智能·算法·llm
机器学习之心1 小时前
基于改进鲸鱼优化算法的CNN-BiLSTM-MATT短期电力负荷预测模型
人工智能·算法·cnn·cnn-bilstm-matt·短期电力负荷预测
andongni2031 小时前
SpringBoot 入门实验报告
java·spring boot·后端
黄鸭部落格ShiYuYuanFang1 小时前
【分享】软考每日一练与解析-计组 8/12
java·开发语言
windliang1 小时前
Claude Code 源码分析(十):MCP 外部工具如何进入下一轮 Agent 调用
前端·算法·面试