为什么链表排序要使用归并排序?

0 阅读7分钟

为什么链表排序要使用归并排序?

核心前提:算法适配性 = 算法逻辑 × 数据结构特性

需先明确链表数组的核心差异,为后续代码适配分析铺垫。

链表数组的底层差异,决定排序算法选择,核心差异如下表,后续代码实现均围绕这些差异展开。

特性链表数组
访问方式不支持随机访问,访问第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++指针操作则是体现这一逻辑的关键载体,也是面试中考察的核心重点。