【数据结构之排序系列】直接插入排序,冒泡排序,直接选择排序,堆排序,希尔排序
创始人
2024-05-21 04:38:37
0

目录

    • 前言
    • 一、直接插入排序
    • 二、冒泡排序
    • 三、堆排序
    • 四、直接选择排序
    • 五、希尔排序

前言

排序算法章节在校招方面考察是相对比较频繁的,所以本章中所学习的所有排序算法需要引起很大的重视。需要掌握各种排序算法的时间复杂度,注意事项

一、直接插入排序

思想:对于一个数组而言,假定第一个元素默认是有序的,从第二个位置的元素开始,每次插入之后与前面的数据进行比较,假如排成升序,如果插入的数据比前面的数据小,则需要将前面的数据进行挪动,直到找到前面的数据是小于等于插入的数据,那么再将此插入的数据放到该数据之后。具体代码如下:

// 插入排序
void InsertSort(int* a, int n)
{assert(a);for (int i = 0; i < n - 1; i++){int end = i;int tmp = a[end + 1];// 保存每次插入的数据while (end >= 0){if (tmp < a[end]){a[end + 1] = a[end];--end;}else{break;}}// 循环走到这里是有两种情况的:// 1.循环条件结束// 2.break出来的a[end + 1] = tmp;}
}

插入排序中需要注意的点:

  1. 第一个for循环中,第一次循环end是从第一个位置开始的,然后每次比较后面的数和end位置的大小关系,最后一次循环end是在倒数第二个位置上,也就是相当于插入最后一个数据时,前面的数据已经有序,那么此时再比较最后一个数据和前面数据的大小关系即可。
  2. while循环中,主要是找到a[end+1]的合适位置,不符合时就依次挪动数据,符合数据时退出循环,相当于找到了a[end+1]的合适位置
  3. 跳出循环有两种情况:
  • 循环条件结束(循环条件不满足了),此时end在下标为-1的位置
  • break出来的(a[end+1]找到了合适的位置),此时end还没有发生越界

时间复杂度的分析

插入排序中,最坏的情况就是数据刚好是逆序的,此时每次插入数据都需要移动对应的数据,第一个数据默认有序的话,那么插入第二个数据需要移动1次,第三个数据需要移动2次,第四个数据需要移动3次…第n个数据需要移动n-1次,显然,这是一个等差数列的求和公式,因此时间复杂度为O(N^2)。最好的情况就是有序时,其实本质只需要执行外层的for循环,因此只需要执行n-1,因为当数据有序时,相当于每次插入的数据都是在合适的位置,不需要另找位置,也就不需要挪动数据的位置,因此这种情况的时间复杂度为O(N)
综上,插入排序的时间复杂度为O(N^2)

二、冒泡排序

思想:冒泡排序本质上是属于一种交换排序,冒泡排序算法中需要使用到交换函数,其实就是一个交换的过程,假如排成升序,每轮冒泡排序就是将数据中的最大值交换到最后的位置,然后不考虑该最大值,剩余数据继续进行冒泡排序。代码实现如下:

// 冒泡排序
void BubbleSort(int* a, int n)
{assert(a);for (int j = 0; j < n - 1; j++){for (int i = 0; i < n-j-1; i++){if (a[i] > a[i + 1]){Swap(&a[i], &a[i + 1]);}}}
}

分析

外层循环主要是控制冒泡排序的趟数,10个数其实我们只需要进行9趟就可以了,因为最后一个就默认是最小的,不需要进行冒泡,因此n个数据就是需要进行n趟冒泡排序。内层循环控制的是每一趟冒泡排序控制的数据范围,第一趟冒泡排序控制的数据范围是整个数据,第二趟得将最大值去掉得到剩余的数据,第三趟冒泡排序需要去掉第二趟中的最大值得到第三趟的冒泡排序的数据,依此类推。假如排成升序,每一趟冒泡排序需要做的事情就是将该趟冒泡排序控制的数据的最大值冒泡到数据的结尾,因此最终可以排成升序。
注意事项
上述的代码其实不是冒泡排序的最优解,因为上述的冒泡排序中,无法判断数据有序的情况,当数据已经有序时,该排序算法仍然会进行无脑的冒泡,因此效率较低,下面将给出优化方案:增加一个标志位,标志上一趟冒泡排序中是否发生交换,如果发生交换,则说明上一趟的数据仍然不是有序的,需要进行冒泡,如果上一趟没有发生交换,则说明数据已经达到有序,此时只需要退出循环即可,不需要再进行冒泡排序。代码实现如下:

// 冒泡排序
void BubbleSort(int* a, int n)
{assert(a);for (int j = 0; j < n - 1; j++){int exchange = 0;for (int i = 0; i < n-j-1; i++){if (a[i] > a[i + 1]){// 如果发生交换,修改标志位exchange = 1;Swap(&a[i], &a[i + 1]);}}if (exchange == 0){// 说明上一趟冒泡排序中没有发生交换,数据已经达到有序break;}}
}

三、堆排序

思想:堆排序本质是在一棵完全二叉树中对数据进行选择排序,该排序中需要使用向上调整算法和向下调整算法。堆排序的步骤:建堆,如果排成升序则建大堆,排成降序,则建小堆,然后每次将第一个位置的元素和最后一个元素的值进行交换,因为堆顶存放的是最值,所以每次交换之后,对剩余的数据进行向下调整,使剩余的数据仍然能够满足堆的性质。

  • 向上调整算法代码
// 向上调整算法
void AdjustUp(int* a, int child)
{// child表示调整的起始位置int parent = (child - 1) / 2;while (child > 0){// 调成大堆if (a[parent] < a[child]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}
  • 向下调整算法代码
// 向下调整算法
void AdjustDown(int* a, int n, int root)
{// root表示从根节点开始进行调整int parent = root;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--;}
}

堆排序的本质就是将堆顶数放到最后一个位置,然后再对剩余的数据进行调整,使剩余的数据仍然满足堆的性质。

注意:

  • 如果排成升序,则需要建大堆,因为大堆的堆顶存放的是数据的最大值,所以每次将堆顶数据放到数据的最后一个位置,就可以将数据排成升序。
  • 如果排成降序,则需要建小堆,因为小堆的堆顶存放的是数据的最小值,所以每次将堆顶数据放到数据的最后一个位置,就可以将数据排成降序。

四、直接选择排序

思想:我们需要两个下标,分别称为左右下标,左下标是作为左区间,右下标作为右区间,每次遍历该区间,选择出区间中的最小值和最大值,分别放到左边和右边,再缩小区间范围,即左下标向前走一步,右下标向后走一步,当左右下标相遇时循环结束。代码实现如下:

// 直接选择排序
void SelectSort(int* a, int n)
{assert(a);int left = 0;int right = n - 1;while (left < right){int mini = left;int maxi = left;for (int i = left; i <= right; i++){if (a[i] < a[mini]){mini = i;}if (a[i] > a[maxi]){maxi = i;}}Swap(&a[mini], &a[left]);Swap(&a[maxi], &a[right]);left++;right--;}
}

注意:
上面的代码是存在一定的问题的,在一些极端场景中,上面的代码就会出现问题,如果在遍历的时候,最大值的下标和左下标是一样,相当于就是说,交换前,最大值在数据的最左边,那么如果我们将最小值和最左边的数据进行交换的话,显然会将最大值换到了原来最小值的地方,所以此时我们需要更新最大值的下标,也就是交换前最小值的下标。处理后的代码如下:

// 直接选择排序
void SelectSort(int* a, int n)
{assert(a);int left = 0;int right = n - 1;while (left < right){int mini = left;int maxi = left;for (int i = left; i <= right; i++){if (a[i] < a[mini]){mini = i;}if (a[i] > a[maxi]){maxi = i;}}Swap(&a[mini], &a[left]);if (maxi == left){maxi = mini;}Swap(&a[maxi], &a[right]);left++;right--;}
}

五、希尔排序

思想:希尔排序是在直接插入排序的基础上进行改造的,插入排序中,当数据出现逆序时,时间复杂度为O(N^2)效率比较低,所以希尔排序针对直接插入排序中存在的这样的问题进行优化。在希尔排序中,首先会对数据进行预排序预排序的结果就是使数据接近有序,而不至于逆序,当数据接近有序时,此时在使用直接插入排序算法对数据进行排序,效率就会比较高。希尔排序中需要确定一个间隔gap,gap越大,表明大的数越快到达后面,小的数越快到达前面,但是数据越不接近有序,当gap越小时,预排序之后数据越接近有序。代码实现如下:

// 希尔排序
void ShellSort(int* a, int n)
{assert(a);// 预排序int gap = n;while (gap > 1){gap = gap / 3 + 1;for (int i = 0; i + gap < n; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (tmp < a[end]){a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = tmp;}}
}

上面的Gap一般建议是数据个数的三分之一左右,再加1能够保证最后一次的gap一定是1,表示直接插入排序,注意每次是将a[end]a[end+gap]的数据进行比较,而不是a[end]a[end+1]的数据进行比较,这就是希尔排序直接插入排序的区别之一。

相关内容

热门资讯

安卓子系统windows11,... 你知道吗?最近科技圈可是炸开了锅,因为安卓子系统在Windows 11上的兼容性成了大家热议的话题。...
电脑里怎么下载安卓系统,电脑端... 你有没有想过,你的电脑里也能装上安卓系统呢?没错,就是那个让你手机不离手的安卓!今天,就让我来带你一...
索尼相机魔改安卓系统,魔改系统... 你知道吗?最近在摄影圈里掀起了一股热潮,那就是索尼相机魔改安卓系统。这可不是一般的改装,而是让这些专...
安卓系统哪家的最流畅,安卓系统... 你有没有想过,为什么你的手机有时候像蜗牛一样慢吞吞的,而别人的手机却能像风一样快?这背后,其实就是安...
安卓最新系统4.42,深度解析... 你有没有发现,你的安卓手机最近是不是有点儿不一样了?没错,就是那个一直在默默更新的安卓最新系统4.4...
android和安卓什么系统最... 你有没有想过,你的安卓手机到底是用的是什么系统呢?是不是有时候觉得手机卡顿,运行缓慢,其实跟这个系统...
平板装安卓xp系统好,探索复古... 你有没有想过,把安卓系统装到平板上,再配上XP系统,这会是怎样一番景象呢?想象一边享受着安卓的便捷,...
投影仪装安卓系统,开启智能投影... 你有没有想过,家里的老式投影仪也能焕发第二春呢?没错,就是那个曾经陪你熬夜看电影的“老伙计”,现在它...
安卓系统无线车载carplay... 你有没有想过,开车的时候也能享受到苹果设备的便利呢?没错,就是那个让你在日常生活中离不开的iOS系统...
谷歌安卓8系统包,系统包解析与... 你有没有发现,手机更新换代的速度简直就像坐上了火箭呢?这不,最近谷歌又发布了安卓8系统包,听说这个新...
微软平板下软件安卓系统,开启全... 你有没有想过,在微软平板上也能畅享安卓系统的乐趣呢?没错,这就是今天我要跟你分享的神奇故事。想象你手...
coloros是基于安卓系统吗... 你有没有想过,手机里的那个色彩斑斓的界面,背后其实有着一个有趣的故事呢?没错,我要说的就是Color...
安卓神盾系统应用市场,一站式智... 你有没有发现,手机里的安卓神盾系统应用市场最近可是火得一塌糊涂啊!这不,我就来给你好好扒一扒,看看这...
黑莓平板安卓系统升级,解锁无限... 亲爱的读者们,你是否还记得那个曾经风靡一时的黑莓手机?那个标志性的全键盘,那个独特的黑莓体验,如今它...
安卓文件系统采用华为,探索高效... 你知道吗?最近安卓系统在文件管理上可是有了大动作呢!华为这个科技巨头,竟然悄悄地给安卓文件系统来了个...
深度系统能用安卓app,探索智... 你知道吗?现在科技的发展真是让人惊叹不已!今天,我要给你揭秘一个超级酷炫的话题——深度系统能用安卓a...
安卓系统的分区类型,深度解析存... 你有没有发现,你的安卓手机里藏着不少秘密?没错,就是那些神秘的分区类型。今天,就让我带你一探究竟,揭...
安卓系统铠无法兑换,揭秘无法兑... 最近是不是有很多小伙伴在玩安卓系统的游戏,突然发现了一个让人头疼的问题——铠无法兑换!别急,今天就来...
汽车安卓系统崩溃怎么刷,一键刷... 亲爱的车主朋友们,你是否曾遇到过汽车安卓系统崩溃的尴尬时刻?手机系统崩溃还能重启,但汽车系统崩溃了,...
miui系统可以刷安卓p系统吗... 亲爱的手机控们,你是否对MIUI系统情有独钟,同时又对安卓P系统的新鲜功能垂涎欲滴?今天,就让我带你...