目录
1. 冒泡排序(含优化)
基本思想及原理
基本思想
每次比较两个相邻的元素,如果它们的顺序错误就将它们交换过来。
排序原理
将数列中所有相邻的两个元素进行比较。每一次对数列进行遍历都可以找到现有数字中最大的数,当本次遍历完成后,该数将被冒泡到未排好序数中的最后一位,最终在多次遍历后完成排序。
排序过程
- 以从小到大排列为例,设原数组 [89, 23, 45, 7],灰色即当次比较并处理好的区域
- 第一趟
- 第一次 [23, 89, 45, 7]
- 第二次 [23, 45, 89, 7]
- 第三次 [23, 45, 7, 89]
- 第二趟
- 第一次 [23, 45, 7, 89]
- 第二次 [23, 7, 45, 89]
- 第三趟
- 第一次 [7, 23, 45, 89]
复杂度分析
平均时间复杂度 | O(n^2) |
最好情况 | O(n) |
最坏情况 | O(n^2) |
空间复杂度 | O(1) |
稳定性 | 稳定 |
代码实现
#include<iostream>
using namespace std;
int main() {
cout << "请输入数组长度:";
int i, j, n;
cin >> n;
int* a = new int[n]; // 动态创建大小为n的数组
cout << "请输入数组:";
for (i = 0; i < n; i++) {
cin >> a[i];
}
for (i = 0; i < n; i++) {
for (j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) { // 两两对比,此情况为从小到大排序
int temp = a[j]; // 满足条件,交换
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
cout << "排序后数组:";
for (i = 0; i < n; i++) {
cout << a[i] << " ";
}
}
优化
方案一
设置一个值,判断当轮冒泡有无发生交换,若没有交换说明数组已经有序,可直接退出循环
for (i = 0; i < n; i++) {
bool swap = false; // 添加bool值判断当次循环有无发生交换
for (j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) { // 两两对比,此情况为从小到大排序
int temp = a[j]; // 满足条件,交换
a[j] = a[j + 1];
a[j + 1] = temp;
swap = true; // 发生交换
}
}
if (!swap) { // 若没有交换,说明有序,直接退出循环
break;
}
}
方案二
在方案一的基础上,记录最后一次发生交换的位置,因为在最后一次交换的时候,那个位置之后的元素已经有序,后续冒泡时就无需比较该位置后的元素
int lastIndex = 0; // 记录最后交换位置
int lastBorder = n - 1; // 记录后边界,初始化为数组长度
for (i = 0; i < n; i++) {
bool swap = false; // 添加bool值判断当次循环有无发生交换
if (i != 0) { // 防止初次遍历数组越界
lastBorder = lastIndex; // 后边界,说明之后数组有序,无须比较
}
for (j = 0; j < lastBorder; j++) {
if (a[j] > a[j + 1]) { // 两两对比,此情况为从小到大排序
int temp = a[j]; // 满足条件,交换
a[j] = a[j + 1];
a[j + 1] = temp;
swap = true; // 发生交换
lastIndex = j; // 记录最后一次交换位置,作为后边界
}
}
if (!swap) { // 若没有交换,说明有序,直接退出循环
break;
}
}
2. 选择排序
基本思想及原理
基本思想
每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。
排序原理
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
排序过程
- 以从小到大排列为例,设原数组 [89, 23, 45, 7],灰色即当次比较并处理好的区域
- 第一趟
- 第一次 [23, 89, 45, 7]
- 第二次 [23, 89, 45, 7]
- 第三次 [7, 89, 45, 23]
- 第二趟
- 第一次 [7, 45, 89, 23]
- 第二次 [7, 23, 89, 45]
- 第三趟
- 第一次 [7, 23, 45, 89]
复杂度分析
平均时间复杂度 | O(n^2) |
最好情况 | O(n^2) |
最坏情况 | O(n^2) |
空间复杂度 | O(1) |
稳定性 | 不稳定 |
代码实现
#include<iostream>
using namespace std;
int main() {
cout << "请输入数组长度:";
int i, j, n;
cin >> n;
int* a = new int[n]; // 动态创建大小为n的数组
cout << "请输入数组:";
for (i = 0; i < n; i++) {
cin >> a[i];
}
for (i = 0; i < n - 1; i++) {
for (j = i + 1; j < n; j++) { // 从待排序数据中选出最小的交换
if (a[j] < a[i]) { // 从小到大
int temp = a[j]; // 交换
a[j] = a[i];
a[i] = temp;
}
}
}
cout << "排序后数组:";
for (i = 0; i < n; i++) {
cout << a[i] << " ";
}
}
3. 插入排序
基本思想及原理
基本思想
每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。
排序原理
逐步构建有序的序列,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
排序过程
- 以从小到大排列为例,设原数组 [89, 23, 45, 7],灰色即当次比较并处理好的区域
- 第一趟
- 第一次 [23, 89, 45, 7]
- 第二趟
- 第一次 [23, 45, 89, 7]
- 第二次 [23, 45, 89, 7]\
- 第三趟
- 第一次 [23, 45, 7, 89]
- 第二次 [23, 7, 45, 89]
- 第三次 [7 ,23, 45, 89]
复杂度分析
平均时间复杂度 | O(n^2) |
最好情况 | O(n) |
最坏情况 | O(n^2) |
空间复杂度 | O(1) |
稳定性 | 稳定 |
代码实现
#include<iostream>
using namespace std;
int main() {
cout << "请输入数组长度:";
int i, j, n;
cin >> n;
int* a = new int[n]; // 动态创建大小为n的数组
cout << "请输入数组:";
for (i = 0; i < n; i++) {
cin >> a[i];
}
for (i = 0; i < n - 1; i++) {
for (j = i + 1; j > 0; j--) { // 每次将下一个待排序数插入到对应位置
if (a[j] < a[j - 1]) { // 从小到大,依次将数往前排,直至插到正确位置
int temp = a[j]; // 插入数据需要被插入后续位置后移,所以要交换
a[j] = a[j - 1];
a[j - 1] = temp;
}
}
}
cout << "排序后数组:";
for (i = 0; i < n; i++) {
cout << a[i] << " ";
}
}
4. 桶排序
基本思想及原理
基本思想
把数组划分为若干个大小相同的子区间(桶),每个子区间各自排序,最后合并。
排序原理
将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。
排序过程
- 以从小到大排列为例,设原数组 [27, 23, 45, 42],分为10个桶,每个桶区间大小为10
- 桶【2】:[27, 23]
- 桶【4】:[45, 42]
- 其余桶都为空
- 对桶排序
- 桶【2】:[23, 27]
- 桶【4】:[42, 45]
- 合并桶
- [23, 27, 42, 45]
复杂度分析
平均时间复杂度 | O(n+k) |
最好情况 | O(n+k) |
最坏情况 | O(n^2) |
空间复杂度 | O(n+k) |
稳定性 | 稳定 |
代码实现
#include<iostream>
#include<vector>
using namespace std;
void bucketSort(int* a, int len) {
int i;
int maxValue = a[0];
for (i = 1; i < len; i++)
if (a[i] > maxValue) // 输入数据的最大值
maxValue = a[i];
// 设置10个桶
const int bucketCnt = 10;
vector<int> buckets[bucketCnt];
// 桶的大小bucketSize根据数组最大值确定:最大值/桶数,(桶数*桶大小)要大于最大值
// 根据最高位数字映射到相应的桶,映射函数为 arr[i]/bucketSize
int bucketSize = 1;
while (maxValue) { //求最大尺寸
maxValue /= 10;
bucketSize *= 10;
}
bucketSize /= 10; //桶的个数
// 入桶
for (int i = 0; i < len; i++) {
int idx = a[i] / bucketSize; //放入对应的桶
buckets[idx].push_back(a[i]);
// 对该桶进行排序
for (int j = int(buckets[idx].size()) - 1; j > 0; j--) {
if (buckets[idx][j] < buckets[idx][j - 1]) {
swap(buckets[idx][j], buckets[idx][j - 1]);
}
}
}
// 顺序访问桶,得到有序数组
for (int i = 0, k = 0; i < bucketCnt; i++) {
for (int j = 0; j<int(buckets[i].size()); j++) {
a[k++] = buckets[i][j];
}
}
}
int main() {
cout << "请输入数组长度:";
int i, j, n;
cin >> n;
int* a = new int[n]; // 动态创建大小为n的数组
cout << "请输入数组:";
for (i = 0; i < n; i++) {
cin >> a[i];
}
bucketSort(a, n);
cout << "排序后数组:";
for (i = 0; i < n; i++) {
cout << a[i] << " ";
}
}
5. 快速排序
基本思想及原理
基本思想
通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
排序原理
设定第一个数为基准数,以从小到大排序为例,给定两个下标,一左一右,从右往左遇到小于基准数的,右下标停下;从左往右遇到大于基准数的,左下标停下。左下标需保持小于右下标,然后交换两数,继续重复该过程,直到下标遭遇。遭遇位置即为基准数归位的位置,归位基准数后,将基准数两侧作为两个新的数组执行上述过程,即递归,直至所有数归位,数组有序。
排序过程
- 以从小到大排列为例,设原数组 [23, 6, 45, 7, 93]
- 第一次归位 基准数:23
- 从右到左找到小于基准数的,下标停在 7 的位置,下标为【3】
- 从左到右找到大于基准数的,下标停在 45 的位置,下标为【2】
- 交换,此时数组为[23, 6, 7, 45, 93]
- 从右到左找到小于基准数的,此时在下标相遇前没有找到,相遇下标为【2】
- 将基准数归位,此时数组为[7, 6, 23, 45, 93]
- 再对[7, 6]与[45, 93]重复上述步骤
- 先执行[7, 6]
- 第二次归位 基准数:7
- 从右到左找到小于基准数的,下标停在6的位置,下标为【1】
- 从左到右找到大于基准数的,此时在下标相遇前没有找到,相遇下标为【1】
- 将基准数归位,此时左侧拆分数组为[6, 7]
- 左侧只有一个数值(left = right),退出,右侧无数值(left > right),退出
- 第三次归位 基准数:45
- 从右到左找到小于基准数的,此时在下标相遇前没有找到,相遇下标为【3】
- 无变化,此时右侧拆分数组为[45, 93]
- 左侧无数值(left > right),退出,右侧只有一个数值(left = right),退出
- 至此,数组全部有序,为[6, 7, 23, 45, 93]
复杂度分析
平均时间复杂度 | O(nlogn) |
最好情况 | O(nlogn) |
最坏情况 | O(n^2) |
空间复杂度 | O(logn) |
稳定性 | 不稳定 |
代码实现
// c++
#include<iostream>
using namespace std;
void quickSort(int left, int right, int* a);
int main() {
int n;
cout << "请输入数组长度:";
cin >> n;
cout << "\n输入整数数组:";
int* a = (int*)malloc(sizeof(int) * n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
quickSort(0, n - 1, a);
cout << "\n排序后数组为:";
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
}
void quickSort(int left, int right, int* a) {
if (left >= right) return;
int i, j, t, temp; // temp为基准数
temp = a[left]; // 设置第一个元素为基准数
i = left;
j = right;
while (i < j)
{
while (a[j] >= temp && i < j) j--; // 从右到左找小于基准值的数
while (a[i] <= temp && i < j) i++; // 从左到右找大于基准值的数
if (i < j) { // 交换
t = a[i]; a[i] = a[j]; a[j] = t;
}
}
a[left] = a[i]; a[i] = temp; // 将基准数归位
quickSort(left, i - 1, a); // 递归
quickSort(i + 1, right, a);
return;
}
6. 合并排序
基本思想及原理
基本思想
通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
排序原理
将整个数组不断采用二分法拆分至不能继续拆分,即每组只有一个数据,再把它们两组两组合并到一起,并在合并过程中进行排序,直至把整个数据合到一组为止。
排序过程
- 以从小到大排列为例,设原数组 [8, 4, 5, 7, 1, 3, 6, 2]
- 拆分
- 第一次:[8, 4, 5, 7] [1, 3, 6, 2]
- 第二次:[8, 4] [5, 7] [1, 3] [6, 2]
- 第三次:[8] [4] [5] [7] [1] [3] [6] [2]
- 合并并排序
- 第一次:[4, 8] [5, 7] [1, 3] [6, 2]
- 第二次:[4, 5, 7, 8] [1, 2, 3, 6]
- 第三次:[1, 2, 3, 4, 5, 6, 7, 8]
复杂度分析
平均时间复杂度 | O(nlogn) |
最好情况 | O(nlogn) |
最坏情况 | O(nlogn) |
空间复杂度 | O(n) |
稳定性 | 稳定 |
代码实现
- mergeSort
传入参数为要排序的数组、数组长度
对要排序的数组使用二分法进行划分处理,再分别递归处理划分后的数组,具体代码如下:
void mergeSort(int* A, int lenA) {
if (lenA > 1) { // 数组A还能被分配
// 分治
int n1 = lenA / 2;
int n2 = lenA - n1;
// 将A拆成B与C两个数组
int* B = (int*)malloc(sizeof(int) * n1); // 动态分配
int* C = (int*)malloc(sizeof(int) * n2);
for (int i = 0; i < n1; i++) {
B[i] = A[i];
}
for (int i = 0; i < n2; i++) {
C[i] = A[n1 + i];
}
// 递归
mergeSort(B, n1);
mergeSort(C, n2);
// 合并
merge(B, n1, C, n2, A, lenA);
// 释放内存
free(B);
free(C);
}
}
- merge
传入参数为二分后的两个数组和原数组及它们的数组长度
将两个数组按顺序插入,替换掉原数组的值,使原数组有序,具体代码如下:
void merge(int* B, int lenB, int* C, int lenC, int* A, int lenA) { // 合并
int i = 0, j = 0, k = 0; // i对应数组B, j对应数组C, k对应数组A
while (i < lenB && j < lenC) { // 数组B和数组C的数都没被分配完的情况
if (B[i] <= C[j]) A[k++] = B[i++]; // 升序,从小到大 将较小的先合并进新数组
else A[k++] = C[j++];
}
if (i == lenB) { // 数组B被分配完,数组C剩余的依次分配进新数组
while (j < lenC) A[k++] = C[j++];
}
else { // j == lenC 数组C被分配完,数组B剩余的依次分配进新数组
while (i < lenB) A[k++] = B[i++];
}
}
- 完整代码
#include<iostream>
#include<cstdlib>
using namespace std;
void mergeSort(int* A, int lenA);
void merge(int* B, int lenB, int* C, int lenC, int* A, int lenA);
int main() {
int n;
cout << "请输入数组长度:";
cin >> n;
cout << "\n输入整数数组:";
int* a = (int*)malloc(sizeof(int) * n); // 动态分配
for (int i = 0; i < n; i++) {
cin >> a[i];
}
mergeSort(a, n);
cout << "\n排序后数组为:";
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
}
void mergeSort(int* A, int lenA) {
if (lenA > 1) { // 数组A还能被分配
// 分治
int n1 = lenA / 2;
int n2 = lenA - n1;
// 将A拆成B与C两个数组
int* B = (int*)malloc(sizeof(int) * n1); // 动态分配
int* C = (int*)malloc(sizeof(int) * n2);
for (int i = 0; i < n1; i++) {
B[i] = A[i];
}
for (int i = 0; i < n2; i++) {
C[i] = A[n1 + i];
}
// 递归
mergeSort(B, n1);
mergeSort(C, n2);
// 合并
merge(B, n1, C, n2, A, lenA);
// 释放内存
free(B);
free(C);
}
}
void merge(int* B, int lenB, int* C, int lenC, int* A, int lenA) { // 合并
int i = 0, j = 0, k = 0; // i对应数组B, j对应数组C, k对应数组A
while (i < lenB && j < lenC) { // 数组B和数组C的数都没被分配完的情况
if (B[i] <= C[j]) A[k++] = B[i++]; // 升序,从小到大 将较小的先合并进新数组
else A[k++] = C[j++];
}
if (i == lenB) { // 数组B被分配完,数组C剩余的依次分配进新数组
while (j < lenC) A[k++] = C[j++];
}
else { // j == lenC 数组C被分配完,数组B剩余的依次分配进新数组
while (i < lenB) A[k++] = B[i++];
}
}