文章目录
1、冒泡排序
从左往右相邻的数两两比较,如果左边大于右边则交换,每一轮冒泡会选出一个未排序数字中的最大值放到最后,每一轮确定一个数字的最终位置,排序完n个数字需要n-1轮。
-
外层循环通过
i
控制遍历次数,最大到n-1
。每轮比较中最大的元素都会被“冒泡”到数组的末尾,因此内层循环的范围是n - i - 1
。 -
swapped
用于记录当前轮次是否有元素交换。如果某轮中没有发生交换,意味着数组已经有序,可以提前退出循环,避免不必要的比较。 -
冒泡排序在最差情况下的时间复杂度是 O(n²),但在最佳情况下(数组已经有序时),通过
swapped
变量,时间复杂度可以优化为 O(n)。
void bubble_sort(int a[], int n) {
for (int i = 0; i < n - 1; i++) { // 每轮排好一个数字,排n-1轮
bool swapped = false; // 每次循环开始时重置标志位
for (int j = 0; j < n - i - 1; j++) { //排好的放在后面,未排序的数字减少一个
if (a[j] > a[j + 1]) { // 比较相邻元素
swap(a[j], a[j + 1]); // 交换元素
swapped = true; // 记录交换发生
}
}
if (!swapped) break; // 如果没有发生交换,数组已经有序,提前退出
}
}
2、选择排序
从左往右扫描,每一轮选出一个当前未排序数字中的最小值,放到前面,每一轮确定一个数字的最终位置,排完n个数字需要n-1轮。
-
外层循环通过
i
控制遍历次数,最大到n-1
,每次遍历选择数组中未排序部分的第一个元素。 -
内层循环通过
j
遍历未排序的部分,从i + 1
开始到n
。内层循环的目的是找到当前未排序部分的最小元素,并记录其索引minIndex
。 -
在每轮结束时,将当前元素
a[i]
与找到的最小元素a[minIndex]
进行交换。 -
即使数组已经有序,选择排序也仍然需要 O(n²) 的比较次数,因为每次都需要遍历未排序部分。
void select_sort(int a[], int n) {
for (int i = 0; i < n - 1; i++) { // 每轮排好一个数字,排n-1轮
int minIndex = i; // 假设当前元素为最小值
for (int j = i + 1; j < n; j++) // 排好的放在前面,未排序的数字减少一个
if (a[j] < a[minIndex]) minIndex = j; // 找到更小的元素,更新最小值索引
swap(a[i], a[minIndex]); // 交换最小值到当前排序位置
}
}
3、插入排序
外层循环 (for (int i = 1; i < n; i++)
):从第二个元素开始遍历,作为当前要插入的元素 x
。
x
变量:存储当前要插入的元素,以便在内层循环中进行比较和插入操作。
已排序部分的下标 j
:j
初始化为 i - 1
,表示已排序部分的最后一个元素。
内层循环 (while (j >= 0 && x < a[j])
):
- 在已排序部分中查找
x
的插入位置。 - 越界检查:首先判断
j >= 0
,确保访问a[j]
不会越界。 - 如果
a[j]
大于x
,则将a[j]
向右移动,腾出j + 1
的位置来插入x
。
插入操作 (a[j + 1] = x
):当找到合适位置后,将 x
插入到 j + 1
的位置。
void insert_sort(int a[], int n) {
for (int i = 1; i < n; i++) { // 从第二个元素遍历要插入的元素
int x = a[i]; // 当前要插入的元素
int j = i - 1; // 已排序的最后一个元素下标
// 在已排序部分中找到 x 的插入位置
while (j >= 0 && x < a[j]) { // 先判断j>=0,防止越界错误
a[j + 1] = a[j]; // 元素向右移动,为x空出位置
j--; // 移动到前一个元素
}
a[j + 1] = x; // 将 x 插入到找到的位置
}
}
4、桶排序
cnt[N] = {0}
:定义一个大小为 N
的数组 cnt
,用于统计每个元素的出现次数,初始化为 0。
输入读取和计数:使用 for
循环读取 n
个整数输入,检查 x
是否在合法范围内 [0, N)
,并通过 cnt[x]++
对每个 x
的出现次数进行统计。
输出:第二个 for
循环遍历 cnt
数组,通过嵌套循环输出每个数 i
的值,输出的次数取决于 cnt[i]
的值。
const int N = 10000;
int cnt[N] = {0}, n;
cin >> n; // 读取元素个数
// 读取每个元素并进行计数
for (int i = 0; i < n; i++) {
int x;
cin >> x;
if (x >= 0 && x < N) cnt[x]++;
}
for (int i = 0; i < N; i++)
for (int j = 0; j < cnt[i]; ++j)
cout << i << " "; // 按照计数输出数字 i
5、快速排序
通过分治法,每次递归时选取一个基准值,然后通过双指针将数组划分成两部分,使得左边部分的值小于等于基准值,右边部分的值大于等于基准值。然后递归地对两部分进行排序,直到数组被完全排序。
a[]
是要排序的数组。l
是当前处理的数组区间的左边界(闭区间)。r
是当前处理的数组区间的右边界(闭区间)。
递归终止的条件是 l >= r
,也就是说当前区间长度为 0 或 1 时,不需要继续排序。
基准值:x = a[(l + r) >> 1]
通过 (l + r) >> 1
计算出中间元素的下标,并将它作为基准值 x
。
双指针初始化:
i = l - 1
,指向比基准值小的区域。j = r + 1
,指向比基准值大的区域。
主循环条件:while (i < j)
,保证左右指针在数组内并且没有交错。
内循环(左指针 i
):do i++; while (a[i] < x);
从左向右找到第一个大于等于基准值的元素。
内循环(右指针 j
):do j--; while (a[j] > x);
从右向左找到第一个小于等于基准值的元素。
元素交换:当 i < j
时,交换 a[i]
和 a[j]
,确保基准值左边的元素都小于等于它,右边的元素都大于等于它。
左半部分递归:对 l
到 j
的部分继续执行快速排序。此时 j
是划分完成后,左半部分的最大值位置。
右半部分递归:对 j + 1
到 r
的部分继续执行快速排序。
void quick_sort(int a[], int l, int r) {
// 递归结束条件:当区间元素只有1个元素或者没有元素时
if (l >= r) return;
// 初始化双指针i和j,
int i = l - 1, j = r + 1; //后面会先++或--,所以指向边界的前一个或后一个元素
int pivot = a[(l + r) >> 1]; // 选择中间位置的值作为基准值
// 双指针划分过程
while (i < j) {
// 左指针向右移动,指针左侧都是小于基准值的,找到第一个大于等于基准值的元素
do i++; while (a[i] < pivot);
// 右指针向左移动,指针右侧都是大于基准值的,找到第一个小于等于基准值的元素
do j--; while (a[j] > pivot);
// 如果i和j两个指针还没有相遇,交换a[i]和a[j]
if (i < j) swap(a[i], a[j]);
}
quick_sort(a, l, j); // 递归调用,对左半部分(l到j)进行快速排序
quick_sort(a, j + 1, r); // 递归调用,对右半部分(j+1到r)进行快速排序
}
6、归并排序
通过递归将数组分成两半,分别排序,递归到最后,实际是把单个元素看作一个有序序列,开始两两归并,形成一个两个元素的有序序列,再两两归并,形成一个四个元素的有序序列,不断合并两个有序的子数组来达到排序的效果。
a[]
是要排序的数组。l
是当前处理的数组区间的左边界(闭区间)。r
是当前处理的数组区间的右边界(闭区间)。
终止条件:如果 l >= r
(即数组只剩一个或没有元素时),直接返回。
mid = (l + r) >> 1
:通过取中间位置mid
将当前区间[l, r]
分为两个子区间[l, mid]
和[mid + 1, r]
。- 递归调用
merge_sort(a, l, mid)
对左半部分排序,merge_sort(a, mid + 1, r)
对右半部分排序。
合并两个有序部分:
- 使用两个指针
i
和j
分别指向左半部分[l, mid]
和右半部分[mid + 1, r]
的起始位置。通过比较a[i]
和a[j]
的值,将较小的值放入临时数组tmp[]
。 - 如果左半部分未遍历完,将剩余部分加入
tmp[]
。 - 如果右半部分未遍历完,也将其加入
tmp[]
。 - 最后,将临时数组
tmp[]
中的元素复制回原数组a[]
,完成本次合并。
void merge_sort(int a[], int l, int r) {
// 递归终止条件:如果子序列中只有1个元素或0个元素,返回
if (l >= r) return;
int mid = l + r >> 1; // 计算中间索引,将数组一分为二
merge_sort(a, l, mid); // 递归排序左半部分
merge_sort(a, mid + 1, r); // 递归排序右半部分
// 合并两个已排序的部分
int k = 0; // 临时数组的索引
int i = l, j = mid + 1; // 左半部分和右半部分的指针
// 合并过程:将较小的元素放入临时数组
while (i <= mid && j <= r) {
// 如果左半部分当前元素小于右半部分,加入临时数组
if (a[i] < a[j]) tmp[k++] = a[i++];
// 否则,加入右半部分的当前元素
else tmp[k++] = a[j++];
}
// 将左半部分的剩余元素加入临时数组
while (i <= mid) tmp[k++] = a[i++];
// 将右半部分的剩余元素加入临时数组
while (j <= r) tmp[k++] = a[j++];
// 将临时数组的内容复制回原数组
for (i = l, k = 0; i <= r; i++, k++) a[i] = tmp[k];
}
7、sort()排序
7.1 概述
sort()
函数是一个比较灵活的函数。很多解释是:sort()
函数是类似于快速排序的方法,时间复杂度为n*log2(n),执行效率较高。
7.2 sort()的使用方法
在C++中使用sort()
函数需要使用#include <algorithm>
头文件,algorithm意为"算法",是C++的标准模版库(STL)中最重要的头文件之一,提供了大量基于迭代器的非成员模版函数。
sort(begin, end, cmp);
sort()
函数可以对给定区间的元素进行排序,它有三个参数:
- 其中
begin
为待排序数组的起始地址 end
为指向待排序数组结束地址下一个位置的指针cmp
参数为排序准则,如果不写,默认从小到大进行排序。如果我们想从大到小排序可以将cmp
参数写为greater<int>()
,<>
中表示排序数组的类型,C++11中可以透明比较器greater<>()
。如果需要按照其他的排序准则,那么需要我们自己定义一个bool
类型的函数来传入。
使用sort()
不仅仅可以从大到小或者从小到大排,还可以按照一定的准则进行排序,编写cmp
函数传入sort()
函数。比如按照个位从小到大比较:
bool cmp(int a, int b) {
return a % 10 > b % 10;
}
对结构体进行排序,比如定义一个结构体包含学生的姓名和年龄,按照年龄从小到大排序:
struct Student {
string name;
int age;
};
bool cmp(Student a, Student b) {
return a.age < b.age;
}
在以上的代码示例中使用了值传递,每次调用函数都会创建Student
对象的副本,增加额外的开销,降低排序的效率,使用引用传递可以避免拷贝开销,更加高效。值传递会创建参数的副本,对于大型对象或复杂数据结构,可能涉及大量的内存分配和数据复制,引用传递避免了这些操作,因为它直接操作原始对象。
特性 | 值传递 | 引用传递 |
---|---|---|
数据传递方式 | 副本传递,函数操作的是实参的副本 | 引用传递,函数操作的是实参的原数据 |
内存开销 | 需要创建副本,开销较大,尤其对于大型对象 | 无需创建副本,内存开销小 |
是否修改实参 | 函数内部的修改不会影响实参 | 函数内部的修改会直接影响实参 |
安全性 | 相对更安全,因为函数无法修改外部数据 | 可能产生副作用,修改不应修改的数据 |
适用场景 | 当不希望修改外部数据,或数据结构比较简单时使用 | 当需要修改实参,或数据结构较大时使用 |
bool cmp(const Student& a,const Student& b) {
return a.age < b.age;
}
引用参数:const Student& a
和 const Student& b
表示对 Student
类型的常量引用,这样函数内部无法修改 Student
的内容,同时避免了复制带来的性能损失。
const 关键字:使用 const
关键字表明函数不会修改传入的对象,这也是一个良好的编码习惯。