在考研中,经常会考到各种排序算法,所以总结一下,以便复习。
各类排序的时间复杂度和空间复杂度概览;
1. 直接插入排序
其基本操作是将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表。
特性
- 时间复杂度
- 最好情况就是全部有序,此时只需遍历一次,最好的时间复杂度为
O(n)
- 最坏情况全部反序,内层每次遍历已排序部分,最坏时间复杂度为
O(n^2)
- 综上,因此直接插入排序的平均时间复杂度为
O(n^2)
2.空间复杂度
- 辅助空间是常量
- 平均的空间复杂度为:
O(1)
3.算法稳定性
- 判断标准:相同元素的前后顺序是否改变
- 插入到比它大的数前面,所以直接插入排序是稳定的
4. 完整可实现代码
c++
#include <bits/stdc++.h>
using namespace std;
// 直接插入排序
// 其基本操作是将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表
void InsertSort(int a[], int l) {
int temp;
int j;
// 先看第一个数,将数组划分为有序和无序部分,第一个数默认有序。
for (int i = 1; i < l; i++) {
// 取出无序部分的首个,在有序部分从后向前比较,插入到合适的位置
if (a[i] < a[i - 1]) {
temp = a[i]; // temp暂存需要插入的数
for (j = i - 1; j >= 0 && temp < a[j]; j--) {
a[j + 1] = a[j]; // 取出原序列中大于暂存的数,往后移
} // 循环结束后,已经找到需要插入元素的位置
a[j + 1] = temp; // 将该位置插入元素
}
cout << "第" << i << "次插入:";
for (int k = 0; k < l; k++) {
cout << a[k] << " ";
}
cout << endl;
}
}
int main() {
int a[10] = {2, 5, 8, 3, 6, 9, 1, 4, 7};
int b[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int len = 9;
InsertSort(a, len);
return 0;
}
2. 折半插入排序
- 特性
- 时间复杂度:
折半查找只是减少了比较次数,但是元素的移动次数不变,所以时间复杂度为O(n^2)
- 空间复杂度
平均的空间复杂度也是:O(1)
- 稳定性: 稳定的排序算法
c++
#include <bits/stdc++.h>
using namespace std;
void BInsertSort(int a[], int l) {
int temp;
int low, high;
int m;
for (int i = 1; i < l; i++) {
// 取出无序部分的首个,在有序部分二分查找到位置
if (a[i] < a[i - 1]) {
low = 0;
high = i - 1;
// 在前面有序的部分进行二分查找
while (low <= high) {
m = low + (high - low) / 2;
if (a[m] > a[i]) // 有序部分的中间数,大于无序部分的首个元素
high = m - 1;
else
low = m + 1; // low的位置最终为小于等于a[i]的数
}
temp = a[i]; // 暂存需要插入的元素
// 遍历,将元素向右移,找到插入的位置
for (int j = i; j > low; j--) {
a[j] = a[j - 1];
}
a[low] = temp; // 在找到的位置进行赋值
}
cout << "第" << i << "次插入:";
for (int k = 0; k < l; k++) {
cout << a[k] << " ";
}
cout << endl;
}
}
int main() {
int a[10] = {2, 5, 8, 3, 6, 9, 1, 4, 7};
int b[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int len = 9;
BInsertSort(a, len);
return 0;
}
3. 折半查找
也称二分查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
c++
#include <bits/stdc++.h>
using namespace std;
// 也称二分查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列
int BinarySearch(int a[], int low, int high, int target) {
// 最终low与high会合并(注意合并时也要比较),按照规律,此时high会到low前面,这时才可以确定没有,所以这个循环的条件就是low<=high
while (low <= high) {
int mid = low + (high - low) / 2; // 溢出问题
cout << "low:" << low << " high:" << high << " mid:" << mid << endl;
if (a[mid] >
target) { // 将两个数进行比较,然后缩小查找范围,。当中间数大于查找数时,往左边找
high = mid - 1;
} else if (a[mid] < target) { // 中间数小于查找数时,往右边找
low = mid + 1;
} else { // 相等时,直接返回
return mid;
}
}
return -1;
}
int main() {
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int len = 9;
cout << BinarySearch(a, 0, len - 1, 3) << endl;
return 0;
}
4. 希尔排序
是插入排序的一种又称"缩小增量排序"(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。
特性 1.时间复杂度 对于这个算法有两个常见结果 O ( n^2 ) O(n^1.5)
2.空间复杂度 和直接插入一样 平均的空间复杂度为:O(1)
3.算法稳定性 相同元素的前后顺序改变
c++
#include <bits/stdc++.h>
using namespace std;
void ShellSort(int a[], int len) {
int temp;
int j;
int gap = len / 2;
int index = 0; // 增加增量
while (gap) // 多组
{
for (int i = gap; i < len; i++) // 直接从组内第二个判起
{
// 是否需要变,比如第一步的2和9位置不需要改变
if (a[i] < a[i - gap]) {
index++;
cout << "第" << index << "次交换" << a[i] << "和" << a[i - gap]
<< "后:";
temp = a[i]; // 取出改变值,就是需要交换位置,更大的那个数
// 需要交换位置的数,小于大的数,还要保证下标大于等于0,
for (j = i - gap; j >= 0 && temp < a[j]; j = j - gap) // 找位置
{
a[j + gap] = a[j]; // 右移
}
a[j + gap] = temp; // 插入
for (int k = 0; k < len; k++)
cout << a[k] << " ";
cout << endl;
}
}
gap /= 2;
}
}
int main() {
int a[10] = {2, 5, 8, 3, 6, 9, 1, 4, 7, 0};
int b[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int len = 10;
ShellSort(a, len);
return 0;
}
5. 冒泡排序
是一种简单的排序算法
由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名"冒泡排序"。
特性
- 时间复杂度
- 最好情况就是全部有序,此时只需遍历一次,因此冒泡排序最好的时间复杂度为
O(n)
- 最坏情况全部反序,两重for循环全部执行,因此冒泡排序的最坏时间复杂度为
O(n^2)
综上,因此冒泡排序总的平均时间复杂度为O(n^2)
- 空间复杂度
- 最优的空间复杂度就是不需要借用第三方内存空间,则复杂度为0
- 最差的空间复杂度就是开始元素逆序排序,每次都要借用一次内存,按照实际的循环次数,为
O(n)
平均的空间复杂度为:O(1)
- 算法稳定性
- 相同元素的前后顺序是否改变, 因为只交换不同大小的,所以冒泡排序是稳定的
c++
#include <iostream>
using namespace std;
void BubbleSort(int a[], int l) {
int ans;
int temp;
int index;
for (int i = l - 1; i >= 1; i--) // 一重循环,n-1次
{
ans = 1;
for (int j = 1; j <= i; j++) // 第二个数到未排序数
{
//大的往后提
if (a[j - 1] > a[j]) {
ans = 0; // 有未排序的
// temp = arr[j];
// arr[j] = arr[j + 1];
// arr[j + 1] = temp;
swap(a[j], a[j - 1]); // 交换
cout << "第" << ++index << "次排序:";
for (int k = 0; k < l; k++) // 打印数组
{
cout << a[k] << " ";
}
cout << endl;
}
}
if (ans) // 额外标记
break;
}
}
int main() {
int a[20] = {2, 5, 8, 3, 6, 9, 1, 4, 7};
int b[20] = {1, 1, 1, 1, 1, 1, 1, 1, 1};
int len = 9, temp;
BubbleSort(a, len);
return 0;
}
6. 快速排序
快速排序的基本思想是:通过一次排序将要排序的数据分成两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后,再按此方法对这两部分数据分别进行快速排序,直到有序。
- 算法设计
(1)分解: 先从数列中取出一个元素作为基准元素。以基准元素为标准,将问题分解为两个子序列,使小于或者等于基准元素的子序列在左侧,使大于基准元素的子序列在右侧。
(2)治理 : 对两个子序列进行快速排序(递归快速排序)。
(3)合并: 将排好的两个子序列合并在一起,得到原问题的解。
(4)基准元素的选取:
①:取第一个元素。(通常选取第一个元素)
②:取最后一个元素
③:取中间位置的元素
④:取第一个、最后一个、中间位置元素三者之中位数
⑤:取第一个和最后一个之间位置的随机数 k (low<=k<=hight)
- 快速排序的情况比较棘手,在最糟情况下,其运行时间为O(n2)。在平均情况下,快速排序的运行时间为O(nlogn)。
c++
#include <bits/stdc++.h>
using namespace std;
// 快速排序(从小到大)
void quickSort(int left, int right, int arr[]) {
if (left >= right)
return;
int i, j, base, temp;
i = left,
j = right; // 把待排序数组元素的第一个和最后一个下标分别赋值给i,j,使用i,j进行排序;
// 将待排序数组的第一个元素作为哨兵,将数组划分为大于哨兵以及小于哨兵的两部分
base = arr[left]; // 取最左边的数为基准数
while (i < j) {
while (arr[j] >= base && i < j)
j--;
while (arr[i] <= base && i < j)
i++;
if (i < j) {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 基准数归位
arr[left] = arr[i];
arr[i] = base;
quickSort(left, i - 1, arr); // 递归左边
quickSort(i + 1, right, arr); // 递归右边
}
void print(int a[], int n) {
for (int j = 0; j < n; j++) {
cout << a[j] << " ";
}
cout << endl;
}
int main() {
int a[10] = {8, 1, 9, 7, 2, 4, 5, 6, 10, 3};
cout << "初始序列:";
print(a, 10);
quickSort(0, 9, a);
cout << "排序结果:";
print(a, 10);
return 0;
}
7. 简单选择排序
特性
1.时间复杂度:O(n^2)
2.空间复杂度:平均的空间复杂度为:O(1)
3.算法稳定性: 不稳定
可实现代码
c++
#include <bits/stdc++.h>
using namespace std;
void SelectSort(int a[], int len) {
int k;
for (int i = 0; i < len - 1; i++) // 未排序第一个数
{
k = i; // 取第一个数
for (int j = i + 1; j < len; j++) // 遍历未排序部分
{
if (a[j] < a[k]) {
k = j; // 取更小
}
}
if (i != k) // 不在未排序第一的位置
{
swap(a[k], a[i]);
}
cout << "第" << i + 1 << "次排序后";
for (int t = 0; t < len; t++)
cout << a[t] << " ";
cout << endl;
}
};
int main() {
int a[9] = {2, 7, 8, 3, 7, 9, 1, 3, 7};
int len = 9;
SelectSort(a, len);
return 0;
}
8. 堆排序
- 什么是堆 首先堆heap是一种数据结构,是一棵完全二叉树且满足性质:所有非叶子结点的值均不大于或均不小于其左、右孩子结点的值.
- 下面通过一组数据说明堆排序的方法:
9, 79, 46, 30, 58, 49
基本过程:
- 先将待排序的数视作完全二叉树(按层次遍历顺序进行编号, 从0开始),如下图:
- 完全二叉树的最后一个非叶子节点,也就是最后一个节点的父节点。
- 最后一个节点的索引为数组长度len-1,那么最后一个非叶子节点的索引应该是为(len-1)/2.也就是从索引为2的节点开始
- 如果其子节点的值大于其本身的值。则把他和较大子节点进行交换,即将索引2处节点和索引5处元素交换。交换后的结果如图:
建堆从最后一个非叶子节点开始即可
3:向前处理前一个节点,也就是处理索引为1的节点,此时79>30,79>58,因此无需交换。
- 向前处理前一个节点,也就是处理索引为0的节点,此时9 < 79,9 < 49, 因此需交换。应该拿索引为0的节点与索引为1的节点交换,因为79>49. 如图:
- 如果某个节点和它的某个子节点交换后,该子节点又有子节点,系统还需要再次对该子节点进行判断。如上图因为1处,3处,4处中,1处的值大于3,4处的值,所以还需交换。
牢记: 将每次堆排序得到的最大元素与当前规模的数组最后一个元素交换。
6. 完整可实现代码
c++
#include <bits/stdc++.h>
using namespace std;
/*
8
1 14
3 21 5 7
10
*/
void adjust(int arr[], int len, int index) {
int left = 2 * index + 1;
int right = 2 * index + 2;
int maxIdx = index;
if (left < len &&
arr[left] >
arr[maxIdx]) // 左边的小标小于总的数组长度,和左边的值小于传入索引的值
maxIdx = left; // 记录大值的索引
if (right < len && arr[right] > arr[maxIdx])
maxIdx = right; // maxIdx是3个数中最大数的下标
if (maxIdx != index) // 如果maxIdx的值有更新
{
swap(arr[maxIdx], arr[index]);
adjust(arr, len, maxIdx); // 递归调整其他不满足堆性质的部分
}
}
void heapSort(int arr[], int size) {
for (int i = size / 2 - 1; i >= 0;
i--) { // 对每一个非叶结点进行堆调整(从最后一个非叶结点开始)
adjust(arr, size, i); // i为调整元素的下标
}
for (int i = size - 1; i >= 1; i--) {
swap(arr[0], arr[i]); // 将当前最大的放置到数组末尾
adjust(arr, i, 0); // 将未完成排序的部分继续进行堆排序
}
}
int main() {
int array[8] = {8, 1, 14, 3, 21, 5, 7, 10};
heapSort(array, 8);
for (auto it : array) {
cout << it << " ";
}
return 0;
}
其实整个堆排序过程中, 我们只需重复做两件事:
- 建堆(初始化+调整堆, 时间复杂度为
O(n)
; - 拿堆的根节点和最后一个节点交换(siftdown, 时间复杂度为
O(n*log n)
;
因而堆排序整体的时间复杂度为O(n*log n)
.
9. 归并排序
- 归并排序是比较稳定的排序方法。
- 它的基本思想是把待排序的元素分解成两个规模大致相等的子序列。
- 如果不易分解,将得到的子序列继续分解,直到子序列中包含的元素个数为1。
- 因为单个元素的序列本身就是有序的,此时便可以进行合并,从而得到一个完整的有序序列。
- 流程图示例
- 首先我们先给定一个无序的数列(42,15,20,6,8,38,50,12),我们进行合并排序数列,如下图流程图所示:
思路:
-
步骤一:首先将待排序的元素分成大小大致相同的两个序列。
-
步骤二:再把子序列分成大小大致相同的两个子序列。
-
步骤三:如此下去,直到分解成一个元素停止,这时含有一个元素的子序列都是有序的。
-
步骤四:进行合并操作,将两个有序的子序列合并为一个有序序列,如此下去,直到所有的元素都合并为一个有序序列。
解释一下int *
;
在C++中,"int * a"通常表示一个整型指针。这是一个指针变量,它存储的是整型变量的地址。
举个例子,假设我们有一个整数变量 int num = 5;
,如果我们声明一个指针变量 int * a;
并使其指向 num,那么 a = #
或 a = num;
(此处是取地址运算符的应用)之后,a 就存储了 num 的地址。
之后,我们可以通过解引用操作符 * 来访问 num 的值,如 cout << *a;
就会输出 5。因为我们把 a 理解为存储了 num 的地址,解引用操作符 * 就是取出该地址处存储的值。
此外,如果我们声明了一个指针数组,例如 int * a[5];
,那么 a 就存储了五个整型指针的地址,每个整型指针都可以存储一个整型变量的地址。
c++
#include <bits/stdc++.h>
using namespace std;
void merge(int* a, int low, int mid, int hight) // 合并函数
{
cout << "a: " << *a << endl;
cout << "low: " << low << endl;
cout << "mid: " << mid << endl;
cout << "hight: " << hight << endl;
// 用 new 申请一个辅助函数,创建一个新的动态数组,用于存放合并后的有序数组。
int* b = new int[hight - low + 1];
int i = low, j = mid + 1, k = 0; // k为 b 数组的小标
cout << "a[i]: " << a[i] << endl;
cout << "a[j]: " << a[j] << endl;
while (i <= mid && j <= hight) {
if (a[i] <= a[j]) {
b[k++] = a[i++]; // 按从小到大存放在 b 数组里面
} else {
b[k++] = a[j++];
}
}
while (i <= mid) // j 序列结束,将剩余的 i 序列补充在 b 数组中
{
b[k++] = a[i++];
}
while (j <= hight) // i 序列结束,将剩余的 j 序列补充在 b 数组中
{
b[k++] = a[j++];
}
k = 0; // 从小标为 0 开始传送
for (int i = low; i <= hight; i++) // 将 b 数组的值传递给数组 a
{
a[i] = b[k++];
}
delete[] b; // 辅助数组用完后,将其的空间进行释放(销毁)
}
void mergesort(int* a, int low, int hight) // 归并排序
{
if (low < hight) {
int mid = (low + hight) / 2;
mergesort(a, low, mid); // 对 a[low,mid]进行排序
mergesort(a, mid + 1, hight); // 对 a[mid+1,hight]进行排序
merge(a, low, mid, hight); // 进行合并操作
}
}
int main() {
int a[9] = {2, 4, 8, 3, 565, 9, 1, 56, 7};
int n = 9;
mergesort(a, 0, n - 1);
cout << "归并排序结果:" << endl;
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
10. 基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基数排序基于计数排序。
图像过程:
c++
#include <bits/stdc++.h>
using namespace std;
int maxBit(vector<int> vec) {
int max = vec[0];
for (int i = 1; i < vec.size(); i++) {
if (vec[i] > max)
max = vec[i];
}
int p = 1;
while (max / 10 != 0) {
max = max / 10;
p++;
}
return p;
}
vector<int> countSort(vector<int> vec,
int place) { // 按指定位置的值进行计数排序
vector<int> temp(vec); // 临时数组,存储place位置的值
vector<int> ordered(vec); // 排好序的数组放进ordered
int count[10] = {0};
for (int i = 0; i < vec.size(); i++) {
// 找到每个元素place位置上的数字存在temp中
temp[i] = (int)(vec[i] / pow(10, place - 1)) % 10;
}
for (int i = 0; i < vec.size(); i++) {
count[temp[i]]++; // 统计数字出现的频率
}
for (int i = 1; i < 10; i++) {
// 此时count中存放着temp中每个数字的索引
count[i] = count[i] + count[i - 1];
}
for (int i = vec.size() - 1; i >= 0; i--) {
int index = count[temp[i]] - 1;
count[temp[i]]--;
ordered[index] = vec[i];
}
// cout << "next:" << endl;
return ordered;
}
void radixSort(vector<int>& vec) {
// 最长位数是多少,代表要进行几次排序
int maxbit = maxBit(vec);
// i为多少,代表按第几位数字比较(从右向左)
for (int i = 1; i <= maxbit; i++) {
vec = countSort(vec, i);
}
for (int i = 0; i < vec.size(); i++) {
cout << vec[i] << " ";
}
}
int main() {
// vector<int> 是 C++
// 标准模板库(STL)中的一个模板类,它表示一个动态数组,可以存储整数类型(int)的数据。
// 具体来说,vector<int>
// 是一个可以动态改变大小的数组,它提供了方便的接口来管理数组中的元素。你可以通过
// push_back() 方法添加元素,通过 erase()
// 方法删除元素,也可以通过下标访问或者迭代器访问元素。
vector<int> vec{3, 5, 34, 10, 8, 6, 16, 5, 8, 6, 2, 4, 900, 4, 7,
0, 1, 8, 9, 7, 38, 1, 2, 5, 9, 7, 4, 0, 2, 6};
radixSort(vec);
return 0;
}
11. 计数排序
当待排序数组中的元素是某个区间的整数时,可以使用计数排序。
- 其基本思想就是对每一个输入元素,确定出小于x的元素个数。有了这一信息,就可以把x直接放到它在最终输出数组中的位置上。
- 例如,如果有17个元素小于x,则x就属于第18个输出位置。
c++
/*算法:计数排序*/
#include <cstring>
#include <iostream>
using namespace std;
/***************************************************************************
1.计数排序法,仅可用于正整数排序。
2.计数排序的基本思想就是对每一个输入元素x,确定出小于x的元素个数。
有了这一信息,就可以把x直接放到它在最终输出数组中的位置上。
3.例如,如果有17个元素小于x,则x就属于第18个输出位置。
4.当排序的数较为稠密时,所需的辅助空间较小。
****************************************************************************/
bool ctsort(int A[], int size) {
if (NULL == A || size < 0)
return false;
/*寻找数组A中的最大整数k*/
int k = A[0];
for (int i = 1; i < size; i++)
if (A[i] > k)
k = A[i];
k++; // 最大整数+1
// 申请数组,使counts数组最大下标对应A中的最大整数
int* counts = new int[k];
int* tmp = new int[size]; // 存放排序的结果
// 初始化counts数组
for (int i = 0; i < k; i++)
counts[i] = 0;
// counts[i]表示数字i在data中的个数
for (int i = 0; i < size; i++)
counts[A[i]] = counts[A[i]] + 1;
// 将counts累计起来,这样counts[i]表示小于等于i的元素个数
for (int i = 1; i < k; i++)
counts[i] = counts[i] + counts[i - 1];
// 从后往前开始数A中元素排序
for (int i = size - 1; i >= 0; i--) {
// counts[data[j]]表示当前小于等于数据data[j]的元素个数,counts[data[j]]
// -1则将其转换为temp中的位置
tmp[counts[A[i]] - 1] = A[i];
counts[A[i]] = counts[A[i]] - 1;
}
memcpy(A, tmp, size * sizeof(int));
delete[] counts;
delete[] tmp;
return true;
}
int main() {
int A[] = {9, 3, 7, 4, 5, 2, 1}; // 测试用例1
// int A[] = {9,3,7,4,5,2,7};//测试用例2
ctsort(A, 7);
for (int i = 0; i < 7; i++)
cout << A[i] << " ";
}