👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:【C/C++】算法
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵
希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、直接插入排序
- 1.1 基本思想
- 1.2 算法思路
- 1.3 代码实现
- 1.4 特性总结
- 二、希尔排序
- 2.1 希尔排序的由来
- 2.2 算法思路
- 2.3 如何选择gap
- 2.4 希尔排序效率问题
- 2.5 代码实现
- 2.6 希尔排序的特性总结
- 三、直接选择排序
- 3.1 基本思想
- 3.2 算法思路
- 3.3 代码实现
- 3.4 优化版本
- 3.5 特性总结
- 四、堆排序
- 五、冒泡排序
- 5.1 思想
- 5.2 算法分析
- 5.3 代码实现(未优化版)
- 5.4 代码实现(优化版)
- 六、快速排序
一、直接插入排序
1.1 基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
实际中,玩扑克牌时,整理一副牌从小到大或者从大到小就用到了插入排序的思想
1.2 算法思路
首先数据内的第一个元素(下标为0)是不需要排序的。因此,当插入第
i
(i ≥ 1)个元素时,可以拿a[i]
与当前数组内的最后一个元素进行比较,若a[i] < 数组最后一个元素
,则将原来的位置上的元素向后移;若满足条件,则直接插入。
1.3 代码实现
#include <stdio.h>
// n - 数组元素个数
void InsertSort(int* a, int n)
{
//数组第一个元素不需要参与排序
for (int i = 1; i < n; i++)
{
// end - 每一次数组最后一个元素的下标
int end = i - 1;
//InsertNum - 要插入的元素
int InsertNum = a[i];
while (end >= 0)
{
if (a[end] > InsertNum)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
//当循环来到此处有两种可能性
//1.break跳出来的
//2.头插
a[end + 1] = InsertNum;
}
}
int main()
{
int a[] = { 9,4,2,3,10,8,6,1,7,5 };
int ArraySize = sizeof(a) / sizeof(a[0]);
InsertSort(a, ArraySize);
for (int i = 0; i < ArraySize; i++)
printf("%d ", a[i]);
return 0;
}
【运行结果】
1.4 特性总结
- 时间复杂度
最好情况:原数组已经是按需求有序了,每趟只需与前面的有序元素序列的最后一个元素进行比较,总比较次数为n - 1
,时间复杂度为O(N)
;
最坏情况:假设要排升序,原数组是逆序的情况下,O(N2)
综上,直接插入排序的时间复杂度为O(N2)- 空间复杂度:
O(1)
- 稳定性:插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。如果碰见一个和插入元素相等的,那么将会把待插入元素放在相等元素的后面。所以,相等元素的相对的前后顺序没有改变,所以插入排序是稳定的。
二、希尔排序
2.1 希尔排序的由来
从直接插入排序中,(目标数组是升序)我们总结了:当原数组是降序的时候,时间复杂度为O(N2),效率极低;当原数组是接近升序的,那么时间复杂度就是O(N),效率最高。因此,又一位名叫希尔的大佬发现,如果一开始就让数组内的元素接近有序的话,插入排序的效率不就大大提升了吗?所以,希尔排序是直接插入排序的优化
2.2 算法思路
- 首先,预排序,目的:让数组内的元素接近有序
如何使数组接近有序?
先选定一个间隔gap
的整数,然后以距离为gap
数进行分组排序,其分组排序还是运用到直接插入排序
- 最后对有序数组进行插入排序即可
当间隔
gap == 1
时,也就是说,距离为1的分为一组,那不就是插入排序
【演示】
(数据来源于后面的代码)
通过以上图片,我们还可以总结一个规律:gap为几,就代表有几组
2.3 如何选择gap
gap
的取法有多种。最初希尔大佬提出取gap = n / 2
,gap =gap / 2
,直到gap =1
,后来 Knuth提出取gap = gap / 3 + 1
,还有人提出取奇数好,也有人提出gap
互质好。但无论哪一种主张都没有得到证明。但是在算法思路中,图中的gap
取值是根据希尔大佬来的,而我在代码实现中给定的是Knuth的方法
2.4 希尔排序效率问题
有人想:希尔排序在预排序的时候不是运用到很多的插入排序,为什么其效率还是比插入排序高?
原因是:其实gap
的取值决定数组内的元素是否接近有序,gap
越大,排的也越快,但越不接近有序;gap
越小,排的也就越慢,但越接近有序。所以一开始gap
的值可以设为数组元素个数(gap一定不可能超过数组元素个数),每次进行/2
,不断缩小gap
,其实最后发现,希尔排序的插入排序的次数其实是小于直接排序的插入次数的。
2.5 代码实现
#include <stdio.h>
//n - 数组元素个数
void ShellSort(int* a, int n)
{
//gap最大也只能取到数组元素个数
int gap = n;
while (gap > 1)
{
//gap的取值无论是多少,最后结果一定是1
//那么当gap=1时,就是最后的插入排序
gap = gap / 3 + 1;
//gap为几,就代表有几组
for (int i = 0; i < gap; i++)
{
//以下代码和插入排序差不多
for (int j = i; j < n - gap; j += gap)
{
int end = j;
int InsertNum = a[end + gap];
while (end >= 0)
{
if (a[end] > InsertNum)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = InsertNum;
}
}
}
}
int main()
{
int a[] = { 9,4,2,3,10,8,6,1,7,5 };
int ArraySize = sizeof(a) / sizeof(a[0]);
ShellSort(a, ArraySize);
for (int i = 0; i < ArraySize; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
【结果展示】
2.6 希尔排序的特性总结
1. 希尔排序是对直接插入排序的优化
2. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算。Knuth进行了大量的试验统计,计算出希尔排序的时间复杂度大约为O(N1.3)但是我们可以用代码进行性能测试的对比
如图,当数据个数由10w时,性能结构如下图所示
三、直接选择排序
3.1 基本思想
每一次从原数组的元素中选出最大(降序)/ 最小(升序)的一个元素,放在序列的起始位置,直至全部待排序的数据全部排完。
3.2 算法思路
【未优化版(升序)】
如上动图所示,定义一个left
表示数组的首元素,假设它是数组中最小的,然后以首元素的大小为基准值再后遍历数组,找到其最小的放在左边,重复上述步骤,直到集合剩余一个元素为止
3.3 代码实现
//【升序为例】
#include <stdio.h>
void Swap(int* p1, int* p2)
{
int t = *p1;
*p1 = *p2;
*p2 = t;
}
void select_sort(int* a, int n)
{
int left = 0;
//LastIdex - 数组最后一个元素的下标
int LastIdex = n - 1;
while (left < LastIdex)
{
//假设数组开头的元素是最小的
int Min = left;
//往后遍历每一个元素
for (int i = left + 1; i <= LastIdex; i++)
{
if (a[i] < a[Min])
Min = i;
}
//循环结束后,Min一定是整个数组中最小元素的下标
//交换到开头
Swap(&a[left], &a[Min]);
left++;
}
}
int main()
{
int a[] = { 10,1,6,9,4,7,2,3,8,5 };
int aSize = sizeof(a) / sizeof(a[0]);
select_sort(a, aSize);
for (int i = 0; i < aSize; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
【结果展示】
3.4 优化版本
【算法思路】
- 定义
left
和right
分别表示数组的头和尾- 每次遍历
left~right
区间里的元素,找到最小的数与left
为下标的元素进行交换;同理,找到最大的元素与以right
为下标的元素进行交换,完成交换后分别缩小left
和right
之间的范围- 重复上述步骤,直到集合剩余一个元素为止
【代码实现】
#include <stdio.h>
void Swap(int* p1, int* p2)
{
int t = *p1;
*p1 = *p2;
*p2 = t;
}
void select_sort2(int* a, int n)
{
int left = 0, right = n - 1;
while (left < right)
{
int Max = left, Min = left;
for (int i = left + 1; i <= right; i++)
{
//更新
if (a[i] > a[Max])
{
Max = i;
}
if (a[i] < a[Min])
{
Min = i;
}
}
//交换
Swap(&a[left], &a[Min]);
//可能存在left和Max重叠
if (left == Max)
{
Max = Min;
}
Swap(&a[right], &a[Max]);
//缩小区间
left++;
right--;
}
}
int main()
{
int a[] = { 10,1,6,9,4,7,2,3,8,5 };
int aSize = sizeof(a) / sizeof(a[0]);
select_sort2(a, aSize);
for (int i = 0; i < aSize; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
【结果展示】
分析
left
和Max
重叠的情况
若没有修正的那一步,交换完a[left]
和a[Min]
后,Max
就不再指向最大的元素了。因此,当left
和Max
重叠时,就要修正Max
的位置
【优化版与未优化版性能比较】
【结果展示】
3.5 特性总结
1. 选择复杂度无论好坏都是O(N2),效率极低,实际中很少使用
2. 空间复杂度:O(1)
3. 稳定性:不稳定
四、堆排序
//堆排序
#include <stdio.h>
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;//最后一个元素
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
int main()
{
int a[] = { 4,7,10,8,1,5,2,3 };
int Asize = sizeof(a) / sizeof(a[0]);
HeapSort(a, Asize);
for (int i = 0; i < Asize; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
堆排序在上篇博客中详细分析过了,这里就不再赘述了 —> [点击跳转]
五、冒泡排序
5.1 思想
两两相邻的元素进行比较,有可能需要交换
5.2 算法分析
N
个数字要排序,总共进行N - 1
趟排序,每x
趟的排序次数为(N-x)
次
5.3 代码实现(未优化版)
#include <stdio.h>
void Swap(int* p1, int* p2)
{
int t = *p1;
*p1 = *p2;
*p2 = t;
}
void bubble_sort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
// 排升序
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
}
}
}
}
int main()
{
int a[] = { 10,1,6,9,4,7,2,3,8,5 };
int aSize = sizeof(a) / sizeof(a[0]);
bubble_sort(a, aSize);
for (int i = 0; i < aSize; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
【程序结果】
【程序特性分析】
- 未优化版的时间复杂度最好(升序)和最坏(降序)的情况都需要遍历一遍数组,因此时间复杂度为:O(N2)
- 空间复杂度:O(1)
- 稳定性:稳定
5.4 代码实现(优化版)
优化版是对有序序列进行了特判,如果第一次遍历一遍数组发现内部根本没有进行交换,就代表其有序
#include <stdio.h>
#include <stdbool.h>
void Swap(int* p1, int* p2)
{
int t = *p1;
*p1 = *p2;
*p2 = t;
}
void bubble_sort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
bool exchage = false;
for (int j = 0; j < n - 1 - i; j++)
{
// 排升序
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
exchage = true;
}
}
//若没发生交换,则有序
if (exchage == false)
{
break;
}
}
}
int main()
{
int a[] = { 10,1,6,9,4,7,2,3,8,5 };
int aSize = sizeof(a) / sizeof(a[0]);
bubble_sort(a, aSize);
for (int i = 0; i < aSize; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
【程序结果】
【程序特性分析】
对于时间复杂度的优化,假设数组原本就有序(最好),因此只用遍历一遍数组,最好的时间复杂度为O(N),但最坏的时间复杂度还是O(N2)
六、快速排序
博主还在更新中~,4_28上线hh