当前位置:   article > 正文

初等排序——插入排序、冒泡排序、希尔排序

初等排序——插入排序、冒泡排序、希尔排序

对于一个给定包含n个元素的数组,我们总需要将其按照相对大小进行排序,进而方便进行数据处理。如排序学生的成绩,员工的薪水。这就设计到了排序算法

今天我学习的是三种初等排序方法——插入排序、冒泡排序、希尔排序

对于不同的排序算法,我们有几个重要的关注点:
1.时间复杂度:是数组元素n的函数,越低效率越高

2.排序的稳定性:当数据中存在2个或2个以上的数值相等的元素时,这些元素在排序处理前后的顺序不变,被称为排序的稳定性

一、插入排序

想象自己正在打扑克牌,一开始收到的一手牌是无序的,若我们需要将其由小到大地排序,则我们从左往右检索每一张牌,当找到的牌比前面的牌数值要小时,就插入到前面去,逐步进行直到手牌有序

插入排序就是模拟这样的一个过程

假定我们现在要将数组由小到大排序

我们将数组从左到右划分为有序部分和无序部分

每次检索无序部分的第一个元素(称其为a),并将其逐渐往前与有序部分作比较,当找到第一个比a小的元素(称其为b)时,将a插到b的下一位。届时,我们就多排好了一个元素。继续循环下去,直到将无序部分的元素全部插入有序部分,即完成了对数组的排序

由于一开始数组无序,我们认为第一个元素有序即可

如何将a插入到b的下一位呢?我们只需要将有序部分中比a大的元素全都往后移一位即可(直接覆盖后一位)

实现代码如下:

  1. void InsertSort(int* nums, int numsSize){
  2. for(int i=1;i<numsSize;i++){//认为nums[0]有序
  3. int j=i-1;//从nums[i]的前一位开始查找,即有序部分的最后一位
  4. int check=nums[i];
  5. while(j>=0&&nums[j]>check){
  6. nums[j+1]=nums[j];//向后移动一位,覆盖原元素
  7. j--;
  8. }
  9. nums[j+1]=check;//找到比nums[i]小的元素nums[j]后,在其下一位插入nums[i]
  10. }
  11. }

研究排列数组的不同情况,我们有:

当插入排序对完全逆序的数组进行排序时,每一个元素都要向前插入,此时其时间复杂度为O(n^2)

当插入排序对完全顺序的数组进行排序时,每一个元素只需检索一次,此时其时间复杂度为O(n)

在插入排序中,我们只将比check大的元素向后平移,不相邻的元素不会直接交换位置,因此整个排序算法十分稳定

二、冒泡排序

冒泡排序法就是让数组元素像水中的气泡一样逐渐上浮,从而达到排序的目的。

冒泡排序同样将数组分为有序部分与无序部分,并假定一开始的有序部分为空

假定我们要将数组由小到大排序

则每检索一次无序部分,我们就当将其中的最大元素上浮(到数组最右边)到有序部分

如此循环,我们就可以将第二大、第三大的元素逐个排到有序部分,进而完成数组的排序

那们如何找到无序部分的最小值并将其上浮呢?

我们采用相邻元素对比并交换的方法:

对于给定的数组{a,b,c,d,e,f,g}

研究元素a与b,若a>b,则将a与b位置交换,从而使较大的a向右移动一位

接着再比较a与c,如此循环,从而将数组中的最大值移动到数组最右边

我们只需要循环n次上述对无序部分检索的步骤,即可将数组排序

至此,我们可以写出实现代码:
 

  1. void BubbleSort(int* nums, int numsSize){
  2. for(int i=0;i<numsSize;i++){ //i可以表示已排序的元素个数
  3. for(int j=0;j<numsSize-1-i;j++){//无序部分长度为数组长度-有序部分长度
  4. if(nums[j]>nums[j+1]){ //再-1是为了保证[j+1]不溢出
  5. int tem=nums[j+1];
  6. nums[j+1]=nums[j];
  7. nums[j]=tem; //交换位置
  8. }
  9. }
  10. }
  11. }

该算法的时间复杂度为O(n^2)

因为冒泡排序仅对数组中的相邻元素进行比较和交换,因此数值相同的元素并不会改变顺序,所以冒泡排序具有稳定性

tips:若将if中的判断条件的>改为>=,则数值相等时两个元素会交换位置,此时不再具有稳定性

三、希尔排序

前面两种算法的时间复杂度最高都为O(n^2),当n很大时,时间消耗很高

那么能不能对其进行一些优化,尽量减少其时间复杂度呢?

我们把目光看向插入排序,发现,当被排列的数组越接近有序时,插入排序所需要的时间越少

那么,我们能否设计一种算法,在使用插入排序之前,使数组尽量有序?

这就是希尔排序的设计思路

假定给出一个数组{2,5,3,8,9,6,3,4,7,1},个数n=10

我们先对其进行预处理:

按照一定的间隔gap,将其分为几个组

当gap等于5时,我们有:

{2,6} , {5,3} , {3,4} , {8,7} , {9,1}

对这五个小数组,我们将其从小到大排序,于是原数组被改造为:

{2,3,3,7,1,6,5,4,8,9}

再取gap的值为2

我们有:

{2,3,1,5,8} , {3,7,6,4,9}

对其从小到大进行排序,原数组被改造为:

{1,3,2,4,3,6,5,7,8,9}

此时数组已经变得接近有序,我们再使用插入排序,即可完成排序

为什么将大数组按一定间隔分组后排序,大数组会逐渐变得有序呢?

因为我们在给小数组排序的过程中,已经尽量地将相对小的元素排列在左边,而相对大的元素排列在右边

那么我们如何实现对小数组的排序呢?

对不同的数组,我们应当设置不同的gap值,对不同的gap,给小数组排序的总次数就不同,这就会导致代码量的改变,那么我们如何避免这一个问题呢?

观察直接插入排序的实现代码,我们可知,在将无序部分第一个元素b插入有序部分时,比a大的元素全都移到了它们的下一位,这是因为它们的排序是连续的。那么对于存在gap的不连续的小数组而言,我们也只需要对其使用插入排序,并在插入时将其后移到下gap位即可

我们如何确定gap的值的大小呢?

希尔排序的时间复杂度与gap值的确定方式息息相关,这是一个复杂的问题,涉及到一些数学上未能攻克的难题

最初希尔排序的发明者——希尔提出的gap=n/2,每排序完一次小数组,gap/=2,使gap变为原来的1/2,直到gap==1时变为直接插入排序

后来Knuth提出了gap=(gap/3)+1,使每次排序过后gap变为原来的1/3,(加1是为了防止gap<=3时gap/3==0的情况),在这种情况下,时间复杂度基本维持在O(n^{1.25})

在这里我们采用希尔的方法确定gap的值

至此,我们可以写出实现代码如下:

  1. void ShellSort(int* nums, int numsSize){
  2. int gap=numsSize;
  3. while(gap>1){
  4. gap/=2; //gap的值减少一半
  5. for(int i=0;i<n-gap;i++){ //通过i所指的元素,确定我们在排序哪个小数组
  6. int end=i;
  7. int temp=nums[end+gap]; //end跳跃一个gap,即为等待插入的值temp
  8. while(end>=0){ //end用于判断是否超出小数组左边
  9. if(temp<nums[end]){ //temp比小数组中其前一个元素小时,前一个元素向后移动gap尾
  10. nums[end+gap]=nums[end];
  11. end-=gap; //检索小数组中更前一个元素
  12. else{
  13. break; //当检测到小的,不再前插
  14. }
  15. }
  16. nums[end+gap]=temp; //将temp插到end后第gap个,即小数组中end的下一个
  17. }
  18. }

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号