希尔排序--基于快速排序的高级排序

有个叫希尔的人提出的算法

据说是基于插入排序的性质进行的优化:
1、插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
2、但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

解释:
假设全局上大的全部在后面,小的前面在前面;局部上也满足这个条件,那么交换的次数将大大降低。
假设最大值在前面,排序完成最大值需要移动n次才能移动到最后,因为每次都只能移动一位。假设我们能一次把这个最大值移动到后面,接近将来目标的位置,效率就提高了很多。

希尔排序思路:
将序列按照步长分组,分别两两取0 0+step, 1 1+step, 2 2+step进行排序,这样大的部分就到了后面
然后按照一定规则,递减步长,直到步长为1,排序完成,,,
只要保证最后一次会用1进行简单插入排序,结果一定是满足排序规则的,前面的过程其实是将数组往前面小,后面大的趋势格式化。所以我们可以借助之前实现的二分插入排序,在外面再套一层循环,循环一个增量序列,将间隔传入内层循环,我的代码也是这么实现的。

序列理论上可以任意选择,只要最后一次序列为1,理论上序列怎么选都可以,当然目的是更快
这个数列如何选取才是最优是个数学难题,至今未有最优解,只有根据经验来的最优解

有多种增量序列,其中最快的是塞奇威克的增量序列
1、希尔(Shell)原始步长序列:N / 2,N / 4,…,1(重复除以2),步长的选择简单粗暴,但是思想很重要;
2、希伯德(Hibbard)的步长序列:1,3,7,…。由前面的数用2 k + 1递归获得;
3、克努特(Knuth)的步长序列:1,4,13,…。由前面的数用3 k + 1递归获得;
4、塞奇威克(Sedgewick) 的步长序列:1,5,19,41,109,….
  它是通过交织两个序列的元素获得的: 步长序列数组下标 n 从0开始,奇数的k值从0开始,逢奇数+1;偶数的下标从2开始,逢偶数+1,定义k1 = 0;k2 = 2;
  n偶数用 :1,19,109,505,2161,…。
  公式为:9(4 ^k1 - 2^ k1)+ 1;然后k1++…
  n奇数用 :5,41,209,929,3905,…。
  公式为:4 ^k2- 3(2^k2)+ 1;然后k2++…

专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法.

时间复杂度:
最差n(log(n)),最快n

稳定性:
不稳定,因为被拆开之后不会相邻比较,所以不能保持稳定性

Java代码实现:
public static void main(String[] args){
int[] arr = new int[]{10, 1, 4 ,2 , 8, 3, 5};

System.out.println(Arrays.toString(indexSort1(arr)));
arr = new int[]{10, 1, 4 ,2 , 8, 3, 5};
System.out.println(Arrays.toString(indexSort2(arr)));
arr = new int[]{10, 1, 4 ,2 , 8, 3, 5};
System.out.println(Arrays.toString(shellIndexSort(arr)));

}

/**
* 相邻逐个比较,找到合适的位置,放入
* @param arr
* @return
*/
private static int[] indexSort1(int[] arr){

// 外层控制次数,从1开始,不必从0开始
for(int i = 1;i < arr.length;i++){
    // 从后向前比较
    int idx = i;
    int value = arr[idx];
    // 1 2 3 4 3
    // 拿着value和前面的数据比,直到找到第一个大于等于的位置,和j比大于等于,那么j+1,就是要插入的位置
    // 找到第一个后面大等于前面的位置,停下来,这个位置后一个位置就是要放入的位置
    // 有可能第一趟就停了下来,这个时候j应该为j+1
    // 2 3 4 1
    // 如果一直找不到,找到0
    for(int j = idx - 1; j >= 0; j--){
        // 记录当前位置
        idx = j;
        if(value >= arr[j]){
            idx = j + 1;
            break;
        }
    }

    // 将从找到的位置开始到i的位置(不包括i)的数据全部后移一位
    for(int j = i - 1; j >= idx; j--){
        arr[j + 1] = arr[j];
    }
    arr[idx] = value;
}
return arr;

}

/**
* 使用二分查找法查找应该插入的位置
* @param arr
* @return
*/
private static int[] indexSort2(int[] arr){
return binarySort(arr, 1);
}

/**
* 使用二分查找的插入排序,使用step作为参数,用来在希尔排序中复用这个方法
* @param arr
* @param step
* @return
*/
private static int[] binarySort(int[] arr, int step){
for(int i = 1; i< arr.length;i++){
int idx = i;
int value = arr[idx];
// 找到应该插入的位置
int insertIndex = binarySearch(arr, value, 0, i - 1);
for(int j = i - 1; j >= insertIndex; j–){
arr[j + 1] = arr[j];
}
arr[insertIndex] = value;
}
return arr;
}

/**
* 递归查找
* @param arr
* @param value
* @param startIndex
* @param endIndex
* @return
*/
private static int binarySearch(int[] arr, int value, int startIndex, int endIndex){
// 推到startIndex = endIndex的时候,就是应该退出的时候
// 假设0-9,获取中间一位4,前半部分[0 - 4] 后半部分 [5 - 9]
// 假设0-8,获取中间一位4,前半部分[0-4] 后半部分[5-8]
// 如果中间一位大于等于,往后找,否则往前找
// 如果找到startIndex等于endIndex的时候,无论如何都应该退出
// 考虑一般情况,如果
// 最终返回值应该是0~endIndex+1之间的数,因为多了一位数,这个数可能在最后
// 举特例:假设往1 或者 2 后面插入2,2的位置应该是endIndex + 1
// 假设往2 里插入1,1的位置应该是0,也就是startIndex
if(startIndex == endIndex){
// 往后往前的逻辑和下面一样其实
if(arr[startIndex] <= value){
return startIndex + 1;
}else{
return startIndex;
}
}
int midIndex = (startIndex + endIndex) / 2;
if(arr[midIndex] <= value){
return binarySearch(arr, value, midIndex + 1, endIndex);
}else{
return binarySearch(arr, value, 0, midIndex);
}
}

/**
* 使用希尔的分组排序,最后直接插入排序
* 其实就是在上面的排序外面再套一层壳子,然后先进行一些少次但是结果很有用的排序,这样后面的排序就会快很多
* @param arr
* @return
*/
private static int[] shellIndexSort(int[] arr){
// 先计算步长数组
int[] gap = getGap(arr.length);

// 从大到小的间隔
for(int i = gap.length - 1; i >= 0; i--){
    // 使用间隔来比较
    arr = binarySort(arr, gap[i]);
}

return arr;

}

/**
* 获取塞奇威克的增量序列,这个公式网上全是错误的,麻痹
* @param len
* @return
*/
private static int[] getGap(int len){
List gap = new ArrayList();
int i = 0;
int startup1 = 0;
int startup2 = 2;
int value = 0;
while(true){
if(i % 2 == 0){
value = (int)(9 * (Math.pow(4, startup1) - Math.pow(2 ,startup1)) + 1);
startup1++;
}else{
value = (int)(Math.pow(4, startup2) - 3 * Math.pow(2 ,startup2) + 1);
startup2++;
}
gap.add(value);
i++;
if(value >= len){
System.out.println(gap);
int[] arr = new int[gap.size()];
for(int j = 0; j < arr.length;j++){
arr[j] = gap.get(j);
}
return arr;
}
}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值