排序算法(冒泡、选择、插入、快排、堆排、归并)
冒泡排序
void bubbleSort(vector<int>& nums)
{
int n = nums.size();
for (int i = 0; i < n - 1; ++i)
{
for (int j = 0; j < n - 1 - i; ++j)
{
if (nums[j] > nums[j + 1])
swap(nums[j], nums[j+1]);
}
}
}
时间复杂度 O(n2)
原理:
遍历 n-1 轮,每轮遍历未排序部分,通过比较相邻元素并将较大值交换到右侧,来将当前未排序数组的最大值移动到未排序数组的尾部。
通过添加标志位的方式,可以让最好复杂度变成 O(n) ,通过添加标志位,使得冒泡排序在排序已经完成的情况下,退出循环,这样在数组已经排序好的情况下,只会进行一次遍历。
void bubbleSort(vector<int>& nums)
{
bool swapped;
int n = nums.size();
for (int i = 0; i < n - 1; ++i)
{
swapped = false;
for (int j = 0; j < n - 1 - i; ++j)
{
if (nums[j] > nums[j + 1])
{
swap(nums[j], nums[j + 1]);
swapped = true;
}
}
if (!swapped) break;
}
}
选择排序
void selectionSort(vector<int>& nums)
{
int n = nums.size();
for (int i = 0; i < n - 1; ++i)
{
int minIndex = i;
for (int j = i + 1; j < n; ++j)
{
if (nums[j] < nums[minIndex])
{
minIndex = j;
}
}
swap(nums[i], nums[minIndex]);
}
}
时间复杂度 O(n2)
原理:
遍历 n-1 轮,每轮遍历数组未排序部分,寻找最小值的索引,将其交换到未排序部分的首部。
插入排序
void insertionSort(vector<int>& nums)
{
int n = nums.size();
for (int i = 1; i < n; ++i)
{
int key = nums[i];
int j = i - 1;
while (j >= 0 && nums[j] > key)
{
nums[j + 1] = nums[j];
j--;
}
nums[j + 1] = key;
}
}
时间复杂度 O(n2)
原理:
将数组分为已排序和未排序两部分,逐个将未排序元素插入到已排序部分的正确位置。
快速排序
void quickSort(vector<int>& nums, int left, int right)
{
if (left >= right) return; // 终止条件
int pivot = nums[left];
int begin = left, end = right;
while (begin < end)
{
while (begin < end && nums[end] > pivot)
end--;
if (begin < end)
{
nums[begin] = nums[end];
}
while (begin < end && nums[begin] <= pivot)
begin++;
if (begin < end)
{
nums[end] = nums[begin];
}
}
nums[begin] = pivot;
quickSort(nums, left, begin - 1);
quickSort(nums, begin + 1, right);
}
时间复杂度为 O(nlogn),最坏情况时间复杂度为 O(n2)
原理:
- 选择基准值:从数组中选择一个元素作为基准
- 分区操作:排列数组使得小于基准的元素位于基准前面,大于基准的元素位于基准后面
- 递归排序:递归地对小于基准的子数组排序,递归地对大于基准的子数组排序
为什么最坏情况时间复杂度是 O(n²)?
当每次分区后,子数组极度不平衡时,例如:
- 数组已完全有序(升序或降序)。
- 每次选择的枢轴是当前子数组的最小或最大元素。
此时每次分区只能将数组分为 1 个元素(枢轴) 和 n-1 个元素,导致递归树退化为链表,从而递归深度为n,而每层排序的时间复杂度是O(n)。
堆排序
堆排序分成两部分:维护堆结构和使用维护好的堆排序。
堆结构有大根堆和小根堆,大根堆的父节点大于左右子节点,小根堆的父节点小于左右子节点。因此大根堆的根节点是堆中的最大值,小根堆的根节点是堆中的最小值。
核心思想:
- 构建堆
- 反复将堆顶元素与堆尾元素交换
- 维护剩余元素为堆结构
- 重复直到整个序列有序
以下以大根堆为例:
void keepHeap(vector<int>& nums, int n, int i) { int lChild = 2 * i + 1; int rChild = 2 * i + 2; int parent = i; if (lChild < n) { parent = nums[lChild] > nums[parent] ? lChild : parent; } if (rChild < n) { parent = nums[rChild] > nums[parent] ? rChild : parent; } if (parent != i) { swap(nums[parent], nums[i]); keepHeap(nums, n, parent); } }
void heapSort(vector<int>& nums)
{
int n = nums.size();
int i;
for (i = n/2-1; i >= 0; --i)
{
keepHeap(nums, n, i);
}
for (i = n-1; i>=0; --i)
{
swap(nums[0], nums[i]);
keepHeap(nums, i, 0);
}
}
时间复杂度 O(nlogn),最坏时间复杂度 O(nlogn)
堆排序实质是将数组维护成二叉树结构,以下是大根堆排序过程图解:

归并排序
归并排序分成两步:分组和合并。
核心思想:
- 分组:将数组递归拆分成左右两个子数组
- 合并:当子数组长度为1时,认为其有序,不再分组。并将两个有序数组合并成一个有序数组
void merge(vector<int>& nums, int left, int mid, int right) { // 合并两个有序数组,[left, mid] [mid+1, right] vector<int> tmp(right - left + 1); int i = left, j = mid+1; int index = 0; while (i < mid + 1 && j <= right) { if (nums[i] < nums[j]) tmp[index++] = nums[i++]; else tmp[index++] = nums[j++]; }while (i < mid + 1) { tmp[index++] = nums[i++]; } while (j <= right) { tmp[index++] = nums[j++]; } index = 0; for (i = left; i <= right; ++i) { nums[i] = tmp[index++]; }}
void mergeSort(vector<int>& nums, int left, int right)
{
if (left >= right) return; // 边界条件
// 分组
int mid = left + (right - left) / 2;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 合并
merge(nums, left, mid, right);
}
最优/平均/最差时间复杂度:O(nlogn)
原因:
- 递归深度:log n
- 每层合并操作:O(n)
算法图解:

浙公网安备 33010602011771号