内部排序之一:插入排序和希尔排序的N中实现

2024-09-02 06:08

本文主要是介绍内部排序之一:插入排序和希尔排序的N中实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

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

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

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

插入排序

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

   直接插入排序

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

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

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

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

     第一种实现方法

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

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

第二种实现方法


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

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

第三种实现方法


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

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

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

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



   折半插入排序

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

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

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



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



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

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


   第一种实现方法


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

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

第二种实现方法

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

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

第二种实现方法的改进


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

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

第三种实现方法

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

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

第三种实现方法的改进


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

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

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

   希尔排序的时间复杂度根据选择的增量序列不同会有所不同,但一般都会比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


来源: csdn   作者:兰亭风雨   
源自:http://tech.ddvip.com/2014-03/1394804220209161.html

这篇关于内部排序之一:插入排序和希尔排序的N中实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1129203

相关文章

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

opencv图像处理之指纹验证的实现

《opencv图像处理之指纹验证的实现》本文主要介绍了opencv图像处理之指纹验证的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录一、简介二、具体案例实现1. 图像显示函数2. 指纹验证函数3. 主函数4、运行结果三、总结一、