空间复杂度
排序方法 | 平均情况 | 最坏情况 | 最好情况 |
---|---|---|---|
基数排序 | 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)。
算法核心步骤
- 确定最大位数:遍历数组找到最大值,计算其位数
d
。 - 逐位排序(从低位到高位):
- 分配:根据当前位的值(0-9),将元素分配到对应的桶中。
- 收集:按桶顺序将元素合并回原数组。
- 重复步骤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)
,需额外空间存储临时数组。
算法核心步骤
- 分解:将数组从中间分为左右两半。
- 递归排序:对左右子数组分别递归调用归并排序。
- 合并:将两个有序子数组合并为一个有序数组。
#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))。
- 缺点:
- 时间复杂度高,不适用于大规模数据。
- 元素移动次数较多,平均性能低于希尔排序等优化版本。
- 适用场景:
- 小规模数据或近乎有序的数组。
- 作为其他排序算法(如希尔排序)的基础。
直接插入排序是理解插入排序思想的基础,实际应用中常被优化版本(如希尔排序)替代。