🏆个人主页:企鹅不叫的博客
🌈专栏
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
💙系列文章💙
【初阶数据结构与算法】第一篇:算法中的时间复杂度和空间复杂度
【初阶数据结构与算法】第六篇:栈和队列(各个功能实现+练习题包含多种方法)
【初阶数据结构与算法】第七篇:二叉树和堆的基本概念+以及堆的实现
【初阶数据结构与算法】第八篇——二叉树的顺序结构的应用(堆排序+TOPK问题)
【初阶数据结构与算法】第九篇——二叉树(链式结构实现+四种遍历方式+基本操作实现+基本练习详解)
前言
🌏一、排序介绍
🍯1.排序概念
⭐️ 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
⭐️ 排序的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。⭐️内部排序 :数据元素全部放在内存中的排序。
⭐️外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
🍯2.排序分类
🌏二.插入排序
🍯1.直接插入排序
🍍基本思想
⭐️现在有一个有序的区间,我们插入一个数据,保持它依旧有序
⭐️一般地,我们把第一个看作是有序的,所以我们可以从第二个数开始往前插入,使得前两个数是有序的,然后将第三个数插入直到最后一个数插入。
🍍实现过程
⭐️单趟排序:首先选中end+1下标位置的数据存放到tmp中之后依次从end位置开始向前比较,直到end小于0,每个数进行比较,最后将tmp插入即可
⭐️整合: 总共有n个数,所以需要排序n-1次
在最后一趟开始前,所有元素不一定在最终位置上,例如
给出数列45 80 48 40 22 78
第一趟结果45 80 48 40 22 78(排第一个元素)
第二趟结果45 80 48 40 22 78(排第一第二个元素)
第三趟结果45 48 80 40 22 78(排第一第二个元素第三个元素)
void InsertSort(int* a, int n) {
//有n个数据只用n-1趟排序
for (int i = 0; i < n - 1; ++i) {
//单趟排序
int end = i;
//待插入的数据
int tmp = a[end + 1];
//依次往前移动
while (end >= 0) {
//依次比较
//降序只需要将下面改成 < 即可
if (a[end] > tmp) {
a[end + 1] = a[end];
end--;
}
else {
//当插入的数据是最小值的话,end移动到0位置处理完后,所有数据都往后移完了
//那下一次end为-1时,结束循环没有成功插入数据
//a[end + 1] = tmp;
break;
}
a[end + 1] = tmp;
}
}
}
🍍时间复杂度、空间复杂度、稳定性分析
⭐️时间复杂度:
O ( N 2 ) O(N^2) O(N2)
第一趟end最多往前移动1次,第二趟是2次……第n-1趟是n-1次,所以总次数是1+2+3+……+n-1=n*(n-1)/2,所以说时间复杂度是O(N2)最好情况:O(N)顺序
最坏的情况:O(N2)逆序
⭐️空间复杂度:
O ( 1 ) O(1) O(1)
没有开辟额外空间⭐️稳定性:
直接插入排序在遇到相同的数时,可以不移动,就可以保持稳定性了,所以说这个排序是稳定的。
🍯2.希尔排序
🍍基本思想
⭐️希尔排序是建立在直接插入排序之上的一种排序,希尔排序的思想上是把较大的数尽快的移动到后面,把较小的数尽快的移动到前面。所以先预排序使数据接近有序,然后再直接插入排序。
⭐️先选定一个整数,把待排序数列中所有记录分成多个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。(直接插入排序的步长为1),这里的步长不为1,而是大于1,我们把步长这个量称为gap,当gap>1时,都是在进行预排序,当gap==1时,进行的是直接插入排序。
⭐️如果gap越小,说明,数组越接近有序,如果gap越大,大的数据可以更快的到后面,小的数据可以更快的到前面,然是整个数组越不接近有序
🍍实现过程
⭐️单趟排序,和直接插入差不多,原来是gap == 1,现在是gap了。
//单组
int end = 0;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
⭐️首先对于每一组进行排序,之后再进行下一组排序
// gap组
for (int j = 0; j < gap; j++)
{
int i = 0;
for (i = 0; i < n-gap; i+=gap)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
⭐️所有数据一起排序,不再是一组一组数据排序,少嵌套一层循环
// 一起预排序
int i = 0;
//最后一个数据下标是n-1,那么我们只要到n-1-gap下标循环后就截止
for (i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
//将所有间隔gap的数据排列
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
⭐️关于gap取值,当gap=1时,直接插入排序,当gap > 1时,预排序,并且gap越大,预排序越快,排序结果越不接近有序,gap越小,排序越慢,预排序后越接近有序
⭐️对于控制gap,我们可以让最初的gap控制为n,最后一次gap控制为1就可以了,我们可以gap /= 2(最后一个是偶数,也是1),也可以
g a p = g a p / 3 + 1 gap = gap / 3+1 gap=gap/3+1
加1是为了,防止gap为0
void ShellSort(int* a, int n) {
int gap = n;
//注意gap>1,可以保证最后一次是2
while(gap > 1){
gap = gap / 3 + 1;
//最后一个数据下标是n-1,那么我们只要到n-1-gap下标循环后就截止
for (int i = 0; i < n - gap; ++i) {
int end = i;
int tmp = a[end + gap];
while (end >= 0) {
if (a[end] > tmp) {
a[end + gap] = a[end];
end -= gap;
}
else {
break;
}
}
a[end + gap] = tmp;
}
}
}
🍍时间复杂度、空间复杂度、稳定性分析
⭐️时间复杂度:
O ( N 1.3 ) O(N^{1.3}) O(N1.3)当gap很大的时候几乎都跳到后面去了,差不多时O(N),很小差不多也是O(N)
平均下来是O(N1.3)
⭐️空间复杂度:
O ( 1 ) O(1) O(1)
⭐️稳定性分析:相同的数,可能被分到不同的gap当中,不稳定。
🌏三.选择排序
🍯1.直接选择排序
🍍基本思想
⭐️ 每次从数组中选择最大的一个数和最小的一个数,把他们放到开头和结尾,然后再取次大的一个数和次小的一个数,放到开头第二或者结尾第二,依次这样进行,直到只剩下一个元素或者没有。
🍍实现过程
⭐️首先选出第一个元素left和最后一个元素right,同时从两头二分数组,创建当前最小下标和最大下标,选出最小下标mini和最大下标maxi,然后交换left下标的值和mini下标的值,再交换right下标的值和maxi下标的值的时候,要判断left下标是否和maxi下标相等,防止接下来的交换将maxi掉包
和直接插入不同的是,每趟排列会选出最小的放前面,第一趟选出最小的放最前面,第二趟选出次小的放第二个位置
void SelectSort(int* a, int n){
int left = 0, right = n - 1;
while (left < right) {
//初始化记录最大下标和最小下标
int mini = left, maxi = left;
//i从left+1开始,是因为mini从left开始了,然后两边都是闭区间
for (int i = left + 1; i <= right; ++i) {
//如果i下标对应的数比mini下标对应数大,则交换
if (a[mini] > a[i]) {
mini = i;
}
如果i下标对应的数比maxi下标对应数小,则交换
if (a[maxi] < a[i]) {
maxi = i;
}
}
Swap(&a[left], &a[mini]);
//left和maxi重叠,说明maxi被换到mini原来的位置上去了,修正一下maxi即可
//防止left和maxi相等时,mini与left交换会导致maxi的位置发生变化
if (left == maxi) {
maxi = mini;
}
Swap(&a[right], &a[maxi]);
right--;
left++;
}
}
🍍时间复杂度、空间复杂度、稳定性分析
⭐️时间复杂度:
O(N^2)
第一趟遍历n-1个数,选出两个数,第二趟遍历n-3个数,选出两个数……最后一次遍历1个数(n为偶数)或2个数(n为奇数),所以总次数是n-1+n-3+……+2,所以说时间复杂度是O(n^2)
⭐️空间复杂度:
O(1)
⭐️稳定性分析:
下面就是红色的5和黑色的5相对顺序变了不稳定。
🍯2.堆排序(详细介绍点这里)
🍍基本思想
⭐️首先建立堆(升序建大堆,降序建小堆),然后调整数据
🍍实现过程
void AdjustDown(int* a, size_t size, size_t root)
{
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
// 1、选出左右孩子中小的那个,注意,child+1 < size要写在前面,防止越界
if (child + 1 < size && a[child + 1] > a[child])
{
++child;
}
// 2、如果孩子小于父亲,则交换,并继续往下调整
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
// 向下调整--建堆 O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
for(int i = n-1; i > 0; --i){
swap(a[0], a[i]);
AdjustDown(a, i, 0);
}
//size_t end = n - 1;
//while (end > 0)
//{
// Swap(&a[0], &a[end]);
// AdjustDown(a, end, 0);
// --end;
//}
}
🍍时间复杂度、空间复杂度、稳定性分析
⭐️时间复杂度:
O ( N l o g N ) O(NlogN) O(NlogN)
⭐️空间复杂度:
O ( 1 ) O(1) O(1)
⭐️稳定性分析:升序大堆,两个数都是8,之后最上面8插入到最后一个顺序就颠倒了不稳定
🌏四.交换排序
🍯1.冒泡排序
🍍基本思想
⭐️以升序为例,每一趟的冒泡排序都是把一个最大的数放到最后面,如果 a[i-1]>a[i],我们将i-1,i的值进行交换,依次循环反复。
🍍实现过程
⭐️用升序举例子首先从下标为1的数开始,依次将前一个数和后一个数交换,之后每一次遍历都会少一个需要遍历的数,单趟冒泡完了,每一个都小于后一个数,那么后面就不需要遍历了
void BubbleSort(int* a, int n) {
//多趟
for (int j = 0; j < n; j++) {
//发生了交换,将flag置1
int flag = 0;
//单趟,从1开始,让前一个与后一个比较
for (int i = 1; i < n - j; ++i) {
if (a[i] < a[i - 1]) {
Swap(&a[i], &a[i - 1]);
flag = 1;
}
}
//没有发生交换,后续不需要再冒泡
if (flag == 0) {
break;
}
}
}
🍍时间复杂度、空间复杂度、稳定性分析
⭐️时间复杂度:
第一趟最多比较n-1次,第二趟最多比较n-2次……最后一次最多比较1次,所以总次数是n-1+n-2+……+1,所以说时间复杂度是O(N2)
最好的情况: O(N)(顺序)
最坏的情况: O(N2)(逆序)⭐️空间复杂度:
O(1),没有开辟额外空间
⭐️稳定性:
冒泡排序在比较遇到相同的数时,不进行交换,这样就保证了稳定性,所以说冒泡排序数稳定的。
🍯2.快速排序(递归版本)
🍍hoare版本
🔑基本思想
⭐️用升序举例子:首先定义一个关键字Key(一般是第一个或者是最后一个),然后将数组第一个定义为begin和最后一个定义为end,end负责找小,如果遇到end比Key小的话,移动begin,找到比Key大的位置,然后交换begin和end,如果begin和end相遇则将相遇的位置与Key交换。
⭐️原则:关键词取左,右边先找小再左边找大;关键词取右,左边先找大再右边找小。可以保证相遇位置比Key小
⭐️一趟单趟排序排完后,此时keyi下标对应的数不用变了,接下来将keyi左边和右边分别分治递归即可,直至左右两边都有序
🔑实现过程
⭐️hoare版本找keyi值代码,注意如果left和right都一样时,right和left也要移动,不忍会死循环,如果是顺序的数组,要判断下标防止越界
//horae
int PartSort1(int* a, int left, int right) {
//将第一个值作为keyi
int keyi = left;
while (left < right) {
//(5,5,2,3,5)
//没有等于的问题是,如果left下标对应的值和right下标对应的值和keyi下标对应的值相等,则会死循环
//(1,2,3,4,5)
//如果没有判断left和right,数组是升序的话,right和left会一直访问直到越界
//注意险些left < right 不然会越界
while (left < right && a[right] >= a[keyi]) {
right--;
}
while (left > right && a[right] <= a[keyi]) {
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
//返回数组第一个元素下标,返回的是left 不是keyi
return left;
}
⭐️快排代码,找出keyi下标,此时不变,将keyi下标分成左右两个区间,依次递归
void QuickSort(int* a, int left, int right) {
//此时区间不可以再分割了,此时区间不存在
if (left >= right) {
return;
}
int keyi = PartSort1(a, left, right);
//[left, keyi-1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
🍍挖坑法
🔑基本思想
⭐️相较于horae法,挖坑发不需要理解为什么,最终相遇的位置比key小,不需要理解为什么左边作为key要右边先走.
⭐️选出第一个数或者最后一个数作为坑位储存起来,包括下标,然后然后从右往左找到比key小的数字,将key替换为此数,然后从左往右找到比key大的数字,然后替换为此数,循环往复,直到right和left相等
🔑实现过程
⭐️将a[left]存到key中,将left下标存到pit中,然后先移动right,交换坑位,之后移动left交换坑位,最后结束循环放入坑位
//挖坑法
int PartSort2(int* a, int left, int right) {
int key = a[left];
//坑位
int pit = left;
while (right > left) {
//(5,5,2,3,5)
//没有等于的问题是,如果left下标对应的值和right下标对应的值和keyi下标对应的值相等,则会死循环
//(1,2,3,4,5)
//如果没有判断left和right,数组是升序的话,right和left会一直访问直到越界
while (right > left && a[right] >= key) {
right--;
}
a[pit] = a[right];
pit = right;
while (right > left && a[left] <= key) {
left++;
}
a[pit] = a[left];
pit = left;
}
a[pit] = key;
return pit;
}
⭐️全趟递归
void QuickSort(int* a, int left, int right) {
//此时区间不可以再分割了,此时区间不存在
if (left >= right) {
return;
}
int keyi = PartSort2(a, left, right);
//[left, keyi-1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
🍍前后指针法
🔑基本思想
⭐️从左边开始,选择第一个数作为keyi,那么pre从下标为0开始,cur从下标为1开始,直到cur到最后一个数结束,cur在前面找小,找到了,prev往前走一步,然后交换pre和cur所在位置的值,然后cur继续找小,直到cur走到空指针的位置就结束,最后将pre的值与key交换就完成了一次分割区间的操作
⭐️如果选择右边作为keyi的话,那么pre起始下标从-1位置开始,cur从0位置开始,循环在right-1位置结束
🔑实现过程
⭐️用第一个数为keyi为例:pre为第一个数下标,cur为第二个数下标,cur一直往后走,直到到right结束,当以cur为下标的值遇到以keyi为下标的值要小,则将pre往后移动一位,同时判断pre如果不等于cur则将cur的值与pre的值交换,之后cur往后走,循环结束后,交换pre下标的值和keyi下标的值,并且返回作为头的pre
//前后指针法
int PartSort3(int* a, int left, int right) {
int keyi = left;
int pre = left;
int cur = left + 1;
//cur走到尾就结束了,需要等于不然最后一组没有测到
while (cur <= right) {
//首先往后找比keyi要小的数
//找到了比keyi小的数就先++pre
//如果pre和cur相等就不交换两个数,如果不相等就交换两个数,防止自己和自己交换
if (a[cur] < a[keyi] && a[++pre] != a[cur]) {
Swap(&a[cur], &a[pre]);
}
//cur接着往下走
cur++;
}
//交换后,此时pre左边比keyi小,右边比keyi大
Swap(&a[pre],&a[keyi]);
//返回值给keyi
return pre;
}
⭐️全趟递归
void QuickSort(int* a, int left, int right) {
//此时区间不可以再分割了,此时区间不存在
if (left >= right) {
return;
}
int keyi = PartSort3(a, left, right);
//[left, keyi-1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
🍍时间复杂度、空间复杂度、稳定性分析
⭐️时间复杂度:
O ( N l o g N ) O(NlogN) O(NlogN)
最好情况每次都选的是中间为key最坏情况是每次选的keyi都是第一个或者是最后一个,那么每次循环都要遍历所有的数**(此时可能会导致栈溢出)**
⭐️空间复杂度:
空间复杂度一般为为O(logN),最坏情况下是O(N),需要进行n‐1递归调用,退化为冒泡排序
⭐️稳定性分析:
黑色key 2 要放到中间去,有点不稳==不稳定==
🍍优化快速排序
🔑选出中间值优化
🌰基本思想和代码实现
⭐️选出不是最大或者最小的函数,因为遇到的数组是随机的,可能有序可能无序,如果是有序,那么三数取中后时间复杂度从最坏变到最好,随机的情况,即最坏的情况也被避免掉了选出数组中间值的下标,然后每次单趟排序时,将中间的值和第一个数交换,下面是选出中间值代码。
int MidIndex(int* a, int left, int right)
{
//防止数据溢出
int mid = left + (right - left) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else //a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
⭐️此时的单趟排序中改变的只是,中间值和第一个数交换了而已
int PartSort1(int* a, int left, int right)
{
//选出中间值下标,然后交换中间值和第一个数
int midi = MidIndex(a, left, right);
Swap(&a[left], &a[midi]);
//最左边的做key为例
int key = left;
while (left<right)
{
//因为我们是最左边的取key,所以必须是右边先走找比key小的,思考下为什么?
//右边先走
while (left < right && a[right] >= a[key])
{
--right;
}
//然后左边走
while (left < right && a[left] < a[key])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[key]);//此时left已经和right相遇,一样的
return left;
}
🔑小区间优化
🌰基本思想和代码实现
⭐️在多次排序中,会多次递归调用,最后区间会被切成很小的一块,但其实,当区间变得很小的时候再去递归效率就会显得很慢,所以我们可以选择其他排序来解决这个问题。
⭐️还有一个我们要思考的问题就是最后这段小区间用什么排序比较好?
希尔排序适应的是比较多的数据才有优势,堆排序需要建堆,其他三个插入排序、选择排序和冒泡排序相比,还是插入排序比较优,所以我们小区间选择用插入排序进行排序。
void QuickSort2(int* a, int begin, int end)
{
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
return;
// 当区间个数小于10时为小区间,小区间直接插入排序控制有序
if (end - begin + 1 <= 10)
{
//确定任意位置的小区间
InsertSort(a + begin, end - begin + 1);
}
else
{
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
}
🍯3.快速排序(非递归版本)
🍍基本思想
⭐️原因:当递归深度过大,可能会造成栈溢出。
⭐️利用栈,首先将要插入的区间头尾放到栈当中,然后循环取出头尾,得到keyi再将[left, keyi-1]和 [keyi+1,right]依次放到栈当中,直到栈为空停止。
🍍实现过程
⭐️首先创建一个栈,然后把我们传过来的区间放到栈当中,然后取出栈当中区间的两头,同时单趟排一遍得到keyi此时keyi左边比keyi小,右边比keyi大,然后判断左右两个区间的边界范围,如果边界相等说明只有一个元素了,就不用再入栈了,依次循环直到栈为空。
void QuickSort2(int* a, int left, int right) {
//创建并且初始化st然后将
ST st;
StackInit(&st);
//将最开始的区间插入到栈当中
StackPush(&st, left);
StackPush(&st, right);
//栈为空,表示排完了
while (!StackEmpty(&st)) {
//取出我们要排序的区间头和尾,注意栈是先入后出
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
//单趟排序得到keyi
int keyi = PartSort3(a, left, right);
//[left, keyi-1] keyi [keyi+1, right]
//当区间越界了就不会再入栈了,说明排到底了
if (left < keyi - 1) {
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
if (keyi + 1 < right) {
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
}
StackDestory(&st);
}
🌏五.归并排序
🍯1.递归实现
🍍相较于其他排序的优势
⭐️前面的排序都是内排序,数据在内存,访问速度快,但是访问量小,下标随机访问,归并排序是外排序,数据在磁盘,访问速度漫,但是访问量大,串行访问。
🍍基本思想
⭐️该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
⭐️其实就是先分治再递归,分成若干个小区间然后再合并。
🍍实现过程
⭐️创建一个临时数组,然后将需要排序的数组和临时数组都传送到子函数当中,首先分治:如果只剩下一个元素或者第一个元素下标超出最后一个元素下标时,返回否则获得中间节点,然后将中间节点的左区间和右区间分别分治。然后归并:将两个区间一起比较,将较小的值放入tmp中,之后将未排序完的数组放入到tmp中,再把tmp拷贝到a中
void _MergeSort(int* a, int left, int right, int* tmp) {
//当区间只剩下一个值(=)或者超出区间(>)的时候,结束返回
if (left >= right) {
return;
}
//得到中间节点
int mid = left + (right - left) / 2;
//分治[left, mid][mid+1, right]
//最好不要[left, mid-1][mid, right],容易左右不均匀
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid+1, right, tmp);
//归并[left, mid][mid+1, right]
//printf("归并[%d,%d][%d,%d]\n", left, mid, mid+1, right);
//两组left和right分别决定归并的左右两组,两边分别让它们有序
int left1 = left, right1 = mid;
int left2 = mid + 1, right2 = right;
//记录tmp下标的
int index = left;
//归并过程是,有一组结束了循环结束
while (left1 < right1 && left2 < right2) {
//升序:将两个区间中较小的一个放到tmp中
if (a[left1] < a[left2]) {
tmp[index++] = a[left1++];
}
else {
tmp[index++] = a[left2++];
}
}
//检查两个区间中剩下的数归并到tmp中
while (left1 <= right1) {
tmp[index++] = a[left1++];
}
while (left2 <= right2) {
tmp[index++] = a[left2++];
}
//拷贝数据从tmp到a,闭区间个数要加1
//+left:每次拷贝不是考全部,只需要拷贝归并的那一段就可以了
memcpy(a+left, tmp+left,(right-left + 1)*sizeof(int));
}
void MergeSort(int* a, int n) {
//创建一个临时数组
int* tmp = (int*)malloc(sizeof(int) * n);
assert(tmp);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
🍍时间复杂度、空间复杂度、稳定性分析
⭐️时间复杂度:
O ( N l o g N ) O(NlogN) O(NlogN)
递归过程中每次都将一组平均分,分完后高度大概是logN⭐️空间复杂度:
O ( N ) O(N) O(N)
要来一个临时空间存放归并好的区间的数据⭐️稳定性分析:
在遇到相同的数时,可以就先将放前一段区间的数,再放后一段区间的数就可以保持稳定性了,所以说这个排序是稳定的.
🍯2.非递归实现
🍍基本思想
⭐️首先区间的间距是1,所以就以间距为1一组一组合并,然后区间间距是2,所以就以间距是2一组一组合并,以此类推,直到所有区间都合并了。
🍍实现过程
⭐️首先创建一个临时数组tmp,一开始gap间距是1,饭后我们定义左区间是[i, i+gap-1],右区间是[i+gap, i+2*gap-1],每一次循环到n-1为止,间隔是跳过两个区间,如果遇到right1越界则修正,left2越界则表示区间不存在,left2正常right2越界则修正right2,接着就是归并的过程,例如升序就将两个区间中较小的值放入tmp中,直到直到两个区间中有一个没有数据了结束,之后检查两个区间中剩下的数归并到tmp中,然后将tmp中的数拷贝到a中。
void MergeSortNot(int* a, int n) {
int* tmp = (int*)malloc(sizeof(int) * n);
assert(tmp);
int gap = 1;
//当间距小于数组长度时继续
while (gap < n) {
for (int i = 0; i < n; i += gap * 2) {
//控制两个区间的边界
//[i, i+gap-1] [i+gap, i+2*gap-1]
int left1 = i, right1 = i + gap - 1;
int left2 = i + gap, right2 = i + gap * 2 - 1;
int index = i;
// 情况1 right1越界 修正
if (right1 >= n)
right1 = n - 1;
// 情况2 left2越界 表示第二个区间不存在,修正成一个不存在的区间
if (left2 >= n) {
left2 = n;
right2 = n - 1;
}
// 情况3 left2正常 right2越界 修正right2
if (left2 < n && right2 >= n)
right2 = n - 1;
printf("归并[%d,%d][%d,%d]--gap = %d\n", left1, right1, left2, right2,gap);
//归并过程是,有一组结束了循环结束
while (left1 <= right1 && left2 <= right2) {
//升序:将两个区间中较小的一个放到tmp中
if (a[left1] < a[left2]) {
tmp[index++] = a[left1++];
}
else {
tmp[index++] = a[left2++];
}
}
//检查两个区间中剩下的数归并到tmp中
while (left1 <= right1) {
tmp[index++] = a[left1++];
}
while (left2 <= right2) {
tmp[index++] = a[left2++];
}
}
//拷贝数据从tmp到a,闭区间个数要加1
//+left:每次拷贝不是考全部,只需要拷贝归并的那一段就可以了
memcpy(a, tmp, n * sizeof(int));
//每次gap间距都乘以2
gap *= 2;
}
free(tmp);
tmp = NULL;
}
🌏六.计数排序(非比较排序)
🍍基本思想
⭐️计数排序是一个非基于比较的排序算法,优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序。基数排序可以排序负数,但是不能排序浮点数
🍍实现过程
⭐️为了不必要的空间浪费,我们首先采用相对映射的方法,计算出数组中的最大值和最小值,然后依据最大值和最小值开辟一个空间count,然后将所有数组映射到count中,之后偏离数组将所有数组依次取出来
void CountSort(int* a, int n) {
//初始化min和max
int min = a[0];
int max = a[0];
//相对映射,可以节省不比要空间,而且可以从第二个数开始比较
for (int i = 1; i < n; ++i) {
if (a[i] > max) {
max = a[i];
}
if (a[i] < min) {
min = a[i];
}
}
//开辟的范围
int range = max - min + 1;
//计数数组
int* count = (int*)calloc(range, sizeof(int));
assert(count);
//遍历映射计数
for (int i = 0; i < n; ++i) {
count[a[i] - min]++;
}
//遍历排序数组
int index = 0;
for (int i = 0; i < range; ++i) {
while (count[i]--) {
a[index++] = i + min;
}
}
free(count);
count = NULL;
}
🍍时间复杂度、空间复杂度、稳定性分析
⭐️时间复杂度:
O ( N + K ) O(N+K) O(N+K)
取决于次数和数组范围谁更大⭐️空间复杂度:
O ( N ) O(N) O(N)
⭐️稳定性分析:计数是在统计每个数出现的次数,但是相同的数哪个在前哪个在后,并没有区分,所以我们写的不稳定.。
但是只是我们这里写的不稳定,计数排序可以写成稳定的,所以综合来说是稳定的
🌏七.八大排序比较
🍯1.性能测试代码
#define N 10000
void TestOP()
{
srand((unsigned int)time(NULL));
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);
int* a8 = (int*)malloc(sizeof(int) * N);
int i = 0;
for (i = 0; i < N; i++)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a2[i];
a4[i] = a3[i];
a5[i] = a4[i];
a6[i] = a5[i];
a7[i] = a6[i];
a8[i] = a7[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();
int begin8 = clock();
//CountSort(a7, N);
int end8 = clock();
printf("InsertSort:%dms\n", end1 - begin1);
printf("ShellSort:%dms\n", end2 - begin2);
printf("SelectSort:%dms\n", end3 - begin3);
printf("HeapSort:%dms\n", end4 - begin4);
printf("QuickSort:%dms\n", end5 - begin5);
printf("MergeSort:%dms\n", end6 - begin6);
printf("BubbleSort:%dms\n", end7 - begin7);
printf("CountSort:%dms\n", end8 - begin8);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
free(a8);
}
void TextInsertSort() {
int a[] = { 9,1,2,5,7,4,8,6,3,5 };
InsertSort(a, sizeof(a) / sizeof(a[0]));
PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextBubbleSort() {
int a[] = { 9,1,2,5,7,4,8,6,3,5 };
BubbleSort(a, sizeof(a) / sizeof(a[0]));
PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextShellSort() {
int a[] = { 9,1,2,5,7,4,8,6,3,5 };
ShellSort(a, sizeof(a) / sizeof(a[0]));
PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextHeapSort() {
int a[] = { 9,1,2,5,7,4,8,6,3,5 };
HeapSort(a, sizeof(a) / sizeof(a[0]));
PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextSelectSort() {
int a[] = { 9,1,2,5,7,4,8,6,3,5 };
SelectSort(a, sizeof(a) / sizeof(a[0]));
PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextQuickSort() {
int a[] = { 9,1,2,5,7,4,8,6,3,5 };
QuickSort2(a, 0, sizeof(a) / sizeof(a[0])-1);
PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextMergeSort() {
int a[] = { 10,6,7,1,3,9,4,2,5,2 };
PrintArry(a, sizeof(a) / sizeof(a[0]));
MergeSort(a, sizeof(a) / sizeof(a[0]));
PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextMergeSortNot() {
int a[] = { 4,5,2,9,3,4,7,5,8 };
PrintArry(a, sizeof(a) / sizeof(a[0]));
MergeSortNot(a, sizeof(a) / sizeof(a[0]));
PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextCountSort() {
int a[] = { 4,5,2,9,3,4,7,5,8 };
//PrintArry(a, sizeof(a) / sizeof(a[0]));
CountSort(a, sizeof(a) / sizeof(a[0]));
PrintArry(a, sizeof(a) / sizeof(a[0]));
}
🍯2.排序空间复杂度、时间复杂度、稳定性
⭐️稳定性:相同的数排完数之后,相对顺序不变,那么就是稳定的。
排序方法 | 时间复杂度(平均情况) | 时间复杂度(最好) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O(N2) | O(N) | O(N2) | O(1) | 稳定 |
希尔排序 | O(N1.3) | O(N) | O(N2) | O(1) | 不稳定 |
选择排序 | O(N2) | O(N2) | O(N2) | O(1) | 不稳定 |
堆排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(1) | 不稳定 |
冒泡排序 | O(N2) | O(N) | O(N2 ) | O(1) | 稳定 |
快速排序 | O(NlogN) | O(NlogN) | O(N2 ) | O(logN) | 不稳定 |
归并排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(N) | 稳定 |
计数排序 | O(N+K)(k是整数的范围) | O(N+K) | O(N+K) | O(N+K) | 稳定 |
🌏总结
⭐️排序当然不止这八种,过不这几种比较经典,感谢阅读。
⭐️码字不易喜欢的话,欢迎大家点赞支持和指正~