【数据结构与算法】内部排序之一:插入排序和希尔排序的N中实现(不断优化,附完整源码)

转载请注明出处:http://blog.csdn.net/ns_code/article/details/20043459

 

前言

本来想将所有的内部排序总结为一篇博文,但是随着研究的深入,还是放弃了这个念头,斟前酌后,还是觉得分开来写比较好,具体原因,看完本篇博文也就自然明了了。

本篇文章主要探讨插入排序和希尔排序,之所将二者放在一起,很明显,是因为希尔排序是建立在插入排序的基础之上的。

    注:以下各排序算法的N种实现方法大部分都是我根据算法思想,自己写出来的,或者是参考其本身的经典实现,我自己都已测试通过,但不敢保证一定都没问题,如果有疑问,欢迎指出。

 

插入排序

插入排序的思想很简单,它的基本操作就是将一个数据插入到已经排好序的序列中,从而得到一个新的有序序列。根据查找插入位置的实现思路不同,它又可以分为:直接插入排序、折半插入排序、2-路插入排序。。。这里,我们主要探讨下直接插入排序和折半插入排序。

直接插入排序

直接插入排序是最基本的插入排序方法,也是一种最简单的排序方法。其基本实现思想如下:

1、首先把第一个元素单独看做一个有序序列,依次将后面的元素插入到该有序序列中;

2、插入的时候,将该元素逐个与前面有序序列中的元素进行比较,找到合适的插入位置,形成新的有序序列;

3、当有序序列扩大为整个原始序列的大小时,排序结束。

第一种实现方法

按照该思想,我第一次写出来的实现代码如下:

[cpp] view plaincopy

  1. /*
  2. 第一种代码形式
  3. 插入排序后的顺序为从小到大
  4. */
  5. void Insert_Sort1(int *arr,int len)
  6. {
  7. int i;
  8. //从第1个元素开始循环执行插入排序
  9. for(i=1;i<len;i++)
  10. {   //将第i个元素分别与前面的元素比较,插入适当的位置
  11. if(arr[i]<arr[i-1])
  12. {   //一直向左进行比较,直到插入到适当的位置
  13. int key = arr[i];
  14. int count = 0;  //用来记录key在与前面元素时向左移动的位置
  15. while(i>0 && key<arr[i-1])
  16. {
  17. arr[i] = arr[i-1];
  18. arr[i-1] = key;
  19. i--;
  20. count++;
  21. }
  22. //将待插入的数定位到下一个元素,
  23. //因为后面还要执行i++,所以这里不再加1
  24. i += count;
  25. }
  26. }
  27. }

第二种实现方法

很明显,上面的代码有些冗杂,如果面试的时候让你手写插入排序的代码,很难一下子写出来。于是,我考虑将while循环去掉,直接在后面再来一个for循环,每次比较,遇到比自己大的就交换,直到遇到比自己小的,才退出for循环。这样代码改成了如下形式:

[cpp] view plaincopy

  1. /*
  2. 第二种代码形式
  3. 插入排序后的顺序为从小到大
  4. */
  5. void Insert_Sort2(int *arr,int len)
  6. {
  7. int i,j;
  8. for(i=1;i<len;i++)
  9. for(j=i-1;j>=0 && arr[j]>arr[j+1];j--)
  10. {
  11. //交换元素数值
  12. //由于不会出现自己与自己交换的情况,
  13. //因此可以安全地用该交换方法
  14. arr[j]^=arr[j+1];
  15. arr[j+1]^=arr[j];
  16. arr[j]^=arr[j+1];
  17. }
  18. }

第三种实现方法

上面的代码要用到数据的交换,即每次要插入的元素要逐个地与前面比它大的元素互换位置,而数据交换需要三步赋值操作,我们完全可以避免进行如此多的操作 (排序算法中一般都会尽量避免数据的交换操作),为了提高执行效率(虽然该执行效率的提高可能并没有那么显著),我们再回过头来看第一种实现方法,我们可 以通过key变量先将待插入的数据保存起来,在比较时只将元素右移一位即可,最后再将key放到要插入的位置,这样可以减少两个赋值操作的执行时间。这样 我们可以把代码改成如下实现形式:

[cpp] view plaincopy

  1. /*
  2. 第三种代码形式
  3. 插入排序后的顺序为从小到大
  4. */
  5. void Insert_Sort3(int *arr,int len)
  6. {
  7. int i,j;
  8. for(i=1;i<len;i++)
  9. if(arr[i] < arr[i-1])
  10. {   //向前逐个比较,直到需要插入的地方
  11. int key = arr[i];
  12. for(j=i-1;j>=0 && arr[j]>key;j--)
  13. arr[j+1] = arr[j];
  14. arr[j+1] = key;    //插入key
  15. }
  16. }

这也是最常见的实现形式,如果在面试中要手写插入排序的话,直接把这种实现代码写出来就可以了。

    另外,很明显可以看出来,对于长度为n的待排序咧,直接插入排序的平均时间复杂度为O(n*n),而且直接插入排序的比较次数与原始序列的中各元素的位置密切相关,待排序的序列越接近于有序,需要比较的次数就越小,时间复杂度也就越小。

折半插入排序

直接插入排序算法简单,且容易实现,当待排序的长度n很小时,是一种很好的排序方法,尤其当原始序列接近有序时,效率更好。如果待排序的长度n很大,则 不适宜采用直接排序。这时我们可以考虑对其做些改进,我们可以从减少比较和移动的次数入手,因此可以采用折半插入排序,其思想类似于折半查找,这里不再详 细说明,直接给出实现代码:

[cpp] view plaincopy

  1. /*
  2. 插入排序后的顺序为从小到大
  3. */
  4. void BInsert_Sort(int *arr,int len)
  5. {
  6. int i;
  7. //从第1个元素开始循环执行插入排序
  8. for(i=1;i<len;i++)
  9. {
  10. int low =0;
  11. int high = i-1;
  12. int key = arr[i];
  13. //循环至要插入的两个点之间
  14. while(low<=high)
  15. {
  16. int mid = (low+high)/2;
  17. if(key<arr[mid])
  18. high = mid-1;
  19. else
  20. low = mid+1;
  21. }
  22. //循环结束后low=high+1,并且low位置即为key要插入的位置
  23. //从low到i的元素依次后移一位
  24. int j;
  25. for(j=i;j>low;j--)
  26. arr[j] = arr[j-1];
  27. //将key插入到low位置处
  28. arr[low] = key;
  29. }
  30. }

从代码中可以看出,折半插入排序所需附加的存储空间与直接插入排序相等,时间上来看,折半插入排序减少了比较的次数,但是元素的移动次数并没有减少。因此,折半插入排序的平均时间复杂度仍为O(n*n)。

希尔排序

希尔排序(shell排序),又称缩小增量排序。上面我们提到,直接插入排序在原始序列越接近有序的情况下,排序效率越高,希尔排序正是利用了直接插入排序的这个优势。希尔排序的基本思想如下:

它将序列按照某个增量间隔分为几个子序列,分别对子序列进行插入排序,而后再取另一增量间隔,对划分的子序列进行插入排序,依次往后。。。待序列已经大致有序,最后再对整个序列进行插入排序(即增量间隔为1),从而得到有序序列。

本文的重点放在排序算法的各种代码实现上,因此不再对具体的实现思想做过多的阐述,读者可以查阅相关资料或书籍来熟悉希尔排序的具体思想。由于希尔排序要用到插入排序,因此,我们依次根据上面基本插入排序的三种不同实现方法来书写希尔排序的代码。

第一种实现方法

仔细分析希尔排序的实现思想,会发现,如果要循环对各个子序列依次进行插入排序,我们需要在直接插入排序代码的外面再加一层for循环,用来循环所有的子序列。我们根据插入排序的第一种实现方法写出的代码如下:

[cpp] view plaincopy

  1. /*
  2. 第一种形式的代码
  3. 对长为len的数组进行一趟增量为ader的插入排序
  4. 本算法在插入排序算法的第一种实现形式上进行修改得到
  5. */
  6. void Shell_Insert1(int *arr,int len,int ader)
  7. {
  8. int i,k;
  9. //循环对ader个子序列进行插入排序操作
  10. for(k=0;k<ader;k++)
  11. for(i=ader+k;i<len;i+=ader)      //对一个子序列进行插入排序操作
  12. {   //将第i个元素分别与前面的每隔ader个位置的元素比较,插入适当的位置
  13. if(arr[i]<arr[i-ader])
  14. {   //一直向左进行比较,直到插入到适当的位置
  15. int key = arr[i];
  16. int count = 0;  //用来记录key在与前面元素比较时向左移动了几个ader长度
  17. while(i>k && key<arr[i-ader])
  18. {
  19. arr[i] = arr[i-ader];
  20. arr[i-ader] = key;
  21. i -= ader;
  22. count++;
  23. }
  24. //将待插入的数定位到下一个元素,执行下一次插入排序
  25. //因为后面还要执行i+=ader,所以这里回到原位置即可
  26. i += count*ader;
  27. }
  28. }
  29. }

第二种实现方法

很明显,与上面插入排序的第一种实现方法一样,更加冗杂,现在我们用插入排序的第二种实现方法来实现希尔排序,同样采用添加外层for循环的方式,来循环对每个子序列进行插入排序。代码如下:

[cpp] view plaincopy

  1. /*
  2. 第二种形式的代码
  3. 对长为len的数组进行一趟增量为ader的插入排序
  4. 本算法在插入排序算法的第三种实现形式上进行修改得到
  5. */
  6. void Shell_Insert2(int *arr,int len,int ader)
  7. {
  8. int i,j,k;
  9. //循环对ader个子序列各自进行插入排序
  10. for(k=0;k<ader;k++)
  11. for(i=ader+k;i<len;i+=ader)
  12. for(j=i-ader;j>=k && arr[j]>arr[j+ader];j-=ader)
  13. {
  14. //交换元素数值
  15. arr[j]^=arr[j+ader];
  16. arr[j+ader]^=arr[j];
  17. arr[j]^=arr[j+ader];
  18. }
  19. }

第二种实现方法的改进

上面的代码中需要三个for循环,因为我们是循环对每个子序列进行插入排序的,实际上我们还可以这样做:对每个子序列交叉进行排序。比如,第1个子序列中
的第2个元素A5(A5表示它在总序列A中的位置序号是5,下同)刚进行完插入排序操作,便接着对第2个子序列中的第2个元素A6进行插入排序操作。这样
我们就可以少写一个for循环,但实际比较的次数还是相同的,只是代码更加简洁。如下:

[cpp] view plaincopy

  1. /*
  2. 在第二种代码的形式上继续精简代码
  3. 交叉进行各个子序列的插入排序
  4. */
  5. void Shell_Insert2_1(int *arr,int len,int ader)
  6. {
  7. int i,j;
  8. //交叉对ader个子序列进行插入排序
  9. for(i=ader;i<len;i++)
  10. for(j=i-ader;j>=0 && arr[j]>arr[j+ader];j-=ader)
  11. {
  12. //交换元素数值
  13. //由于不会出现自己与自己交换的情况
  14. //因此可以安全地用该交换方法
  15. arr[j]^=arr[j+ader];
  16. arr[j+ader]^=arr[j];
  17. arr[j]^=arr[j+ader];
  18. }
  19. }

第三种实现方法

同样,根据插入排序的第三种实现方法,循环逐个对每个子序列进行插入排序操作,我们可以得到希尔排序的实现方法,如下:

[cpp] view plaincopy

  1. /*
  2. 第三种形式的代码
  3. 对长为len的数组进行一趟增量为ader的插入排序
  4. 本算法在插入排序算法的第二种实现形式上进行修改得到
  5. */
  6. void Shell_Insert3(int *arr,int len,int ader)
  7. {
  8. int i,j,k;
  9. //循环对ader个子序列各自进行插入排序
  10. for(k=0;k<ader;k++)
  11. for(i=ader+k;i<len;i+=ader)
  12. if(arr[i] < arr[i-ader])
  13. {
  14. int key = arr[i];
  15. for(j=i-ader;j>=k && arr[j]>key;j-=ader)
  16. arr[j+ader] = arr[j];
  17. arr[j+ader] = key;
  18. }
  19. }

第三种实现方法的改进

我们可以对该方法做出同样的改进,对各个子序列进行交叉排序,代码如下:

[cpp] view plaincopy

  1. /*
  2. 在第三种代码的形式上继续精简代码
  3. 交叉进行各个子序列的插入排序
  4. */
  5. void Shell_Insert3_1(int *arr,int len,int ader)
  6. {
  7. int i,j;
  8. //对ader子序列交叉进行插入排序
  9. for(i=ader;i<len;i++)
  10. if(arr[i] < arr[i-ader])
  11. {
  12. int key = arr[i];
  13. for(j=i-ader;j>=0 && arr[j]>key;j-=ader)
  14. arr[j+ader] = arr[j];
  15. arr[j+ader] = key;
  16. }
  17. }

    同样,如果在面试中要手写希尔排序的代码,推荐这种方法实现的代码。

    希尔排序的时间复杂度根据选择的增量序列不同会有所不同,但一般都会比n*n小(序列长度为n)。

    在选择增量序列时,应使增量序列中的值没有除1之外的公因子,并且最后一个增量值必须为1.

看如下两个增量序列:

n/2、n/4、n/8...1

1、3、7...2^k-1

第一个序列称为希尔增量序列,使用希尔增量时,希尔排序在最坏情况下的时间复杂度为O(n*n)。

第二个序列称为Hibbard增量序列,使用Hibbard增量时,希尔排序在最坏情况下的时间复杂度为O(n^3/2)。

经验研究表明,在实践中使用这些增量序列要比使用上面两个增量序列的效果好的多,其中最好的序列是

{1、5、9、41、109...}

该序列中的项或是9*4^i-9*2^i+1,或是4^i-3*2^i+1。

    希尔排序的性能是完全可以接受的,即时是对数以万计的n来说也是如此。编程的简单特点使得它成为对适度的大量输入数据进行排序时经常选用的算法。

完整代码下载

    各种实现方式的完整的代码打包下载地址:http://download.csdn.net/detail/mmc_maodun/6969381

时间: 2024-10-22 09:00:17

【数据结构与算法】内部排序之一:插入排序和希尔排序的N中实现(不断优化,附完整源码)的相关文章

各种常见的排序,冒泡排序,选择排序,插入排序,希尔排序,堆排序,快速排序,基数排序,桶排序

各种常见的排序 要开始找工作了,把以前学的各种小知识复习一遍,以下是各种常见的排序的简单实现(冒泡排序,选择排序,插入排序,希尔排序,堆排序,快速排序,基数排序,桶排序),至于原理就不写出来了,代码比较简单,看一下就懂,再不行可以随意找本书或百度! #include <iostream> using namespace std; // 冒泡 void BubbleSort(int data[], int length) { if(data == NULL || length <= 0)

常用算法之----选择排序、插入排序和希尔排序

一些说明 我将会写一系列关于算法的博客,因为我是程序员,并不是计算机科学家,也即我是搞工程的,并不是搞学术的,所以对于我来说,最重要的就是 1.有哪些算法 2.这些算法的原理 3.这些算法的实现 4.这些算法的效率 而其他的,相对而言,并没有那么重要,比如算法的证明,所以以后的博客都会按照上述的思维撰写. 一.首先定义一个抽象类,里面集成了排序算法所需要的共同的方法: public abstract class SortBase { public abstract Integer[] sort(

选择排序、插入排序和希尔排序

一些说明 我将会写一系列关于算法的博客,因为我是程序员,并不是计算机科学家,也即我是搞工程的,并不是搞学术的,所以对于我来说,最重要的就是 1.有哪些算法 2.这些算法的原理 3.这些算法的实现 4.这些算法的效率 而其他的,相对而言,并没有那么重要,比如算法的证明,所以以后的博客都会按照上述的思维撰写. 一.首先定义一个抽象类,里面集成了排序算法所需要的共同的方法: public abstract class SortBase { public abstract Integer[] sort(

常见排序算法的实现(归并排序、快速排序、堆排序、选择排序、插入排序、希尔排序)

这篇博客主要实现一些常见的排序算法.例如: //冒泡排序 //选择排序 //简单插入排序 //折半插入排序 //希尔排序 //归并排序 //双向的快速排序 //单向的快速排序 //堆排序 对于各个算法的实现原理,这里不再多说了,代码中注释较多,结合注释应该都能理解算法的原理,读者也可自己google一下.另外,注释中有很多点,比如边界条件.应用场景等已经用 * 标记,* 越多,越应该多注意. 下面是实现: //冒泡排序 void BubbleSort(int *arr, int n) { if(

简单算法和简单逻辑的小软件,是如何获得技术专利的?附完整源码

源码在最后面有下载,是入行一年时的项目.虽然简陋,却是我人生中的重要里程碑.一直想把背后的故事讲出来,但代码长得丑陋不好意思拿出来. 眼见年终了,丑代码放出来图大家一乐,如果有人学到东西或被后面的故事激起斗志,更是功德一件. 功能介绍:一个大片(图中m1-m4四个红色标记点组成),指定旋转角度,最多能切割成多少黑灰色的小片. 输入:见图,小片宽高,大片宽高,角度. 输出:最大切片数. 技术涉及屏幕坐标系,数学和几何计算,画图,多线程. 完整的项目有串口控制通信和一些实际工程的代码,和硬件绑定在一

【数据结构与算法】二叉树递归与非递归遍历(附完整源码)(转)

转自:http://blog.csdn.net/ns_code/article/details/12977901 二叉树是一种非常重要的数据结构,很多其他数据机构都是基于二叉树的基础演变过来的.二叉树有前.中.后三种遍历方式,因为树的本身就是用递归定义的,因此采用递归的方法实现三种遍历,不仅代码简洁且容易理解,但其开销也比较大,而若采用非递归方法实现三种遍历,则要用栈来模拟实现(递归也是用栈实现的).下面先简要介绍三种遍历方式的递归实现,再详细介绍三种遍历方式的非递归实现. 一.三种遍历方式的递

排序(插入排序,希尔排序,选择排序,堆排序)

选择排序 工作原理:每一次从待排序的数据元素中选出最大或最小的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完. void SelectSort(int* a, size_t size) {     assert(a);     for (int i = 0; i < size; i++)     {         int min = i;         for (int j = i + 1; j < size; j++)         {         //选择最小元素

数据结构精要------直接插入排序和希尔排序算法

上篇总结中主要实践了算法的内排序的选择排序,那么接下来我们继续实践插入排序排序的两种:直接插入排序和希尔排序算法. -----直接插入排序 package com.sort; /** * 直接插入排序 * * @author weixing-yang * * 算法思路: * 每步将一个待排序的元素,插入到前面已排序好的一组元素中的适当位置, * 直到所有元素全部出入完成为止. */ public class InsertionSort { public void insertionSort(in

七大内部排序算法总结(插入排序、希尔排序、冒泡排序、简单选择排序、快速排序、归并排序、堆排序)

 写在前面: 排序是计算机程序设计中的一种重要操作,它的功能是将一个数据元素的任意序列,重新排列成一个按关键字有序的序列.因此排序掌握各种排序算法非常重要.对下面介绍的各个排序,我们假定所有排序的关键字都是整数.对传入函数的参数默认是已经检查好了的.只是简单的描述各个算法并给出了具体实现代码,并未做其他深究探讨. 基础知识: 由于待排序的记录数量不同,使得排序过程中设计的存储器不同,可将排序方法分为两大类:一类是内部排序,指的是待排序记录存放在计算机随机存储器中进行的排序过程.另一类是外部排序,