🔥个人主页:艾莉丝努力练剑
❄专栏传送门:《C语言》、《数据结构与算法》、C语言刷题12天IO强训、LeetCode代码强化刷题
🍉学习方向:C/C++方向
⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平
重要提醒:为什么我们要学那么多的数据结构?这是因为没有一种数据结构能够去应对所有场景。我们在不同的场景需要选择不同的数据结构,所以数据结构没有谁好谁坏之分,而评估数据结构的好坏要针对场景,如果在一种场景下我们需要频繁地对头部进行插入删除操作,那么这个时候我们用链表;但是如果对尾部进行插入删除操作比较频繁,那我们用顺序表比较好。
因此,不同的场景我们选择不同的数据结构。
前言:本篇文章,我们正式开始介绍排序相关的知识点,在初阶的数据结构与算法阶段,我们把知识点分成三部分,复杂度作为第一部分,顺序表和链表、栈和队列、二叉树为第二部分,排序为第二部分,我们之前已经介绍完了第一部分:算法复杂度和第二部分:顺序表和链表、栈和队列、二叉树。本文我们将正式开始学习第三部分中的排序的内容啦。
目录
正文
排序的概念(以及应用)
1、概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
2、应用
比如说购物筛选排序,有按价格高低进行排序的、有按销量高低进行排序的、有按品牌进行排序的,也有按产品上市时间的新旧进行排序的。如下图的平板价格:
再比如院校的排名高低,也体现了排序的应用:
3、常见排序算法
常见排序算法如下图:
除此之外,还有非比较的排序算法——计数排序。
对比排序性能:测试代码
代码演示:
//测试排序的性能对比
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];
}
//begin和end的时间差就是
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),1000 ms = 1s,和算法的时间复杂度挂钩。
一. 比较排序
接下来,我们来实现一下刚才罗列出的这些排序算法——
一、插入排序
大家玩过扑克牌吗?应该都玩过吧!每次发牌之后,我们在整理手头的牌的时候就用到了插入排序的思想。这样打起来一目了然。
(一)直接插入排序
1、概念认识
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
如下图:
2、代码实现
代码演示:
//1)随机插入排序 最差情况:O(N^2) 最好情况:O(N)
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--;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
直接插入排序的时间复杂度:O(n^2) 。
3、直接插入排序的时间复杂度
4、直接插入排序的特性
介绍完直接插入排序,我们来简单总结一下它的特性——
直接插入排序的特性总结:
1、元素集合越接近有序,直接插入排序算法的时间效率越高;
2、时间复杂度:O(N^2) ;
3、空间复杂度:O(1)。
不过虽然有所谓的最好情况O(N),但这种情况非常少见。
我们的老朋友——冒泡排序的时间复杂度就是O(N^2)。
可见这个时间复杂度不好,我们能不能优化一下呢?哎,我们下面要介绍的另一种插入排序的方法——希尔排序——就堂堂登场了!
(二)希尔排序(缩小增量排序)
1、概念
毕竟人家是在直接插入排序的基础上改进的,综合来说它的效率肯定是要高于直接插入排序的,不然还叫什么改进呀!因此希尔排序时间复杂度优于直接插入排序。
希尔排序的概念:
我们通过展开这个图介绍一下希尔排序的思想——
2、代码实现
理解了希尔排序的思想,接下来我们就可以尝试写一下代码了——
代码演示:
//2)希尔排序
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;
}
}
}
我们把代码截出来放到画图软件上看一下——
3、希尔排序的时间复杂度计算
接下来我们来思考一下:希尔排序的时间复杂度是多少?
希尔循环有循环的嵌套,我们分外层循环和内层循环来展开分析一下——
外层循环:
外层循环的时间复杂度可以直接看出来:O(logn)(2为底)或者O((logn)(3为底),即O(logn)。
内层循环:
我们假设n = 9,若gap为3,则一共是gap组,每组有(n/gap)个数据。
通过我们上述的分析,可以画出这样的一个曲线图:
上图中的横轴是gap,纵轴是一趟的比较次数。
因此,希尔排序在最初和最后的排序的次数都为n,即前一阶段排序次数是逐渐上升的状态,当到达某一个顶点时,排序次数逐渐下降至n,而该顶点的计算暂时无法给出具体的计算过程。
希尔排序时间复杂度不好计算,因为 gap 的取值非常多,导致我们很难去计算时间复杂度,因此很多书中给出的希尔排序的时间复杂度都不固定。
这里我们参考严蔚敏老师在他的《数据结构(C语言版)》一书中给出的时间复杂度:O(n^1.3)。
4、希尔排序的特性
1、希尔排序是对直接插入排序的优化;
2、当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的 了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比;
3、希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N^1.3— N^2);
4、稳定性:不稳定。
二、选择排序
我们先来看一下选择排序的基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
(一)直接选择排序
1、概念介绍
1、在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素;
2、若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素进行交换;
3、在剩余的 array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。
2、代码实现
如果maxi出现在begin的位置,我们就要特殊处理一下:
代码演示:
//1)直接选择排序 O(n^2)
void SelectSort(int* arr, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1; i <= end; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
//maxi mini begin end
if (maxi == begin)
{
maxi = mini;
}
Swap(&arr[begin], &arr[mini]);
Swap(&arr[end], &arr[maxi]);
begin++;
end--;
}
}
直接选择排序时间复杂度:O(n^2)。
3、直接选择排序的特性
直接选择排序的特性总结:
1、直接选择排序思考虽然非常好理解,但是因为效率不是很好。实际中我们很少去使用;
2、时间复杂度:O(N ^ 2);
3、空间复杂度:O(1)。
4、稳定性:不稳定
(二)堆排序
1、再次介绍
传送门:【数据结构与算法】数据结构初阶:详解二叉树(五)——链式结构二叉树(下):二叉树的链式结构的实现
简单回顾一下:堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。
这里需要注意的是排升序要建大堆,排降序建小堆。
数据结构堆的回顾:
我们也回顾一下建堆的过程:
我们这里是排升序,要建大堆,要调用向下调整算法(O(N))。
2、几个方法实现
(1)交换——Swap方法
//交换——Swap方法
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
(2)向下调整算法
//向下调整算法
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[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
(3)堆排序实现————使用的是堆结构的思想
//堆排序————使用的是堆结构的思想
void HeapSort(int* arr, int n)
{
//向下调整算法——建堆 O(N)
//乱序数组————建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
////向上调整算法——建堆 N*logN
//for (int i = 0; i < n; i++)
//{
// AdjustUp(arr, i);
//}
//n*logn
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);//logn
end--;
}
}
3、完整代码
堆排序的代码演示:
sort.c:
//交换——Swap方法
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//向下调整算法
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[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆排序————使用的是堆结构的思想
void HeapSort(int* arr, int n)
{
//向下调整算法——建堆 O(N)
//乱序数组————建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
////向上调整算法——建堆 N*logN
//for (int i = 0; i < n; i++)
//{
// AdjustUp(arr, i);
//}
//n*logn
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);//logn
end--;
}
}
test.c:
int main1()
{
//test01();
int arr[100] = { 5,3,9,6,2,4,7,1,8 };
printf("排序之前:\n");
for (int i = 0; i < 6; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
HeapSort(arr, 6);
//BubbleSort(arr, 6);
printf("排序之后:\n");
for (int i = 5; i >= 0; i--)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
堆排序时间复杂度:O(nlog n)。
4、堆排序的特性
1、堆排序使用堆来选数,效率就高了很多;
2、时间复杂度:O(N*logN);
3、空间复杂度:O(1) ;
4、稳定性:不稳定。
三、交换排序
交换排序基本思想:
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
(一)冒泡排序
1、概念重提
冒泡排序是一种最基础的交换排序,而之所以叫做冒泡排序,因为每一个元素都可以像小气泡一样,根据自身大小一点一点向数组的一侧移动。
2、冒泡排序代码实现
这个排序方法是我们的老朋友、老相识了,就不多赘述了,博主这里直接给出代码:
//冒泡排序
void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] < arr[j + 1])
{
Swap(&arr[j], arr[j + 1]);
}
}
}
}
冒泡排序时间复杂度:O(N^2)。
如下图——
3、冒泡排序的特性总结
冒泡排序的特性总结:
1、时间复杂度:O(N ^ 2);
2、空间复杂度:O(1);
3、冒泡排序是一种非常容易理解的排序,具有教学意义;
4、稳定性:稳定。
结尾
下面是博主的碎碎念环节,可以跳过不看(捂脸)!
博主的碎碎念时刻:
从本篇文章开始,我们正式进入排序的学习,数据结构初阶的学习终于来到了尾声,马上我们就将进入C++阶段的学习啦,到时候博主会创建一个新的专栏专门介绍C++的知识点。
我们排序分为比较排序和非比较排序,比较排序在本文中我们就已经介绍了插入排序的直接插入排序和希尔排序、选择排序的直接选择排序和堆排序以及交换排序的冒泡排序,其中冒泡排序是我们的老朋友了,堆排序我们也已经打过几次交道。
下一篇文章我们会介绍交换排序的另一种排序——快速排序,为什么只介绍快速排序呢?因为快速排序还分递归版本和非递归版本,而递归版本因为要找基准值又分Hoare版本、挖空法版本以及Lomuto双指针方法版本三种写法,非递归版本也要用到Lomuto方法,还涉及到前面栈这一数据结构的基础知识,已经干货满满了,而且信息量非常大,因此下一篇文章主要介绍快速排序。再下一篇文章,我们将介绍比较排序中的第四种——归并排序——这种排序方法也分成递归版本和非递归版本,信息量也相当大,丝毫不亚于快速排序。归并排序也要用一篇博客的篇幅来介绍,之后就是排序的第四篇博客,我们将介绍非比较排序——计数排序——又称为鸽巢原理,是对哈希直接定址法的变形应用,还有对排序算法复杂度和稳定性的分析,以及其他关于排序的一些补充,博主的计划是用四篇的篇幅把初阶数据结构与算法部分的排序算法介绍完,具体是四篇还是五篇有待商榷,暂定四篇,计划可能赶不上变化,博主尽可能按计划落实!再次感谢大家的支持和鼓励!
敬请期待!
往期回顾:
【数据结构与算法】数据结构初阶:详解二叉树(六)——二叉树应用:二叉树选择题
【数据结构与算法】数据结构初阶:详解二叉树(五)——链式结构二叉树(下):二叉树的链式结构的实现
【数据结构与算法】数据结构初阶:详解二叉树(四)——链式结构二叉树(上):前中后序遍历、创建一棵链式二叉树
【数据结构与算法】数据结构初阶:详解二叉树(三)——堆(续):向上向下调整算法的证明及时间复杂度、TOP-K问题详解
本期内容需要回顾的C语言知识如下面的截图中所示(指针博主写了6篇,列出来有水字数嫌疑了,就只放指针第六篇的网址,博主在指针(六)把指针部分的前五篇的网址都放在【往期回顾】了,点击【传送门】就可以看了)。
大家如果对前面部分的知识点印象不深,可以去上一篇文章的结尾部分看看,博主把需要回顾的知识点相关的博客的链接都放在上一篇文章了,上一篇文章的链接博主放在下面了:
【数据结构与算法】数据结构初阶:详解顺序表和链表(三)——单链表(上)
结语:本篇文章到这里就结束了,对数据结构的二叉树知识感兴趣的友友们可以在评论区留言,博主创作时可能存在笔误,或者知识点不严谨的地方,大家多担待,如果大家在阅读的时候发现了行文有什么错误欢迎在评论区斧正,再次感谢友友们的关注和支持!