Java内部排序(七)-(插入排序算法之Shell排序)

对于直接插入排序而言,当插入排序执行到一半时,待插值左边的所有数据都已经处于有序状态,直接插入排序将待插值存储在一个临时变量里,然后,从待插值左边第一个数据单元开始,只要该数据单元的值大于待插值,该数据单元就右移一格,直到找到第一个小于待插值的数据单元,接下来,将临时变量里的值放入小于待插值的数据单元之后(前面的所有数据都右移一格,因此该数据单元有一个空格)。

从上面算法可以发现一个问题:如果一个很小的数据单元位于很靠近右端的位置上,为了把这个数据单元移动到左边正确的位置上,中间所有的数据单元都得向右移一格,这个步骤都每一个数据项都执行了近 n 次的复制。虽然不是所有的数据项都得移动 n  个位置,但平均下来,每个数据项都会移动 n/2 格,总共是n^2/2 次复制,因此插入排序的执行效率是 O(n^2)。


Shell排序对直接插入排序进行了简单的改进:它通过加大插入排序中元素之间的间隔,并在这些个有间隔的元素进行插入排序,从而使数据项大跨度地移动。当这些数据项排过一趟序后,Shell排序算法减小数据项的隔间再进行排序。依次下去,这些进行排序的数据项之间的间隔被称之为增量,习惯上用 h 来表示这个增量。

下面以如下数据序列为例,进行说明。

9 , -16, 21*, 23, -30, -49, 21, 30*, 30

如果采用直接插入排序算法:

-16, 9, 21*, 23, -30, -49, 21, 30*, 30——第一趟,将第二个数据插入,前两个元素有序

-16, 9, 21*, 23, -30, -49, 21, 30*, 30——第二趟,将第三个数据插入,前三个元素有序

-16, 9, 21*, 23, -30, -49, 21, 30*, 30——第三趟,将第四个数据插入,前四个元素有序

-30, -16, 9, 21*, 23, -49, 21, 30*, 30——第四趟,将第五个数据插入,前五个元素有序

……

Shell排序就不一样了。假设本次Shell排序的 h 为 4,其插入操作如下:

9 , -16, 21*, 23, -30, -49, 21, 30*, 30


-30, -16, 21*, 23,9, -49, 21, 30*,30

-30, -49, 21*, 23, 9, -16, 21, 30*,30

-30, -49, 21*, 23, 9, -16, 21, 30*,30

-30, -49, 21*, 23, 9, -16, 21, 30*,30

-30, -49, 21*, 23, 9, -16, 21, 30*,30

注意上面排序过程中的粗体部分。

当 h 增量为4时,第一趟将保证索引为 0 ,4,8 的数据元素已经有序。第一趟完成后,算法向右一步,对索引 1,5 的数据元素进行排序。这个排序过程保持持续进行,直到所有的数据项都完成了以4为增量的排序。

当完成以4为增量的Shell排序后,所有元素;离他的最终有序位置相差不到两个单元,这就是数组的“基本有序”的含义,也正是Shell排序的奥义所在。通过这种交错的内部有序的数据项集合,就可以减少直接插入排序算法中数据项“整体搬家”的工作量。


常用的 h 序列由Knuth提出,该序列从 1 开始,通过如下公式产生:

h=3 * h + 1

可以看到常用的 h 序列的值有:1 、4、13、 40……反过来,程序中还要方向计算 h 的值:

h= (h - 1)/ 3

下面是一个简单的Shell排序实现:

模拟数据和直接插入排序一致:-56, -21, 56, -21*, 20, 56*, -123, 9, 26, 33。

public class ShellSort {
	public static void shellSort(DataWrap[] data){
		System.out.println("-开始排序-");
		int arrayLength = data.length;
		//增量的初始值为 1 ,同时也说明了直接插入排序算法是Shell排序算法当 h=1 时的一个特例
		int h = 1;
		//求h的最大值,
		while( h <= arrayLength/3 ){
			h = 3 * h + 1;
		}
		while(h > 0){
			System.out.println("当h等于:"+h);
			//从第h个元素开始判断
			for(int i = h; i < arrayLength; i ++){
				//存放当前索引处的数据元素。
				DataWrap dw = data[i];
				if(data[i].compareTo(data[i - h]) < 0){ //说明当前数据元素比它相邻的前面一个元素要小
					int j = i - h;
					for(; j >= 0 && data[j].compareTo(dw) > 0; j -= h){
						data[j + h] = data[j];
					}
					data[j + h] = dw;
				}

				System.out.println(java.util.Arrays.toString(data));
			}
			h = (h - 1)/3;
		}		
	}


	public static void main(String[] args){
		DataWrap[] data = {new DataWrap(-56,""),
				new DataWrap(-21,""),
				new DataWrap(56,""),
				new DataWrap(-21,"*"),
				new DataWrap(20,""),
				new DataWrap(56,"*"),
				new DataWrap(-123,""),
				new DataWrap(9,""),
				new DataWrap(26,""),
				new DataWrap(33,"")};
		System.out.println("-排序前-"+java.util.Arrays.toString(data))
		shellSort(data);
		System.out.println("-排序后-"+java.util.Arrays.toString(data));
	}
}
运行结果为:

Shell排序是直接插入排序的改进版,因此它是稳定的。它的空间开销也是O(1)。时间开销估计在O(n的二分之三次幂)~O(n的六分之七次幂)之间,具体怎么得来的楼主暂不知。

参考资料:《疯狂Java程序员的基本修养》  --李刚 编著



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值