看似不起眼的日复一日,总会在某一天让你看到坚持的意义。云边有个稻草人-CSDN博客
hello,好久不见!

目录
[一. 排序的概念及运用](#一. 排序的概念及运用)
[1. 概念](#1. 概念)
[2. 运用](#2. 运用)
[3. 常见排序算法](#3. 常见排序算法)
[二. 实现常见排序算法](#二. 实现常见排序算法)
[1. 插入排序](#1. 插入排序)
[Relaxing Time !](#Relaxing Time !)
[------------------------------------《We Don't Talk Anymore》------------------------------------](#————————————《We Don't Talk Anymore》————————————)
正文开始------
一. 排序的概念及运用
1. 概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
2. 运用
院校排名

3. 常见排序算法

接下来我们来学习实现这些常见的排序算法。。。
二. 实现常见排序算法
cpp
int a[] = {5, 3, 9, 6, 2, 4, 7, 1, 8};
1. 插入排序
【基本思想】
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

实际中我们玩扑克牌时,就用了插入排序的思想。
(1)直接插入排序
【图解】
当插入第 i (i>=1) 个元素时,前面的 array[0],array[1],...,array[i-1] 已经排好序,此时用 array[i] 的排序码与 array[i-1],array[i-2],... 的排序码顺序进行比较,找到插入位置即将 array[i] 插入 ,原来位置上的元素顺序后移。

这个动图显示了直接插入排序的步骤,里面原始的数据是10,9,8,7,6,5,4,3,2,1,是一个降序序列,现在我们用直接插入排序将其排列成升序序列,细节过程难以用文字描述,我们对照着代码理解。

- 外层 for 循环来控制内层循环的次数。
- for 循环的 i 的最大值为n-2,最后一次循环end指向倒数第二个元素,此时tmp里面存放的是整个序列里面最后一个数据,就是再将最后一个数据插入到前面有序序列里面合适的位置整体就ok了。
- 每一次进入新的for循环就是新的end,新的tmp,各自又分配到了新的数据,重复前面整体的步骤;进入到里面的 while 循环是在为 tmp 在前面的有序序列里面找合适的位置插入进去。
- 每次插入数据都是往end+1的地方插,end+1这个位置留用,谁大谁往这放。
- tmp保存正在排序的数据,不断比较然后赋值的过程中会将tmp原位置的值给覆盖,所以我们保存一份在找到合适的位置后给它放进去。
【代码】
cpp
//直接插入排序
void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;//--是为了让tmp与区间前面的元素进行挨个比较,最后找到合适的位置
}
else
{
break;
}
}
//此时end可能等于-1,或者是因为end指向的元素 < tmp,此时我们还没有进行最后的赋值,
//这时我们都要把tmp里面的数据赋值给arr[end+1]
arr[end + 1] = tmp;
}
}
【直接插入排序的特性总结】

【冒泡排序,堆排序,直接插入排序时间复杂度比较】
冒泡排序代码见下
时间复杂度为O(N^2),空间复杂度为O(1)
cpp
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//冒泡排序
void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
int exchange = 0;
for (int j = 0; j < n - i - 1; j++)
{
//升序
if (arr[j] > arr[j + 1])
{
exchange = 1;
Swap(&arr[j], &arr[j + 1]);
}
}
if (exchange == 0)
{
break;
}
}
}
堆排序代码见下
时间复杂度为O(N * logN),空间复杂度为O(1)
cpp
//向下调整数据
void AdjustDown(int* arr, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n)
{
//找出左右孩子中最小的->小堆 >
//找出左右孩子中最大的->大堆 <
if (child + 1 < n && arr[child] < arr[child + 1])
{
child++;
}
// < 建小堆
// > 建大堆
if (arr[child] > arr[parent])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(int* arr, int n)
{
//向下调整算法建堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
//堆排序
int end = n - 1;
while (end > 0)
{
//end指向的是最后一个数据
Swap(&arr[0], &arr[end]);
//有效的数据个数减了1
AdjustDown(arr, 0, end);
end--;
}
}
直接插入排序代码见下
时间复杂度最好的情况是O(N),最差的情况为O(N^2),最差的情况只有是有序的降序序列时才发生(如果我们要排升序的话)
cpp
//直接插入排序
void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;//--是为了让tmp与区间前面的元素进行挨个比较,最后找到合适的位置
}
else
{
break;
}
}
//此时end可能等于-1,或者是因为end指向的元素 < tmp,此时我们还没有进行最后的赋值,
//这时我们都要把tmp里面的数据赋值给arr[end+1]
arr[end + 1] = tmp;
}
}
光说不行,我们可以借助测试代码来验证这几种排序的性能到底如何------
TestOP函数思路------我们先开辟10W个数据类型大小的空间,然后随机生成10W个数据并将数据依次插入到数组里面,这里面采用了赋值,保证各个数组里面的数据是一样的,这样在利用同样数据的数组排序的时候就会公平统一,之后利用clock函数来计算各个排序运行的时间。下面是clock函数的简单介绍,使用起来还是很简单的,对于TestOP()里面的使用,就是求两次clock函数之间的时间差就是排序所用的时间。

cpp
// 测试排序的性能对⽐
//下面我们还没有实现的排序求时间我们就先注释掉
void TestOP()
{
srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
/*int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);*/
int* a4 = (int*)malloc(sizeof(int) * N);
/*int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);*/
int* a7 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
/*a2[i] = a1[i];
a3[i] = a1[i];*/
a4[i] = a1[i];
/*a5[i] = a1[i];
a6[i] = a1[i];*/
a7[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
/*int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();*/
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
//int begin5 = clock();
//QuickSort(a5, 0, N - 1);
//int end5 = clock();
//int begin6 = clock();
//MergeSort(a6, N);
//int end6 = clock();
int begin7 = clock();
BubbleSort(a7, N);
int end7 = clock();
printf("InsertSort:%d\n", end1 - begin1);
/*printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);*/
printf("HeapSort:%d\n", end4 - begin4);
/*printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);*/
printf("BubbleSort:%d\n", end7 - begin7);
free(a1);
/*free(a2);
free(a3);*/
free(a4);
/*free(a5);
free(a6);*/
free(a7);
}
下面我们来看一下运行结果

运行结果的单位是ms,对于相同的10W数据直接插入排序时间是3s多,而堆排序还要小得多,但是冒泡排序是接近40s,可见冒泡排序的效率之低(冒泡排序的作用仅仅只是教学意义,让一开始菜鸟的我学习循环嵌套啥的)。堆排序效率确实很高,直接插入排序也还不错,下面我们可以再对直接插入排序进行优化,就变成了希尔排序。
(2)希尔排序
希尔排序法又称 缩小增量法 。希尔排序法的基本思想是:先选定⼀个整数(通常是gap = n/3+1),把待排序文件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进行排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进⾏插入排序,当gap=1时,就相当于直接插入排序。 它是在直接插⼊排序算法的基础上进⾏改进而来的,综合来说它的效率肯定是要⾼于直接插⼊排序算法的。下图是大致的排序思路:

【思路代码图解】

【优化】
注意gap的变化,gap = gap / 3 +1。

【希尔排序代码】
cpp
//希尔排序
//时间复杂度为O(N^1.3)
void ShellSort(int* arr, int n)
{
int gap = n;
//最外层循环来控制每组元素的间隔
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
我们还是调用上面的测试代码来看一下运行时间,结果显示希尔排序 相对于直接插入排序确实提高了不少,赞!

下去我们可以试着测试,对于同样的降序序列,直接插入排序和希尔排序各自的运行效率怎么样,可以先使用冒泡排序搞一个降序序列。
【希尔排序时间复杂度】

经过不断地预排序,小的数据基本在左边,大的数据基本在右边,在以后gap==1时,数组已经接近有序,时间复杂度不可能达到n^2,应该是接近O(n)。总的下来希尔排序的时间复杂度接近O(n^1.3)。
**为什么gap = gap / 3 + 1取3个为一组呢?**假设有10个数据,我们看一下取3个为一组和2个为一组的区别:
gap / 3 + 1 4---->2---->1
gap / 2 + 1 6---->4---->3---->2---->1
显而易见,当取三个为一组的话外层循环的次数比较少的就可以达到gap==1,两种方法相比每组分到的数据个数相差不大,循环次数越多,时间复杂度就越高
通过上面的分析我们可以画出下面的曲线图
因此,希尔排序在最初和最后的排序的次数都为n,即前⼀阶段排序次数是逐渐上升的状态,当到达某⼀顶点时,排序次数逐渐下降⾄n,⽽该顶点的计算暂时⽆法给出具体的计算过程。
希尔排序时间复杂度不好计算,因为 gap 的取值很多,导致很难去计算,因此很多书中给出的希尔排序的时间复杂度都不固定。《数据结构(C语⾔版)》--- 严蔚敏书中给出的时间复杂度为:
2.选择排序
【 基本思想 】
每⼀次从待排序的数据元素中选出最⼩(或最⼤)的⼀个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
(1)直接选择排序
- 在元素集合 array[i]--array[n-1] 中选择关键码最⼤(⼩)的数据元素;
- 若它不是这组元素中的最后⼀个(第⼀个)元素,则将它与这组元素中的最后⼀个(第⼀个)元素;
- 在剩余的 array[i]--array[n-2] ( array[i+1]--array[n-1] ) 集合中,重复上述步骤,直到集合剩余 1 个元素。
【代码】
cpp
//直接插入排序
void SelectSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
//在后面区间内找maxi,mini
for (int j = begin+1; j <= end; j++)
{
if (arr[j] < arr[mini])
{
mini = j;
}
if (arr[j] > arr[maxi])
{
maxi = j;
}
}
if (maxi == begin)
{
maxi = mini;
}
Swap(&arr[begin], &arr[mini]);
Swap(&arr[end], &arr[maxi]);
begin++;
end--;
}
}
看一下运行结果是否正确,对了!

【直接选择排序的特性总结】
- 直接选择排序比较好理解,但是效率不好,实际中很少使用,那为啥还要学呢,为了你知道这是最差的排序方式(哈哈)
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
(2)堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的⼀种排序算法,它是选择排序的⼀种。它是通过堆来进行选择数据。需要注意的是排升序要建⼤堆,排降序建⼩堆。在⼆叉树章节我们已经实现过堆排序,这里不再赘述,我之前写了一篇详细的关于堆排序的博客,下面是博客链接:
【 【数据结构初阶第十五节】堆的应用(堆排序 + Top-K问题)-CSDN博客 】
3.交换排序
(1)冒泡排序
前⾯在算法题中我们已经接触过冒泡排序的思路了,冒泡排序是⼀种最基础的交换排序。之所以叫做冒泡排序,因为每⼀个元素都可以像小气泡⼀样,根据自身大小⼀点⼀点向数组的⼀侧移动。

【代码实现】
cpp
//冒泡排序
void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
int exchange = 0;
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] < arr[j + 1])
{
exchange = 1;
Swap(&arr[j], &arr[j + 1]);
}
}
if (exchange == 0)
{
break;
}
}
}
【冒泡排序特性总结】
时间复杂度:O(N^2)
空间复杂度:O(1)
(2)快速排序
快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序实现主框架:
cpp
//快排
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
//1.找基准值
int key = _QuickSort(arr,left,right);
//2.左子序列进行排序
QuickSort(arr, left, key - 1);
//3.右子序列进行排序
QuickSort(arr, key + 1, right);
}
将区间中的元素进⾏划分的 _QuickSort ⽅法主要有以下⼏种实现⽅式:
【Hoare版本】

【细节问题】

【代码】
cpp
//找基准值
int _QuickSort(int* arr, int left, int right)
{
int key = left;
left++;
while (left <= right)
{
//找比基准值大的
while (left <= right && arr[left] < arr[key])
{
left++;
}
//找比基准值小的
while (left <= right && arr[right] > arr[key])
{
right--;
}
if (left <= right)
{
Swap(&arr[right--], &arr[left++]);
}
}
//将right指向的数据和key指向的数据进行交换
Swap(&arr[key], &arr[right]);
return right;
}
//快排
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
//1.找基准值
int key = _QuickSort(arr,left,right);
//2.左子序列进行排序
QuickSort(arr, left, key - 1);
//3.右子序列进行排序
QuickSort(arr, key + 1, right);
}
效果展示:

我们再根据代码来一遍流程,看看代码内部到底是怎么运行的
【代码图解】

问题1:为什么跳出循环后right位置的值⼀定不⼤于key?
当 left > right 时,即right⾛到left的左侧,⽽left扫描过的数据均不⼤于key,因此right此时指向的数据⼀定不⼤于key
问题2:为什么left 和 right指定的数据和key值相等时也要交换?
相等的值参与交换确实有⼀些额外消耗。实际还有各种复杂的场景,假设数组中的数据⼤量重复时,⽆法进⾏有效的分割排序。
对于同样的10W个数据,我们来看一下这几个排序时间,见下:

我们把数据上调到100W个,来看一下这几个排序的时间,快排确实有点东西

今天就先学那么些,剩下的明天继续更!
完------
Relaxing Time !
------------------------------------《We Don't Talk Anymore》------------------------------------
"为你,千千万万遍" ------哈桑
We Don't Talk Anymore_Charlie Puth、Selena Gomez_高音质在线试听_We Don't Talk Anymore歌词|歌曲下载_酷狗音乐

至此结束------
我是云边有个稻草人
期待与你的下一次相遇!