大家好,欢迎来到IT知识分享网。
>>希尔排序
希尔排序是在插入排序的基础上编写的,由于插入排序元素移动时每次只移动一个位置,所以在数组较大的情况下,效率会很低,因此引入了希尔排序。
希尔排序的思路: 给定一个数组,引入增量,将数组在逻辑上分为几个组,每个组的元素下标相差一个增量,再对这个逻辑上数组进行插入排序。
平均时间复杂度: O(n log n)
最好情况: O(n log^2 n)
最坏情况: O(n log^2 n)
空间复杂度: O(1)
稳定性: 不稳定
幼不晓事,初出牛犊,脑洞大开,写了四种希尔排序,其中第一种是标准按照资料写的,第二种是按照算法给出的思路自己写的,后两种是脑洞大开写的。
: ) 第一种:标准的希尔排序,轮流循环每组元素
代码如下:
/* * 这是,对每个组轮流插入排序。 * * 结论:第一种和第二种插入次数相同,但是第二种更加简洁明了。而第一种显得臃肿。 * */ public static void shellSort2(int[] value,int length){
for(int p=length/2;p>0;p/=2){
for(int i=p;i<length;i++){
insertCount2++; //计数用,与算法无关。 int temp=value[i]; int k = i-p; while(k>=0&&value[k]>temp){
value[k+p]=value[k]; k-=p; } value[k+p]=temp; } } }
对每个逻辑组轮流插入排序,其实从代码for(int i=p;i<length;i++)
可以看出,这完全就是遍历 p 之后的数。之所以如此是因为,插入排序将第一个元素作为已排序列,而 p 之前的数,都是每组第一个元素。
其中第一层循环中的 p 代表的是增量,利用这个增量分割数组。
这种方式代码简洁明了,看上去很舒服,不亏是经典写法。
: ) 第二种:排完一组,再排另一组
代码如下:
/* * 这是,对每个组轮流插入排序。 * * 结论:第一种和第二种插入次数相同,但是第二种更加简洁明了。而第一种显得臃肿。 * */ public static void shellSort2(int[] value,int length){
for(int p=length/2;p>0;p/=2){
for(int i=p;i<length;i++){
insertCount2++; //计数用,与算法无关。 int temp=value[i]; int k = i-p; while(k>=0&&value[k]>temp){
value[k+p]=value[k]; k-=p; } value[k+p]=temp; } } }
这中方式,在逻辑上并没有错误,完全可,但是与第一种想必,就显得臃肿了很多,所以使用优先率肯定没有第一种高。
在实际运行中,我对两种方式插入元素的次数进行了统计,如代码中的insertCount
,insertCount2
,最中结果是,插入次数完全相等(如下图)。
: ) 第三种:自创的,希尔排序二分法版本
温馨提示:内容纯属虚构,请勿模仿 …
代码如下:
/* * 在第二种基础上使用二分插入排序。 * * 问题在于:如何找到每组的第一个元素? 可以中 i%p i是当前元素下标,p是增量。 * * 中间元素如何取? * */ public static void shellSort3(int[] value,int length){
for(int p=length/2;p>0;p/=2){
for(int i=p;i<length;i++){
int left = i%p; int right = i-p; int temp = value[i]; while(left<=right){
/* * 解析这个式子: * right-left,得到差值 * (right-left)/p+1,得到left和right之间共有几个数, * 之所以加1,是因为(right-left)/p,并没有包含left这一项。 * ((right-left)/p+2)/2,得到中间项在第几个, * 之所是加2,其实包含了两次加1,第一次加1是因为,(right-left)/p除出来,少了第一项,第二次加1是因为,这样除2之后能直接得到中间项。 * (((right-left)/p+1)/2-1)*p,得到中间项的left距中间项的差值 * 之所以要减1,是因为,这个这里计算出来的项数多了left自身。 * 最后加上left,得到中间项的值。 * * 如: * 0,3,6,9,12 * 其中:left = 0 , right = 12 , p = 3 * a = right-left = 12;(差值为12) * b = a/p = 4; * c = b+2 = 6;(第一次加1等于5,说明共有5项,第二次加1是为了得到中间项3) * d = c/2 = 3;(第三个为中间项) * e = d-1 = 2;(left偏移中间项两个增量) * f = e*p = 6;(偏移量为6) * g = f+left = 6; * 最后得到中间项是6 * */ int mid = (((right-left)/p+2)/2-1)*p+left; if(value[mid]>temp) right-=p; else left+=p; } //移动 for(int j=i-p;j>=left;j-=p){
value[j+p]=value[j]; } value[left]=temp; } } }
当然,完全是能运行的。
改变主要是将前两中的插入部分,改成了二分插入,其中在写的过程,还是遇到很多问题的,其中最大的问题是中间项如何确定?,连吃午饭都在想,最总结出int mid = (((right-left)/p+2)/2-1)*p+left;
这个公式,来计算带有增量的中间项。在代码注释中已经给出解释,其实也不难想象。
: ) 第四种:自创的,希尔排序快排版本
温馨提示:内容过于浮夸,请未成年人止步 …
代码如下:
/* * 在第二种基础上使用快速排序 * */ public static void shellSort4(int[] value,int length){
for(int p=length/2;p>0;p/=2){
for(int i=p;i<length;i++){
quikSort(value,i,p); } } } public static void quikSort(int[] value,int i,int p){
int start = i%p; int end = i; quikSort(value,start,end,p); } public static void quikSort(int[] value,int start,int end,int p){
if(start<end){
int i=start-p; int j=start; int temp=value[end]; while(j<=end){
if(value[j]<=temp){
i+=p; swap(value,i,j); } j+=p; } quikSort(value,start,i-p,p); quikSort(value,i+p,end,p); } } private static void swap(int[] value,int i,int j){
int temp=value[i]; value[i]=value[j]; value[j]=temp; }
别,你别乱想,它当然能顺利完成任务!就像调皮捣蛋的孩子喜欢冒险,但他总是知道回家的。
这里对 i 就不再是使用插入排序了,也就是说,现在我把它以及它之前的数当成一个待排数组。
在第一次调用时传入 i 和增量 ,调用的这个函数,只是真正排序函数的重载,它只为了过度到能使用递归函数重载。
最后在实现排序时,要注意的就是对循环数的增减,一定要是用 增量 p。
: ) 运行结果
main函数代码如下:
public static void main(String[] args){
Random rand = new Random(); int length = 10;//Math.abs(rand.nextInt()%1000); int[] s = new int[length]; for(int i=0;i<length;i++){
s[i] = Math.abs(rand.nextInt()%1000); } int[] sc = Arrays.copyOf(s,length); int[] sd = Arrays.copyOf(s,length); System.out.println(Arrays.toString(s)+"\n正在排序中..."); long s_time_start=System.nanoTime(); shellSort(s,length); long s_time_end=System.nanoTime(); long sc_time_start=System.nanoTime(); shellSort2(sc,length); long sc_time_end=System.nanoTime(); long sd_time_start=System.nanoTime(); shellSort3(sd,length); long sd_time_end=System.nanoTime(); System.out.println(Arrays.toString(s)); System.out.println("先对一组排序再排另一组所用时间:"+(s_time_end-s_time_start)); System.out.println("每组轮流插入所用时间:"+(sc_time_end-sc_time_start)+" 排序是否成功:"+check(sc,s)); System.out.println("使用二分法所用时间: "+(sd_time_end-sd_time_start)+" 排序是否成功:"+check(sd,s)); //运行希尔排序,快排版本。 int[] se = Arrays.copyOf(s,length); long se_time_start=System.nanoTime(); shellSort4(se,se.length); long se_time_end=System.nanoTime(); System.out.println("使用快速排序所用时间:"+(se_time_end-se_time_start)+" 排序是否成功: "+check(se,s)); System.out.println("\n\ninsertCount: "+insertCount+"\ninsertCount2: "+insertCount2); }
运行结果:(时间单位 纳秒)
以上是部分的运行结果,但是从所有运行情况看的出,他们的优劣很明显。
很明显标准写法是最高效的,而与插入排序关系已经扯的很远的快拍版本效率十分低下,所以虽然看上去有些荒唐的写法,还是给了我好处,那就是强烈的告诉我:希尔排序是对针对插入排序的优化。
所以啊,世间万事无绝对,塞翁失马焉知非福。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/154651.html