《大话数据结构》第9章精读:归并排序与快速排序完整 C++ 实现

1. 归并排序(Merge Sort)

核心思想(分治):

  1. 分解:把数组从中间分成左右两半。
  2. 解决:递归地对左右两半进行归并排序。
  3. 合并:把两个已排序的子数组合并成一个有序数组。

归并排序是稳定的,时间复杂度非常稳定。

完整实现

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

// 合并两个有序子数组 arr[left..mid] 和 arr[mid+1..right]
void merge(vector<int>& arr, int left, int mid, int right) {
    vector<int> temp(right - left + 1);
    int i = left;       // 左半部分起始
    int j = mid + 1;    // 右半部分起始
    int k = 0;

    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {   // 加等号保证稳定性
            temp[k++] = arr[i++];
        } else {
            temp[k++] = arr[j++];
        }
    }

    // 把剩余元素拷贝进去
    while (i <= mid)  temp[k++] = arr[i++];
    while (j <= right) temp[k++] = arr[j++];

    // 拷贝回原数组
    for (int p = 0; p < temp.size(); ++p) {
        arr[left + p] = temp[p];
    }
}

void mergeSort(vector<int>& arr, int left, int right) {
    if (left >= right) return;
    int mid = left + (right - left) / 2;   // 防止溢出
    mergeSort(arr, left, mid);
    mergeSort(arr, mid + 1, right);
    merge(arr, left, mid, right);
}

// 对外接口
void MergeSort(vector<int>& arr) {
    if (arr.size() <= 1) return;
    mergeSort(arr, 0, arr.size() - 1);
}

复杂度:

  • 时间复杂度:最好、平均、最坏都是 O(n log n)
  • 空间复杂度:O(n)(需要额外数组)
  • 稳定性:稳定

2. 快速排序(Quick Sort)

核心思想(分治 + 填坑/交换):

  1. 选取一个基准值(pivot)。
  2. 把比基准小的元素放到左边,比基准大的放到右边(分区)。
  3. 递归地对左右两部分进行快速排序。

快速排序平均性能最优,是实际使用最多的排序算法之一。

完整实现(挖坑法 + 随机基准优化)

cpp 复制代码
#include <cstdlib>   // rand
#include <ctime>

// 分区函数(挖坑法)
int partition(vector<int>& arr, int left, int right) {
    // 随机选择基准,降低最坏情况概率
    int randomIndex = left + rand() % (right - left + 1);
    swap(arr[randomIndex], arr[left]);

    int pivot = arr[left];   // 基准值
    int i = left, j = right;

    while (i < j) {
        // 从右向左找小于基准的
        while (i < j && arr[j] >= pivot) --j;
        if (i < j) arr[i] = arr[j];

        // 从左向右找大于基准的
        while (i < j && arr[i] <= pivot) ++i;
        if (i < j) arr[j] = arr[i];
    }

    arr[i] = pivot;          // 基准归位
    return i;
}

void quickSort(vector<int>& arr, int left, int right) {
    if (left >= right) return;
    int pivotPos = partition(arr, left, right);
    quickSort(arr, left, pivotPos - 1);
    quickSort(arr, pivotPos + 1, right);
}

// 对外接口
void QuickSort(vector<int>& arr) {
    if (arr.size() <= 1) return;
    srand(time(nullptr));    // 设置随机种子
    quickSort(arr, 0, arr.size() - 1);
}

复杂度:

  • 平均时间复杂度:O(n log n)
  • 最坏时间复杂度:O(n²)(已通过随机基准大幅降低概率)
  • 空间复杂度:O(log n)(递归栈)
  • 稳定性:不稳定

3. 归并 vs 快速 对比

对比维度 归并排序 快速排序
时间复杂度 稳定 O(n log n) 平均 O(n log n),最坏 O(n²)
空间复杂度 O(n) O(log n)
稳定性 稳定 不稳定
是否原地排序 基本原地
实际运行速度 较慢(常量较大) 通常最快
适用场景 需要稳定排序、链表排序 大多数通用场景
优化手段 小数组转插入排序 随机基准、三数取中、小数组优化

4. 完整测试代码

cpp 复制代码
void printArray(const vector<int>& arr) {
    for (int x : arr) cout << x << " ";
    cout << endl;
}

int main() {
    vector<int> arr1 = {49, 38, 65, 97, 76, 13, 27, 49, 55, 4};
    vector<int> arr2 = arr1;

    cout << "原数组:";
    printArray(arr1);

    MergeSort(arr1);
    cout << "归并排序后:";
    printArray(arr1);

    QuickSort(arr2);
    cout << "快速排序后:";
    printArray(arr2);

    return 0;
}

5. 总结与思考

归并排序:分治思想的经典体现,稳定且时间复杂度有保障,但需要额外空间。

快速排序:平均性能之王,通过基准分区实现高效排序,实际工程中使用最多。

结合《C++ Primer Plus》的思考:

  • 归并排序中的临时数组体现了"空间换时间"。
  • 快速排序的递归分区过程,与书中讲解的递归思想高度契合。
  • 随机基准的使用,是对"避免最坏输入"这一工程思维的体现。

到这里,第9章常用排序算法已经基本覆盖。下一篇可以补充基数排序或做一次整体排序算法总结对比。

6. 基数排序(Radix Sort)

核心思想(LSD 低位优先):

  1. 按位分配:从最低位(个位)开始,依次对每一位进行排序。
  2. 稳定收集:每一轮使用稳定的计数排序,保证相同位值元素的相对顺序不变。
  3. 逐位推进:从低位到高位逐轮处理,最终得到整体有序序列。

基数排序不基于比较,而是通过"分配 + 收集"的方式,在特定数据范围内能达到线性时间复杂度。

完整实现(LSD + 计数排序子过程)

cpp 复制代码
#include <vector>
#include <algorithm>   // max_element
using namespace std;

// 计数排序子过程:按第 exp 位(10 的幂)对数组排序
void countingSortByDigit(vector<int>& arr, int exp) {
    int n = arr.size();
    vector<int> output(n);
    int count[10] = {0};

    // 统计每个数字(0-9)出现的次数
    for (int i = 0; i < n; ++i) {
        int digit = (arr[i] / exp) % 10;
        ++count[digit];
    }

    // 累加计数,得到每个数字在输出数组中的位置
    for (int i = 1; i < 10; ++i) {
        count[i] += count[i - 1];
    }

    // 从后向前遍历,保证稳定性
    for (int i = n - 1; i >= 0; --i) {
        int digit = (arr[i] / exp) % 10;
        output[count[digit] - 1] = arr[i];
        --count[digit];
    }

    // 拷贝回原数组
    for (int i = 0; i < n; ++i) {
        arr[i] = output[i];
    }
}

// 基数排序(LSD 低位优先)
void RadixSort(vector<int>& arr) {
    if (arr.size() <= 1) return;

    // 找到最大值,确定最大位数
    int maxVal = *max_element(arr.begin(), arr.end());

    // 从个位开始,逐位排序
    for (int exp = 1; maxVal / exp > 0; exp *= 10) {
        countingSortByDigit(arr, exp);
    }
}

复杂度:

  • 时间复杂度:O(d * (n + k)),其中 d 为最大位数,k 为基数(十进制下 k = 10)
  • 空间复杂度:O(n + k)(需要额外输出数组和计数数组)
  • 稳定性:稳定

与归并排序、快速排序的对比

对比维度 归并排序 快速排序 基数排序
排序方式 比较排序(分治) 比较排序(分治) 非比较排序(分配 + 收集)
时间复杂度 稳定 O(n log n) 平均 O(n log n),最坏 O(n²) O(d * (n + k))
空间复杂度 O(n) O(log n) O(n + k)
稳定性 稳定 不稳定 稳定
是否原地排序 基本原地
适用场景 需要稳定排序、链表排序 大多数通用场景 整数、固定长度字符串等可拆位数据
局限 额外空间开销较大 最坏情况退化 依赖数据范围,不适合浮点或任意长度数据

基数排序在数据位数较少、范围可控时表现优异,但通用性不如归并排序和快速排序。三者各有适用场景,实际工程中应根据数据特征和稳定性要求综合选择。

相关推荐
2601_956121974 小时前
背包基础篇(01、完全、分组、多重、混合)
c++·算法·动态规划
fpcc4 小时前
跟我学C++中级篇——编译期的条件选择
开发语言·c++
兴通物联科技5 小时前
SMT PCB 微小 DataMatrix 码扫不动问题分析 兴通 XT8601B 600 万像素工业读码器落地实践
大数据·人工智能·单片机·嵌入式硬件·算法·计算机视觉
月华路7 小时前
G1 GC 对数组与大对象(Humongous)的处理
java·jvm·算法
我想走路带风7 小时前
LRU和最长前缀和(计算机网络算法)
计算机网络·算法
M78佐菲8 小时前
Linux学习笔记:进程
linux·笔记·学习·算法
ShineWinsu8 小时前
对于C++:C++20中线程、初始化、Lambda与内存视图等特性的解析
c++·c++20
徐小夕9 小时前
表格、文档、甘特、大屏、表单一站打通:pxcharts超级表格4.0正式上线!
前端·算法·github