为什么链表排序要使用归并排序?
核心前提:算法适配性 = 算法逻辑 × 数据结构特性。
需先明确链表与数组的核心差异,为后续代码适配分析铺垫。
链表与数组的底层差异,决定排序算法选择,核心差异如下表,后续代码实现均围绕这些差异展开。
| 特性 | 链表 | 数组 |
|---|---|---|
| 访问方式 | 不支持随机访问,访问第k个元素需从头遍历(O(n)) | 支持随机访问,访问第k个元素O(1) |
| 插入/删除 | 已知前驱节点时,O(1)(仅调整指针,无需移动元素) | 需移动后续元素,O(n) |
| 内存分布 | 非连续,分散存储 | 连续存储,可高效利用缓存 |
上述差异导致快排(依赖随机访问)等数组高效算法不适配链表。
归并排序
归并排序核心逻辑:分治 + 合并。
1. 分治阶段:拆分成本极低
归并分治核心:将链表拆分为等长子链表,直至单个节点(天然有序)。
链表拆分无需额外空间,仅通过快慢指针调整指针指向。
#include <iostream>
using namespace std;
// C++ 单链表节点定义
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
// 快慢指针找链表中点(拆分核心代码)
ListNode* findMid(ListNode* head) {
// 快指针先移一步,确保拆分后左右子链表长度差≤1
ListNode* slow = head;
ListNode* fast = head->next;
// 快指针走2步、慢指针走1步,快指针到尾时,慢指针为中点
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// 拆分逻辑:切断中点,返回左右子链表头节点(pair<左, 右>)
pair<ListNode*, ListNode*> splitList(ListNode* head) {
ListNode* mid = findMid(head);
ListNode* rightHead = mid->next;
mid->next = nullptr; // 切断链表,拆分左右
return {head, rightHead};
}
- 拆分时间复杂度O(n)
- 空间复杂度O(1)
- 无需像数组归并那样复制元素(数组需O(n)辅助空间);
- 通过pair返回两个子链表头节点,指针操作简洁高效。
对比快排
#include <utility>
using namespace std;
// 快排链表(低效实现,仅示意弊端)
ListNode* quickSortLinkedList(ListNode* head) {
// 弊端1:需遍历定位基准(无随机访问,无法高效选中间基准)
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* pivot = head; // 只能选头节点当基准,易导致最坏情况(有序链表)
// 弊端2:partition需遍历调整指针,代码繁琐且低效
ListNode* leftDummy = new ListNode(0);
ListNode* rightDummy = new ListNode(0);
ListNode* left = leftDummy;
ListNode* right = rightDummy;
ListNode* cur = head->next;
while (cur != nullptr) {
if (cur->val < pivot->val) {
left->next = cur;
left = left->next;
} else {
right->next = cur;
right = right->next;
}
cur = cur->next;
}
left->next = nullptr;
right->next = nullptr;
// 递归排序,最坏时间复杂度O(n²),且存在内存泄漏风险(需手动释放节点)
ListNode* leftSorted = quickSortLinkedList(leftDummy->next);
ListNode* rightSorted = quickSortLinkedList(rightDummy->next);
// 拼接左、基准、右
ListNode* res = leftSorted;
if (res == nullptr) {
res = pivot;
} else {
ListNode* temp = leftSorted;
while (temp->next != nullptr) {
temp = temp->next;
}
temp->next = pivot;
}
pivot->next = rightSorted;
delete leftDummy;
delete rightDummy;
return res;
}
- 依赖随机访问选基准、基准定位低效。
- partition指针操作繁琐,效率低下,C++实现复杂度高。
- 最坏时间复杂度O(n²)
- C++中需手动管理内存,易造成泄漏,远不如归并排序稳定高效。
2. 合并阶段:高效适配链表
合并核心:将两个有序子链表拼接为一个有序链表,仅调整指针,无需移动元素。
#include <iostream>
using namespace std;
// 合并两个有序链表(核心代码,C++版)
ListNode* mergeTwoSortedList(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(0); // 虚拟头节点,简化拼接逻辑
ListNode* cur = dummy;
// 双指针遍历,调整指针指向完成拼接
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
cur->next = l1;
l1 = l1->next;
} else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
// 拼接剩余节点
cur->next = (l1 != nullptr) ? l1 : l2;
ListNode* res = dummy->next;
delete dummy; // 释放虚拟节点,避免内存泄漏
return res;
}
- 合并时间复杂度O(n)
- 空间复杂度O(1)(仅额外创建1个虚拟节点,用完释放)
- 完全发挥链表“插入无需移动元素”的优势,无额外空间开销;
- C++中手动管理虚拟节点内存,避免泄漏。
对比其他排序算法:
#include <iostream>
using namespace std;
// 1. 冒泡排序链表(O(n²))
ListNode* bubbleSortLinkedList(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* dummy = new ListNode(0);
dummy->next = head;
bool swapped;
// 外层循环控制轮次,内层循环遍历交换
do {
swapped = false;
ListNode* prev = dummy;
ListNode* cur = dummy->next;
while (cur != nullptr && cur->next != nullptr) {
if (cur->val > cur->next->val) {
// 链表交换需调整3个指针,繁琐低效
ListNode* temp = cur->next;
cur->next = temp->next;
temp->next = cur;
prev->next = temp;
swapped = true;
}
prev = cur;
cur = cur->next;
}
} while (swapped);
ListNode* res = dummy->next;
delete dummy;
return res;
}
// 2. 堆排序链表(无法高效实现,需额外转数组)
#include <vector>
#include <algorithm>
ListNode* heapSortLinkedList(ListNode* head) {
// 弊端:需先将链表转数组,失去链表本身优势,且增加内存开销
vector<int> arr;
ListNode* cur = head;
while (cur != nullptr) {
arr.push_back(cur->val);
ListNode* temp = cur;
cur = cur->next;
delete temp; // 释放原链表节点,避免泄漏
}
// 堆排序数组(C++ STL make_heap)
make_heap(arr.begin(), arr.end());
sort_heap(arr.begin(), arr.end());
// 数组转链表
ListNode* dummy = new ListNode(0);
cur = dummy;
for (int val : arr) {
cur->next = new ListNode(val);
cur = cur->next;
}
ListNode* res = dummy->next;
delete dummy;
return res;
}
- 冒泡排序O(n²)低效
- 堆排序需转数组、额外占用内存且破坏链表特性,均不如归并排序适配链表;
- C++中堆排序还需手动管理链表与数组的内存转换,复杂度更高。
3. 稳定性与复杂度(C++代码层面验证)
归并排序的稳定性与时间复杂度,可通过C++完整代码验证,核心特性如下:
- 稳定性:合并阶段按“值小于”拼接,相同值节点相对位置不变(代码验证:见mergeTwoSortedList,l1->val ≤ l2->val时优先选l1,保留原始顺序),C++指针操作不改变节点本身顺序;
- 时间复杂度:分治log n层,每层合并O(n),总复杂度O(n log n),无最坏退化(代码层面无嵌套循环,递归深度log n);C++迭代版可进一步优化空间复杂度至O(1)。
单链表归并排序的核心完整代码
#include <iostream>
#include <utility> // 用于pair
using namespace std;
// 单链表节点定义(C++)
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
// 1. 找中点(拆分基础)
ListNode* findMid(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head->next;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// 2. 拆分链表(返回左右子链表头节点)
pair<ListNode*, ListNode*> splitList(ListNode* head) {
ListNode* mid = findMid(head);
ListNode* rightHead = mid->next;
mid->next = nullptr;
return {head, rightHead};
}
// 3. 合并两个有序链表
ListNode* mergeTwoSortedList(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
cur->next = l1;
l1 = l1->next;
} else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
cur->next = (l1 != nullptr) ? l1 : l2;
ListNode* res = dummy->next;
delete dummy; // 释放虚拟节点
return res;
}
// 4. 归并排序主函数(递归分治+合并)
ListNode* sortList(ListNode* head) {
// 递归终止:空链表或单个节点(天然有序)
if (head == nullptr || head->next == nullptr) {
return head;
}
// 分:拆分左右子链表
auto [left, right] = splitList(head); // C++17结构化绑定,简化赋值
// 治:递归排序左右子链表
ListNode* leftSorted = sortList(left);
ListNode* rightSorted = sortList(right);
// 合:合并有序子链表
return mergeTwoSortedList(leftSorted, rightSorted);
}
// 辅助函数:打印链表(用于测试)
void printList(ListNode* head) {
ListNode* cur = head;
while (cur != nullptr) {
cout << cur->val;
if (cur->next != nullptr) {
cout << "->";
}
cur = cur->next;
}
cout << endl;
}
// 辅助函数:释放链表内存(C++必写,避免内存泄漏)
void freeList(ListNode* head) {
ListNode* cur = head;
while (cur != nullptr) {
ListNode* temp = cur;
cur = cur->next;
delete temp;
}
}
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);
cout << "排序前:";
printList(head);
ListNode* sortedHead = sortList(head);
cout << "排序后:";
printList(sortedHead);
// 释放内存
freeList(sortedHead);
return 0;
}
为什么归并排序是链表的最优解?
归并排序因无需随机访问、仅通过指针操作完成分治与合并,时间复杂度稳定O(n log n),且规避了快排、冒泡等算法在链表上的低效与高复杂度问题,是链表排序的最优解。
实际开发中,C++ STL中无现成链表排序接口,主流实现均基于归并排序。
掌握链表归并排序的C++实现,核心是理解“数据结构决定算法选择”,而C++指针操作则是体现这一逻辑的关键载体,也是面试中考察的核心重点。