最近复习了一下七大排序算法,到 “希尔排序” 这里时,看了一下浙大的数据结构视频(网易云课堂)。
看了几分钟就开始尝试着自己用java实现(先不看视频中给出的源代码),结果发现一个有意思的过程,现在记录一下。
要理解 “希尔排序” ,得先理解 “插入排序”,因为前者相当于后者的进阶。
1.插入排序
插入排序的排序策略很简单,将数组分为两堆:已排序 + 未排序,然后将未排序的元素,一个个的插入到前面已排序的数组中。
// 插入排序
public static void insertSort(int[] nums) {
int size = nums.length;
for(int p=1; p<size; p++) {
int insertNum = nums[p]; //待插入的那个元素
int insertIndex = p;
while(insertIndex>=1 && nums[insertIndex-1]>insertNum) {
nums[insertIndex] = nums[insertIndex-1];
insertIndex--;
}
nums[insertIndex] = insertNum;
}
}
2.希尔排序
首先,按照陈越老师的那个例子,自己手动写了一遍,发现跟给出的代码不一样, 但结果也可行。
先看视频中的例子:排序下面的数组。
value | 81 | 94 | 11 | 96 | 12 | 35 | 17 | 95 | 28 | 58 | 41 | 75 | 15 |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
操作方法1:
视频中,先用5间隔排序:
- 待排序的序列为(用下标表示):(0,5,10),(1,6,11),(2,7,12),(3,8),(4,9)
- 每一个上面的序列,都 分别用插入排序
经过5间隔的操作,结果如下图:
value | 35 | 17 | 11 | 28 | 12 | 41 | 75 | 15 | 96 | 58 | 81 | 94 | 95 |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
然后用3间隔排序:
- 待排序的序列为(用下标表示):(0,3,6,9,12),(1,4,7,10),(2,5,8,11)
- 上面的三个序列,都 分别用插入排序
经过3间隔的操作,结果如下图:
value | 28 | 12 | 11 | 35 | 15 | 41 | 58 | 17 | 94 | 75 | 81 | 96 | 95 |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
最后用1间隔排序:
- 待排序的序列为(用下标表示):(0,1,2,3,4,5,6,7,8,9,10,11,12)
- 上面的序列,用插入排序
经过1间隔的操作,结果如下图:
value | 11 | 12 | 15 | 17 | 28 | 35 | 41 | 58 | 75 | 81 | 94 | 95 | 96 |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
操作完毕!
然后我就照着上面的操作顺序,尝试着写代码:
代码1:
// 希尔排序1
public static void shellSort1(int[] nums) {
int size = nums.length;
//增量序列有很多种,此次我们选择(size/2, size/4, size/8, ... ,1)
for(int D=size/2; D>=1; D/=2) {
for(int s=0; s<D; s++) { // 变量s,代表每次 待“插入排序”序列的起始索引位置
//到此处,就确定了一个待“插入排序序列” (s, s+D, s+2D, .....),下面对其进行插入排序
for(int p=s+D; p<=size-1; p+=D) {
int insertNum = nums[p]; //待插入的那个元素
int insertIndex = p;
while(insertIndex>=s+D && nums[insertIndex-D]>insertNum) {
nums[insertIndex] = nums[insertIndex-D];
insertIndex -= D;
}
nums[insertIndex] = insertNum;
}
}
}
}
运行了一下,也没发现有什么问题,可以将上面的例子正确输出。但是总感觉怪怪的,竟然用了四层循环嵌套,会不会影响效率啊?然后看了看源代码,照着源码的思路写下了:
操作方法2:
// 希尔排序2
public static void shellSort2(int[] nums) {
int size = nums.length;
//增量序列有很多种,此次我们选择(size/2, size/4, size/8, ... ,1)
for(int D=size/2; D>=1; D/=2) {
for(int p=D; p<size; p++) {
int insertNum = nums[p]; //待插入的那个元素
int insertIndex = p;
while(insertIndex>=D && nums[insertIndex-D]>insertNum) {
nums[insertIndex] = nums[insertIndex-D];
insertIndex -= D;
}
nums[insertIndex] = insertNum;
}
}
}
尼玛,怎么只有三层循环嵌套就可以了?
仔细想了想,发现原因如下:
仍以最上面的例子为例,三层循环嵌套的那段代码,操作实际上是:
-
5间隔排序,操作的序列下标为:(下面的数字均是指下标)
(0,5)仅将元素5往前插,仅做一次插入
(1,6)仅将元素6往前插,仅做一次插入
(2,7)仅将元素7往前插,仅做一次插入
(3,8)仅将元素8往前插,仅做一次插入
(4,9)仅将元素9往前插,仅做一次插入
(5,10)仅将元素10往前插,仅做一次插入
(1,6,11)仅将元素11往前插,仅做一次插入,序列(1,6)已在前面的过程排序好
(2,7,12)仅将元素12往前插,仅做一次插入,序列(2,7)已在前面的过程排序好
- 3间隔排序,操作的序列下标为:(下面的数字均是指下标)
(0,3)仅将元素3往前插,仅做一次插入
(1,4)仅将元素4往前插,仅做一次插入
(2,5)仅将元素5往前插,仅做一次插入
(0,3,6)仅将元素6往前插,仅做一次插入,序列(0,3)已在前面的过程排序好
(1,4,7)仅将元素7往前插,仅做一次插入,序列(1,4)已在前面的过程排序好
(2,5,8)仅将元素8往前插,仅做一次插入,序列(2,5)已在前面的过程排序好
(0,3,6,9)仅将元素9往前插,仅做一次插入,序列(0,3,6)已在前面的过程排序好
(1,4,7,10)仅将元素10往前插,仅做一次插入,序列(1,4,7)已在前面的过程排序好
(2,5,8,11)仅将元素11往前插,仅做一次插入,序列(2,5,8)已在前面的过程排序好
(0,3,6,9,12)仅将元素12往前插,仅做一次插入,序列(0,3,6,9)已在前面的过程排序好
-
1间隔排序,操作的序列下标为:(下面的数字均是指下标)
(0,1)仅将元素1往前插,仅做一次插入
(0,1,2)仅将元素2往前插,仅做一次插入
(0,1,2,3)仅将元素3往前插,仅做一次插入
(0,1,2,3,4)仅将元素4往前插,仅做一次插入
(0,1,2,3,4,5)仅将元素5往前插,仅做一次插入
(0,1,2,3,4,5,6)仅将元素6往前插,仅做一次插入
(0,1,2,3,4,5,6,7)仅将元素7往前插,仅做一次插入
(0,1,2,3,4,5,6,7,8)仅将元素8往前插,仅做一次插入
(0,1,2,3,4,5,6,7,8,9)仅将元素9往前插,仅做一次插入
(0,1,2,3,4,5,6,7,8,9,10)仅将元素10往前插,仅做一次插入
(0,1,2,3,4,5,6,7,8,9,10,11)仅将元素11往前插,仅做一次插入
(0,1,2,3,4,5,6,7,8,9,10,11,12)仅将元素12往前插,仅做一次插入
完毕!
3.总结
这两段代码(代码1:四层嵌套; 代码2:三层嵌套)起始对应着两种不同的插入方法,本质都是插入排序。
操作方法1:我先确定这些固定间隔的 完整的待排序列(完整的意思就是从头到数组尾部的间隔序列),然后对这些序列进行单独的插入排序。
操作方法2:我不用首先就立马确定一个待排序的序列,而只要知道下一个即将要插入的元素就可以了。在插入过程中,已排序的序列慢慢变多了,变壮大了。而后面未排序的序列,我不考虑。
而关于 “希尔排序” 的时间复杂度,这非常复杂的!只要记住最坏的时间复杂度是 O(n2)