c++各种排序算法(含模板,时空复杂度,稳定性)

空间复杂度

排序方法平均情况最坏情况最好情况
基数排序O(1)O(1)O(1)
归并排序O(n)O(n)O(n)
堆排序O(1)O(1)O(1)
快速排序O(logn)O(n)O(logn)
简单选择排序O(1)O(1)O(1)
冒泡排序O(1)O(1)O(1)
希尔排序O(1)O(1)O(1)
直接插入排序O(1)O(1)O(1)

时间复杂度

排序方法平均情况最坏情况最好情况
基数排序O(d(n + rd))O(d(n + rd))O(d(n + rd))
归并排序O(nlogn)O(nlogn)O(nlogn)
堆排序O(nlogn)O(nlogn)O(nlogn)
快速排序O(nlogn)O(n²)O(nlogn)
简单选择排序O(n²)O(n²)O(n²)
冒泡排序O(n²)O(n²)O(n)
希尔排序O(nlogn)O(n²)O(nlogn)
直接插入排序O(n²)O(n²)O(n)

稳定性

排序方法稳定性
基数排序稳定
归并排序稳定
堆排序不稳定
快速排序不稳定
简单选择排序不稳定
冒泡排序稳定
希尔排序不稳定
直接插入排序稳定

排序算法模板

基数排序

基数排序算法简介

  • 基本原理‌:基数排序属于非比较型排序算法,通过将整数按位数切割成不同数字,逐位进行稳定排序(通常使用计数排序或桶排序作为子程序)。
  • 核心特点‌:
    • 稳定性‌:相同元素的相对顺序在排序后保持不变。
    • 时间复杂度‌:平均为O(d·n),其中d为数字的最大位数,n为元素数量。
    • 空间复杂度‌:O(n + k)k为基数范围(十进制为10)。

算法核心步骤

  1. 确定最大位数‌:遍历数组找到最大值,计算其位数d
  2. 逐位排序‌(从低位到高位):
    • 分配‌:根据当前位的值(0-9),将元素分配到对应的桶中。
    • 收集‌:按桶顺序将元素合并回原数组。
  3. 重复步骤2‌,直到处理完所有位数。
#include <vector>
#include <algorithm>

// 获取数字num的指定位(exp=10^(k-1),k从1开始)
template <typename T>
int getDigit(T num, int exp) {
    return (num / exp) % 10;
}

// 基数排序主函数
template <typename T>
void radixSort(std::vector<T>& arr) {
    if (arr.empty()) return;

    T max_val = *std::max_element(arr.begin(), arr.end());
    int max_digits = 0;
    for (T temp = max_val; temp > 0; temp /= 10)
        max_digits++;

    for (int exp = 1; max_val / exp > 0; exp *= 10) {
        std::vector<std::vector<T>> buckets(10);

        for (T num : arr)
            buckets[getDigit(num, exp)].push_back(num);

        int idx = 0;
        for (auto& bucket : buckets)
            for (T num : bucket)
                arr[idx++] = num;
    }
}

归并排序

归并排序算法简介

  • 基本原理‌:归并排序是‌分治法‌(Divide and Conquer)的典型应用,将数组递归地分成两半,分别排序后再合并。
  • 核心特点‌:
    • 稳定性‌:相同元素的相对顺序在排序后保持不变。
    • 时间复杂度‌:最坏和平均均为O(n log n),性能稳定。
    • 空间复杂度‌:O(n),需额外空间存储临时数组。

算法核心步骤

  1. 分解‌:将数组从中间分为左右两半。
  2. 递归排序‌:对左右子数组分别递归调用归并排序。
  3. 合并‌:将两个有序子数组合并为一个有序数组。
#include <iostream>
#include <vector>

// 合并两个已排序的子数组
void merge(std::vector<int>& arr, int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;

    // 创建临时数组
    std::vector<int> L(n1), R(n2);

    // 拷贝数据到临时数组L和R
    for (int i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (int j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];

    // 合并临时数组回到原数组
    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    // 拷贝L中剩余元素
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }

    // 拷贝R中剩余元素
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

// 归并排序主函数
void mergeSort(std::vector<int>& arr, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;

        // 分治
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);

        // 合并
        merge(arr, left, mid, right);
    }
}

int main() {
    std::vector<int> arr = {12, 11, 13, 5, 6, 7};
    int n = arr.size();

    std::cout << "Original array: ";
    for (int i = 0; i < n; i++)
        std::cout << arr[i] << " ";
    std::cout << std::endl;

    mergeSort(arr, 0, n - 1);

    std::cout << "Sorted array: ";
    for (int i = 0; i < n; i++)
        std::cout << arr[i] << " ";
    std::cout << std::endl;

    return 0;
}
 

在这段代码中:
 
1.  merge  函数用于合并两个已经排序好的子数组。
 
- 首先创建两个临时数组 L 和 R ,分别复制原数组的左半部分和右半部分。
 
- 然后比较 L 和 R 中的元素,将较小的元素依次放入原数组中。
 
- 最后,如果 L 或 R 还有剩余元素,将它们全部放入原数组。
 
2.  mergeSort  函数是归并排序的主函数。
 
- 它采用分治策略,首先将数组分为两部分,然后递归地对这两部分进行排序,最后合并这两部分。
 
3. 在  main  函数中,创建了一个测试数组,调用 mergeSort 函数对数组进行排序,并输出排序前后的数组。

堆排序

一、堆排序介绍
 
堆排序(Heap Sort)是一种基于二叉堆数据结构的排序算法。它的平均时间复杂度、最坏时间复杂度均为 O(nlogn),空间复杂度为 O(1),并且是一种不稳定的排序算法。
 
1. 二叉堆(Binary Heap)
 
- 二叉堆是一种完全二叉树,分为最大堆和最小堆。
 
- 最大堆(Max Heap):每个节点的值都大于或等于其子节点的值,根节点的值是堆中的最大值。
 
- 最小堆(Min Heap):每个节点的值都小于或等于其子节点的值,根节点的值是堆中的最小值。
 
2. 堆排序原理
 
- 首先将待排序数组构建成一个最大堆(或最小堆)。
 
- 然后将堆顶元素(最大值或最小值)与堆的最后一个元素交换位置,此时最大(小)值就放到了它在排序结果中的最终位置。
 
- 接着对剩下的 n - 1个元素重新调整为最大堆(或最小堆),重复这个过程直到整个数组排序完成。
 
二、堆排序算法模板(以最大堆为例)

void heapify(vector<int>& arr, int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;

    // 如果左子节点大于根节点
    if (left < n && arr[left] > arr[largest])
        largest = left;

    // 如果右子节点大于当前最大节点
    if (right < n && arr[right] > arr[largest])
        largest = right;

    // 如果最大节点不是根节点,则交换并继续调整堆
    if (largest != i) {
        swap(arr[i], arr[largest]);
        heapify(arr, n, largest);
    }
}

void heapSort(vector<int>& arr) {
    int n = arr.size();

    // 构建最大堆
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);

    // 一个个取出元素并调整堆
    for (int i = n - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        heapify(arr, i, 0);
    }
}


 
1.  heapify 函数
 
- 这个函数用于维护最大堆的性质。
 
- 它从给定节点开始,比较该节点与其子节点的值,将最大的值交换到该节点上,并递归地对交换后的子节点进行 heapify 操作,以确保堆的性质得到维护。
 
2.  heapSort 函数
 
- 首先通过循环调用 heapify 函数构建最大堆,从最后一个非叶子节点开始(即 n/2 - 1 ),依次向前调整节点。
 
- 然后通过循环将堆顶元素(最大值)与当前堆的最后一个元素交换位置,并将堆的大小减1,再次调用 heapify 函数调整剩下的元素为最大堆,直到整个数组排序完成。

快速排序

一、快速排序介绍
 
快速排序(Quick Sort)是一种基于分治策略的排序算法。它的基本思想是通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,然后分别对这两部分记录继续进行排序,以达到整个序列有序。
 
快速排序的平均时间复杂度为 O(nlogn),在最坏情况下(例如数组已经有序时)时间复杂度为 O(n^{2}),空间复杂度为 O(logn)(取决于递归调用的栈空间),并且它是一种不稳定的排序算法。
 
二、快速排序算法模板(C++)
 

#include <iostream>
#include <vector>

// 划分函数
int partition(std::vector<int>& arr, int low, int high) {
    int pivot = arr[high];
    int i = (low - 1);

    for (int j = low; j <= high - 1; j++) {
        if (arr[j] <= pivot) {
            i++;
            std::swap(arr[i], arr[j]);
        }
    }
    std::swap(arr[i + 1], arr[high]);
    return (i + 1);
}

// 快速排序函数
void quickSort(std::vector<int>& arr, int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);

        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

int main() {
    std::vector<int> arr = {10, 7, 8, 9, 1, 5};
    int n = arr.size();

    quickSort(arr, 0, n - 1);

    std::cout << "Sorted array: ";
    for (int x : arr)
        std::cout << x << " ";
    std::cout << std::endl;

    return 0;
}
 


1.  partition 函数
 
- 这个函数的作用是选择一个主元(这里选择数组的最后一个元素作为主元),然后将数组分为两部分,左边部分的元素都小于等于主元,右边部分的元素都大于主元。
 
- 通过双指针法, i 指针指向小于等于主元的区域的最后一个元素, j 指针用于遍历数组。当 j 指针指向的元素小于等于主元时,将其与 i + 1 位置的元素交换,最后将主元放到正确的位置( i + 1 位置),并返回主元的位置。
 
2.  quickSort 函数
 
- 这是快速排序的主函数。如果 low 小于 high ,则先调用 partition 函数得到主元的位置 pi ,然后递归地对主元左边和右边的子数组进行快速排序。

简单选择排序

一、简单选择排序介绍
 
简单选择排序(Simple Selection Sort) 是一种直观的原地排序算法,属于选择排序的一种。其核心思想是:每次从待排序序列中选择最小(或最大)的元素,与待排序序列的起始位置交换,直到所有元素排序完成。
 
特点:
 
- 时间复杂度:平均和最坏情况均为 O(n^2),适用于小规模数据。
 
- 空间复杂度:O(1)(原地排序,仅需常数额外空间)。
 
- 稳定性:不稳定(例如序列  [5, 5, 3]  排序时,相同元素的相对顺序可能改变)。
 
二、算法模板(C++)
 

#include <vector>
using namespace std;

void selectionSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n - 1; i++) {
        // 寻找[i, n-1]中的最小值索引
        int minIndex = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        // 交换最小值到当前位置i
        swap(arr[i], arr[minIndex]);
    }
}

// 示例用法
int main() {
    vector<int> arr = {34, 12, 45, 6, 8, 23};
    selectionSort(arr);
    // 输出排序结果...
    return 0;
}


 
三、算法步骤解析
 
1. 外层循环:从第 0 个元素开始,遍历至倒数第 2 个元素(索引  n-2 )。
 
2. 内层循环:在当前外层循环索引  i  到数组末尾的范围内,寻找最小值的索引  minIndex 。
 
3. 交换元素:将找到的最小值与当前位置  i  的元素交换,使  i  位置成为已排序部分的末尾。
 
示例过程(升序排序):
 
- 初始数组: [34, 12, 45, 6, 8, 23] 
 
- 第 1 次循环(i=0):找到最小值  6 (索引 3),交换后数组: [6, 12, 45, 34, 8, 23] 
 
- 第 2 次循环(i=1):找到最小值  8 (索引 4),交换后数组: [6, 8, 45, 34, 12, 23] 
 
- 依此类推,直到所有元素有序。
 
四、优缺点
 
- 优点:实现简单,原地排序,无需额外空间。
 
- 缺点:时间复杂度高,不适用于大规模数据;稳定性差。

希尔排序

一、希尔排序介绍
 
希尔排序(Shell Sort) 是插入排序的改进版,通过将整个数组分成若干子序列(由步长 gap 决定)进行插入排序,逐步缩小步长至1,最终完成整体排序。其核心思想是让元素先“远距离”交换,使数组基本有序,再进行局部精细排序,减少插入排序的比较和移动次数。
 
特点:
 
- 时间复杂度:依赖步长序列,平均情况约为 O(n^{1.3}),优于普通插入排序的 O(n^2)。
 
- 空间复杂度:O(1)(原地排序)。
 
- 稳定性:不稳定(相同元素相对顺序可能改变)。
 
二、算法模板(C++)
 

#include <vector>
using namespace std;

void shellSort(vector<int>& arr) {
    int n = arr.size();
    // 初始步长设为数组长度的一半,逐步减半至0
    for (int gap = n / 2; gap > 0; gap /= 2) {
        // 对每个步长进行插入排序(从gap位置开始)
        for (int i = gap; i < n; i++) {
            int temp = arr[i]; // 保存当前元素
            int j;
            // 在子序列中向前比较并移动元素(类似插入排序)
            for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                arr[j] = arr[j - gap]; // 后移元素
            }
            arr[j] = temp; // 插入正确位置
        }
    }
}

// 示例用法
int main() {
    vector<int> arr = {13, 14, 94, 33, 82, 25, 59, 94, 65, 23};
    shellSort(arr);
    // 输出排序结果...
    return 0;
}



三、算法步骤解析
 
1. 确定步长序列:常见步长序列为 n/2, n/4, ..., 1(本例采用),也可使用其他优化序列(如Knuth序列)。
 
2. 分组插入排序:对于每个步长 gap ,将数组分为 gap 个子序列(如 arr[0], arr[gap], arr[2gap]... ),对每个子序列进行插入排序。
 
3. 逐步缩小步长:步长 gap 每次减半,直至 gap=1 时,数组接近有序,最后一次插入排序完成整体排序。
 
示例过程(步长序列:5→2→1,升序排序):
 
- 初始数组: [13, 14, 94, 33, 82, 25, 59, 94, 65, 23] 
 
- gap=5:子序列为  [13,25,65] 、 [14,59,23] 、 [94,94] 、 [33] 、 [82] ,排序后: [13,14,25,33,23,94,59,94,65,82] 
 
- gap=2:子序列如  [13,25,23,59,65] 、 [14,33,94,94,82] ,排序后: [13,14,23,33,25,65,59,82,94,94] 
 
- gap=1:完整插入排序,最终结果: [13,14,23,25,33,59,65,82,94,94] 
 
四、优缺点
 
- 优点:比插入排序更快,适用于中等规模数据,实现简单。
 
- 缺点:时间复杂度分析复杂,步长序列影响性能,不稳定。
 
希尔排序是一种高效的插入排序改进算法,适用于不需要稳定性且数据规模较大的场景。

冒泡排序

一、冒泡排序介绍
 
冒泡排序(Bubble Sort) 是一种基础交换排序算法,通过重复遍历数组,比较相邻元素并交换逆序对,使较大元素逐步“冒泡”到数组末尾。每轮遍历将当前未排序部分的最大值移至末尾,最终实现整体有序。
 
关键特性:
 
- 时间复杂度:
 
- 最坏/平均:O(n^2)(元素逆序时)。
 
- 最优:O(n)(元素有序,可通过标志位优化提前终止)。
 
- 空间复杂度:O(1)(原地排序,无需额外空间)。
 
- 稳定性:稳定(相同元素相对顺序不变)。
 
二、算法模板(C++)
 


#include <vector>
using namespace std;

void bubbleSort(vector<int>& arr) {
    int n = arr.size();
    bool swapped; // 优化标志:记录是否发生交换
    
    for (int i = 0; i < n - 1; ++i) {
        swapped = false;
        for (int j = 0; j < n - i - 1; ++j) {
            // 升序排序(降序改为 >)
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
                swapped = true; // 标记发生交换
            }
        }
        // 若某轮无交换,说明已有序,提前终止
        if (!swapped) break;
    }
}

// 示例用法
int main() {
    vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
    bubbleSort(arr);
    // 输出排序结果...
    return 0;
}


 
 
三、执行流程解析
 
1. 外层循环:控制排序轮数,最多执行 n-1 轮(n 为数组长度)。
 
2. 内层循环:从数组头部开始,逐对比较相邻元素,若逆序则交换,每轮将当前最大值“冒泡”到未排序部分的末尾(位置 n-i-1)。
 
3. 优化逻辑:若某一轮内层循环中未发生任何交换( swapped=false ),说明数组已完全有序,直接终止排序,避免无效遍历。
 
示例过程(升序排序):
 
- 初始数组: [64, 34, 25, 12, 22, 11, 90] 
 
- 第1轮:比较交换  64↔34 、 34↔25 、 25↔12 、 22↔11 ,最大值 90 已在末尾,结果: [34, 25, 12, 22, 11, 64, 90] 
 
- 第2轮:比较交换  34↔25 、 25↔12 、 22↔11 ,结果: [25, 12, 22, 11, 34, 64, 90] 
 
- 第3轮:比较交换  25↔12 、 22↔11 ,结果: [12, 22, 11, 25, 34, 64, 90] 
 
- 第4轮:比较交换  22↔11 ,结果: [12, 11, 22, 25, 34, 64, 90] 
 
- 第5轮:无交换,提前终止,最终有序数组: [11, 12, 22, 25, 34, 64, 90] 
 
四、优缺点与适用场景
 
- 优点:实现简单,稳定性好,原地排序。
 
- 缺点:时间复杂度高,效率低于进阶算法(如快速排序、归并排序)。
 
- 适用场景:小规模数据或教育场景,实际开发中较少使用。
 
通过标志位优化后,冒泡排序在最优情况下可达线性时间复杂度,是其唯一优势。

直接插入排序

一、直接插入排序介绍
 
直接插入排序(Straight Insertion Sort) 是一种简单直观的排序算法,属于插入排序的一种。其核心思想是将数组分为已排序和未排序两部分,每次从未排序部分取出第一个元素,插入到已排序部分的合适位置,使已排序部分始终保持有序。
 
特点:
 
- 时间复杂度:
 
- 最坏/平均情况:O(n^2)(元素逆序时)。
 
- 最好情况:O(n)(元素已有序,无需移动元素)。
 
- 空间复杂度:O(1)(原地排序,仅需常数额外空间)。
 
- 稳定性:稳定(相同元素相对顺序不变)。
 
二、算法模板(C++)

 
cpp
  
#include <vector>
using namespace std;

void insertionSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 1; i < n; i++) { // i从1开始(假设arr[0]已排序)
        int temp = arr[i]; // 保存当前待插入元素
        int j = i - 1; // 已排序部分的最后一个元素索引
        
        // 向前查找插入位置,同时后移元素
        while (j >= 0 && arr[j] > temp) {
            arr[j + 1] = arr[j]; // 后移元素
            j--;
        }
        arr[j + 1] = temp; // 插入到正确位置
    }
}

// 示例用法
int main() {
    vector<int> arr = {3, 1, 4, 2, 5};
    insertionSort(arr);
    // 输出排序结果...
    return 0;
}


 
 
三、算法步骤解析
 
1. 初始化:将数组的第一个元素(索引0)视为已排序部分,从第二个元素(索引1)开始遍历未排序部分。
 
2. 提取元素:取出当前未排序部分的第一个元素( arr[i] ),作为待插入元素。
 
3. 查找插入位置:在已排序部分( arr[0..i-1] )中从后向前比较,找到第一个小于等于待插入元素的位置 j 。
 
4. 移动元素:将已排序部分中大于待插入元素的元素依次后移一位。
 
5. 插入元素:将待插入元素放入正确位置( j+1 )。
 
示例过程(升序排序):
 
- 初始数组: [3, 1, 4, 2, 5] 
 
- i=1(元素1):与已排序部分 [3] 比较,1<3,插入到最前,数组: [1, 3, 4, 2, 5] 
 
- i=2(元素4):4≥3,无需移动,数组不变: [1, 3, 4, 2, 5] 
 
- i=3(元素2):与已排序部分 [1,3,4] 比较,2<4→2<3→插入到3前,数组: [1, 2, 3, 4, 5] 
 
- i=4(元素5):5≥4,无需移动,最终有序数组: [1, 2, 3, 4, 5] 
 
四、优缺点与适用场景
 
- 优点:
 
- 实现简单,稳定性好。
 
- 对于几乎有序的数组效率较高(接近O(n))。
 
- 缺点:
 
- 时间复杂度高,不适用于大规模数据。
 
- 元素移动次数较多,平均性能低于希尔排序等优化版本。
 
- 适用场景:
 
- 小规模数据或近乎有序的数组。
 
- 作为其他排序算法(如希尔排序)的基础。
 
直接插入排序是理解插入排序思想的基础,实际应用中常被优化版本(如希尔排序)替代。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值