zoukankan      html  css  js  c++  java
  • 排序算法之插入排序

      插入排序,顾名思义就是寻找适当的位置,插入记录,使得整个表变成有序。插入排序有直接插入排序、折半插入排序、2路插入排序、希尔排序等,这边文章主要介绍直接插入排序、折半插入排序和希尔排序。

    一、直接插入排序

      插入排序最好的示例是扑克牌,很多书都以抓扑克牌的过程为例子讲述插入的排序过程,抓牌的过程和插入排序很类似,但有区别,我这里用一个更贴切插入排序的示例展示插入排序的过程。假设你在打牌,突然要上厕所了,牌友给你抓一下牌,牌友只会把属于你的牌扔在你的位子上,不会给你排序,等你回来,你拿起一看,一手黑桃,好牌啊:

        

      现在你需要将手牌从左到右按小到大排序。

      你从最左边开始排序,最左边1张牌,显然对于前1张牌是有序的。

      现在从左边第2张牌开始,第2张牌和第1张比较,发现五比七小,不需要移动,前2张牌就已经有序。

         

      接下来看第3张牌:

        

      第3张牌黑桃四依次与左边的牌比较,四比七小,四应该往左移,四再与五比较,四比五也小,继续往前移。

           

      这样前3张牌就已经有序了;

      往下看第4张牌:

        

      黑桃三依次与左边的牌比较,如果比三大就交换,直到遇到比三小的牌或者前面已经没有牌了,可以看到,三应该插在最前面,其他3张牌依次后移。

         

      移动之后,前4张牌就已经有序了;

      在看第五张牌:

        

      第5张牌黑桃八依次与前面的牌比较,发现八比前面一张牌七大,也就是八比前面的4张牌都大了,不需要移动牌,前5张就已经有序了;

      再看第6张牌:

        

      我们发现第6张牌黑桃九比前面的八大了,那我们不需要移动就可以保持前6张牌有序了;

      最后我们来看第7张牌:

        

      第7张牌黑桃六依次与前面的牌比较,如果前一张牌比六大就交换,直到遇到比六小的牌或者前面已经没有牌了。

        

      黑桃六应该插入到五和七之间,九、八、七依次后移,移动之后这一手牌就按递增有序了。

        

      以上就一个直接插入排序的全过程。我们来看看直接插入排序算法的代码:    

     1 void insertSort(int*  data, int len){
     2     int sentry;//哨兵
     3     int i,j;
     4     for(i = 1; i < len; i ++){//从第二个元素开始排序,前面的一个元素默认有序了        
     5         if (data[i] < data[i-1]){
     6             sentry = data[i];//复制为哨兵
     7             for(j = i-1; sentry < data[j]; j --){
     8                 data[j+1] = data[j];//记录后移 
     9             }
    10             data[j+1] = sentry;//插入到适当的位置
    11         }        
    12     }
    13 }

      直接插入排序的思想很简单,每一趟排序有序表长度加1,在给记录找出入位置的同时,后移比它大(这里针对非递减排序)的元素。分析直接插入排序算法的时空复杂度。

      空间上只需要一个哨兵做辅助空间,即空间复杂度为O(1);

      对每一趟排序,需要完成比较和移动两个操作,最好情况下,待排序的记录已经有序,每一趟排序只需比较1次,共∑1 = (n-1)次比较,不需要移动元素;最坏情况下,待排序的记录为逆序。比较次数共∑i = (n+2)(n-1)/2,移动共∑(i+1)=(n+4)(n-1)/2次。因此平均情况下,直接插入排序的时间复杂度为O(n^2)。

      我选择用这个中途上厕所回来清牌的例子,而不用抓牌的过程,因为我认为插入排序是一个原地排序,不需要申请额外的记录表。抓牌的过程让人感觉手里的牌和还没有抓上来的牌是不同的记录表;而上厕所回来清牌,桌上乱序的牌就是一张待排序的记录表,清牌的过程是在这个表上进行的,明显没有另辟空间。 

    二、折半插入排序

      直接插入排序中,每次查找插入位置时,都是依次往前进行比较。我们进行第i(i>1)趟排序时,前(i-1)个记录已经有序,于是查找插入位置我们可以用折半查找。算法如下:

     1 void bInsertSort(int*  data, int len){
     2     int sentry;//哨兵
     3     int i,j;
     4     int low,high,middle;
     5     for(i = 1; i < len; i ++){
     6         sentry = data[i]; //复制哨兵
     7         low = 0;
     8         high = i - 1;
     9         while(low <= high){        //寻找插入位置
    10             middle = (low + high) / 2; //折半下标
    11             if  (sentry > data[middle]) //插入点在高半区
    12                 low = middle + 1;
    13             else                        //插入点在低半区
    14                 high = middle - 1;
    15         }
    16         for(j = i -1; j >= high + 1; j --){
    17             data[j+1] = data[j];//记录后移
    18         }
    19         data[j+1] = sentry;//插入到适当的位置    
    20     }
    21 }

      折半插入排序只是减少了寻找插入位置而进行的比较次数,而移动记录的次数并没有减少。因此折半插入的时间复杂度仍然是O(N^2)。

    三、希尔排序

      不论是直接插入排序还是折半插入排序,当待排记录表很长时,排序效率会比较低,因为记录表很长时,平均每趟移动的记录数就会很多。

        

      如上图,假设这个记录表的长度是10005,当前面1000个记录已经有序了,现在需要排第10001个记录时,发现第10001个记录需要插入到第一个位置,一共需要进行10000次移动元素。直接插入排序移动元素只能进行相邻的局部移动,不能实现跨越式的移动。

      希尔排序就是从这点出发对直接插入排序进行改进。它的基本思想是:先将整个待排序的记录表分割成几个子表,对子表分别进行直接插入排序,子表的长度比较小,排序的效率会比较高。算法如下:

     1 void shellInsert(int* data,int len, int dk){
     2     int sentry;//哨兵
     3     int i,j;
     4     for (i = dk ; i < len; i ++){
     5         if (data[i - dk] > data[i]){
     6             sentry = data[i];
     7             for(j = i - dk; j >= 0 && sentry < data[j]; j -= dk){
     8                 data[j + dk] = data[j];//记录后移 
     9             }
    10             data[j + dk] = sentry;//插入到适当的位置
    11         }
    12         
    13     }
    14 }
    15 
    16 void shellSort(int* data,int len, int dlta[],int dlen){
    17     int k;
    18     for(k = 0; k < dlen; k ++){        
    19          shellInsert(data,len,dlta[k]);    //一趟增量为dlta[k]的插入排序             
    20     }
    21 }

      希尔排序中的子序列并不是简单的逐段分割,而是将相隔某个增量的记录组成一个子序列,下图的三趟排序的增量分别为5、3、1。

        

      我们发现在希尔排序中,较小的记录不是相邻的局部移动,而是跳跃式的移动,在最后一趟时,增量为1,待排记录表已经基本有序,利用直接插入排序即可完成排序,希尔排序的时间复杂度仍为O(n^2),但多项式系数比直接插入排序小,算法效率较高。另外希尔排序不同与直接插入排序和折半插入排序,希尔排序是不稳定的排序算法,可以考到上图中的两个相同的记录49在排序前后交换了顺序,而直接插入排序和折半插入排序是稳定的,不会改变相同记录之间的相对位置。

      参考资料:[1]严蔚敏,吴伟明.数据结构(C语言版),清华大学出版社.

           [2]Thomas H.Cormen等.算法导论,机械工业出版社.

      画图工具:PPT + QQ截图

  • 相关阅读:
    VirtualBox Network设置的NAT和Bridged Adapter模式区别
    Kubernetes里的ConfigMap的用途
    使用Kubernetes里的job计算圆周率后2000位
    给谷歌输入法增添自定义词组,提高输入效率
    推荐一个yaml文件转json文件的在线工具
    GCC同时使用静态库和动态库链接
    Linux后台开发常用工具
    gcc链接参数--whole-archive的作用
    rdynamic和-whole-archive
    gcc和ld 中的参数 --whole-archive 和 --no-whole-archive
  • 原文地址:https://www.cnblogs.com/fengfenggirl/p/insertSort.html
Copyright © 2011-2022 走看看