
个人主页:
<zephyr05>
个人专栏:
座右铭:
等风来不如追风去,追寻的过程就是人生的意义。
目录
[1. 快慢指针初始化](#1. 快慢指针初始化)
[2. 内存泄漏](#2. 内存泄漏)
[3. 递归深度](#3. 递归深度)
[4. 优化合并](#4. 优化合并)
在链表数据结构上实现高效排序是一个经典问题。不同于数组,链表不支持随机访问,这使得许多常见排序算法(如快速排序、堆排序)难以直接应用或效率低下。本文将深入探讨如何使用C++在单链表上实现O(n log n) 时间复杂度的排序,重点介绍归并排序(Merge Sort),并提供递归与迭代两种实现,分析其复杂度、优缺点及注意事项。
为什么选择归并排序?
对于数组,快速排序通常是首选,因为它原地排序且缓存友好。但对于单链表,情况截然不同:
-
快速排序:需要频繁访问中间元素进行分区,而链表无法随机访问,分区操作需要遍历,导致时间复杂度退化至O(n²)。
-
堆排序:需要堆数据结构,链表难以高效实现。
-
归并排序:
-
天然适合链表,因为合并操作只需改变指针,无需额外内存(除了递归栈)。
-
稳定排序(相等元素保持原有相对顺序)。
-
时间稳定为O(n log n),不受数据分布影响。
-
因此,归并排序是链表排序的不二之选。
递归归并排序实现
核心思想
-
分:使用快慢指针找到链表中间节点,将链表一分为二。
-
治:递归对两个子链表进行排序。
-
合:合并两个已排序的子链表。
寻找中点并分割
快慢指针技巧:
-
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,转载请注明出处。