一/😋案例步骤分解
二/😁Java代码实现—— 锻炼逻辑思维,代码不是必要 还是挺重要的
三/🧐还有一个问题—GAP怎么界定?
四/😎时间复杂度、空间复杂度及稳定性
希尔排序是D.L.Shell于1959年提出的因而得名。希尔排序就是插入排序的一种优化升级版,同时它是第一批在时间复杂度上突破O(n^2)的排序算法,所以意义深远。希尔排序会事先规定一个间隔值 gap 。假设有一个长度为n的数组arr,希尔排序会先对arr的子数组{arr[0],arr[gap],arr[gap2],…}进行简单插入排序,再对子数组{arr[1],arr[1+gap],arr[1+gap2],…}进行简单插入排序,这样依次进行直到子数组
{arr[gap-1],arr[gap2-1],arr[gap3-1],…}
- 我们先来回顾一下插入排序(图片来源于网络)插入排序的算法虽然不如选择排序和冒泡排序简单粗暴,但其原理打过牌的人都能理解——斗地主时抓到一张牌你会按大小顺序插入手中的牌,比如你手里现在有黑桃3,红桃4,梅花6和方块K并已经按顺序排好,这时候你抓到了一张梅花5,你会把它放到红桃4和梅花6的中间。✅相比计算机人脑是一个更完善的机器,再加上多年的练习,这个过程在你的大脑中是一瞬间完成的以至于你可能都未曾意识到。☄🌠无形当中其实你已经在大脑中对其进行了一遍插入排序➡5和K比,比K小➡再和6比,比6小➡继续和4比,比4大➡至此结束,5就插到6和4中间。
- 那为什么说希尔排序是针对插入排序的一种优化呢? 我们都知道,插入排序在两种情况下工作量最小
- 在大多数元素基本有序的情况下。
元素基本有序的话,元素之间也就不需要频繁地交换,工作量自然减少 - 在元素数量比较少的情况下。
元素数量n变小,时间复杂度O(n^2)自然也变小。
有人👀说:这怎么可能?(图片来源:https://baijiahao.baidu.com/s?id=1644158198885715432&wfr=spider&for=pc)
(文中小老鼠图片来源:https://baijiahao.baidu.com/s?id=1644158198885715432&wfr=spider&for=pc)
当然可能🧔希尔排序就是基于这样的思想。先规定一个间隔,按间隔组成多个子数组,分别对子数组进行直接插入排序——这时候每个字数组符合元素数量比较少的情况;然后渐渐缩小间隔值,直到间隔为1是就是直接插入排序——这时候数组已经基本有序。
🌝接下来通过一个案例来说明吧。
一/😋案例步骤分解
假设定义一个数组int[] arr={9,8,7,6,5,4,3,2,1}如下,长度为9,这里先规定gap=4,其希尔排序流程如下 (颜色相同的是一组)
- 先对子数组{9,5,1}进行插入排序,其他位置元素不变,结果如下
- 再对{8,4}插入排序,其他位置不变,结果如下
- 依次再对{7,3}{6,2}{5,9}重复以上动作,这里不一一演示了,全部完成后结果如下:
-
gap=4的情况就全部排完了,这时候令gap缩小为2,重复以上动作;
排完序是这样
-
这时候其实已经排完了,再令gap=1(就是简单插入排序了)走一遍流程,最后得出排好序的数组
二/😁Java代码实现—— 锻炼逻辑思维,代码不是必要 还是挺重要的
public class ShellSortPra {
public static void main(String[] args) {
int[] arr = {9, 8, 7, 6, 5, 4, 3, 2, 1};
sort(arr);
print(arr);
}
//希尔排序算法的排序主方法
static void sort(int[] arr) {
int gap;
for (gap = 4; gap > 0; gap /= 2) {//规定间隔为4,之后每次减半
for (int j = gap; j < arr.length; j++) {
for (int i = j; i > gap - 1; i -= gap) {
if (arr[i] < arr[i - gap]) {
swap(arr, i, i - gap);
System.out.println("gap=" + gap + " j=" + j + " i=" + i);
print(arr);
System.out.println("\n");
} else {
System.out.println("gap=" + gap + " j=" + j + " i=" + i);
print(arr);
System.out.println("\n");
}
}
}
}
}
//打印数组的方法封装
static void print(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
//交换元素的方法封装
static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
🐱💻这段代码是怎么思考出来的?
由于是直接插入排序的改造,所以我们基于直接插入排序的代码,来贴上
static void sort(int[] arr) {
for (int j = 1; j < arr.length; j++) {
for (int i = j; i > 0; i--) {
if (arr[i] < arr[i - 1]) {
swap(arr, i, i - 1);
}
}
}
}
- 第一步就是先定义gap=4;
- 修改外层循环。由于直接插入排序就是gap=1,可以推出第二步外层循环j的初始值应该赋值为gap,后面的 j < arr.length; j++不变。j++(每次自增1)为什么不变?j 是内循环 i 的初始值(也是本次计算子数组的末位值的索引),内循环一次可以看成是子数组的一次直接插入排序;而 j 自增一次就规定了下一个计算的子数组的末位值,因为各个子数组的末位值是一一紧靠排列的,因此 j 自然也是每次自增1;
- 修改内层循环。内循环的条件即是构成了间隔为gap的子数组,所以 i>0应该改为 i > gap-1; i-- (每次自减1)改成 i -= gap(每次自减gap);同时if语句的条件中的索引 i - 1 也要改成 i - gap,同时修改swap方法中的相应变量值。
- 最后加上gap值的循环,这里设定为每次自除以2。
🧑修改后的sort方法如下:
static void sort(int[] arr) {
int gap;
for (gap = 4; gap > 0; gap /= 2) {
for (int j = gap; j < arr.length; j++) {
for (int i = j; i > gap - 1; i -= gap) {
if (arr[i] < arr[i - 1]) {
swap(arr, i, i - gap);
}
}
}
}
}
*大家可以看到我在循环里加入了一段小代码
System.out.println("gap=" + gap + " j=" + j + " i=" + i);
print(arr);
System.out.println("\n");
来体现这个程序的计算步骤,也就是第一步的“步骤分解”,这样更便于直观地理解算法的运行流程,大家可以自己尝试一下,运行结果:
gap=4 j=4 i=4
5 8 7 6 9 4 3 2 1
gap=4 j=5 i=5
5 4 7 6 9 8 3 2 1
gap=4 j=6 i=6
5 4 3 6 9 8 7 2 1
gap=4 j=7 i=7
5 4 3 2 9 8 7 6 1
gap=4 j=8 i=8
5 4 3 2 1 8 7 6 9
gap=4 j=8 i=4
1 4 3 2 5 8 7 6 9
gap=2 j=2 i=2
1 4 3 2 5 8 7 6 9
gap=2 j=3 i=3
1 2 3 4 5 8 7 6 9
gap=2 j=4 i=4
1 2 3 4 5 8 7 6 9
gap=2 j=4 i=2
1 2 3 4 5 8 7 6 9
gap=2 j=5 i=5
1 2 3 4 5 8 7 6 9
gap=2 j=5 i=3
1 2 3 4 5 8 7 6 9
gap=2 j=6 i=6
1 2 3 4 5 8 7 6 9
gap=2 j=6 i=4
1 2 3 4 5 8 7 6 9
gap=2 j=6 i=2
1 2 3 4 5 8 7 6 9
gap=2 j=7 i=7
1 2 3 4 5 6 7 8 9
gap=2 j=7 i=5
1 2 3 4 5 6 7 8 9
gap=2 j=7 i=3
1 2 3 4 5 6 7 8 9
gap=2 j=8 i=8
1 2 3 4 5 6 7 8 9
gap=2 j=8 i=6
1 2 3 4 5 6 7 8 9
gap=2 j=8 i=4
1 2 3 4 5 6 7 8 9
gap=2 j=8 i=2
1 2 3 4 5 6 7 8 9
gap=1 j=1 i=1
1 2 3 4 5 6 7 8 9
gap=1 j=2 i=2
1 2 3 4 5 6 7 8 9
gap=1 j=2 i=1
1 2 3 4 5 6 7 8 9
gap=1 j=3 i=3
1 2 3 4 5 6 7 8 9
gap=1 j=3 i=2
1 2 3 4 5 6 7 8 9
gap=1 j=3 i=1
1 2 3 4 5 6 7 8 9
gap=1 j=4 i=4
1 2 3 4 5 6 7 8 9
gap=1 j=4 i=3
1 2 3 4 5 6 7 8 9
gap=1 j=4 i=2
1 2 3 4 5 6 7 8 9
gap=1 j=4 i=1
1 2 3 4 5 6 7 8 9
gap=1 j=5 i=5
1 2 3 4 5 6 7 8 9
gap=1 j=5 i=4
1 2 3 4 5 6 7 8 9
gap=1 j=5 i=3
1 2 3 4 5 6 7 8 9
gap=1 j=5 i=2
1 2 3 4 5 6 7 8 9
gap=1 j=5 i=1
1 2 3 4 5 6 7 8 9
gap=1 j=6 i=6
1 2 3 4 5 6 7 8 9
gap=1 j=6 i=5
1 2 3 4 5 6 7 8 9
gap=1 j=6 i=4
1 2 3 4 5 6 7 8 9
gap=1 j=6 i=3
1 2 3 4 5 6 7 8 9
gap=1 j=6 i=2
1 2 3 4 5 6 7 8 9
gap=1 j=6 i=1
1 2 3 4 5 6 7 8 9
gap=1 j=7 i=7
1 2 3 4 5 6 7 8 9
gap=1 j=7 i=6
1 2 3 4 5 6 7 8 9
gap=1 j=7 i=5
1 2 3 4 5 6 7 8 9
gap=1 j=7 i=4
1 2 3 4 5 6 7 8 9
gap=1 j=7 i=3
1 2 3 4 5 6 7 8 9
gap=1 j=7 i=2
1 2 3 4 5 6 7 8 9
gap=1 j=7 i=1
1 2 3 4 5 6 7 8 9
gap=1 j=8 i=8
1 2 3 4 5 6 7 8 9
gap=1 j=8 i=7
1 2 3 4 5 6 7 8 9
gap=1 j=8 i=6
1 2 3 4 5 6 7 8 9
gap=1 j=8 i=5
1 2 3 4 5 6 7 8 9
gap=1 j=8 i=4
1 2 3 4 5 6 7 8 9
gap=1 j=8 i=3
1 2 3 4 5 6 7 8 9
gap=1 j=8 i=2
1 2 3 4 5 6 7 8 9
gap=1 j=8 i=1
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9
Process finished with exit code 0
三/🧐还有一个问题—GAP怎么界定?
上面我们直接把gap定为4,是出于最简单的一种考虑——把数组一分为2,由于数组长度为9,则9/2=4。然而这样的想法是最优的吗?🤫😏很遗憾不是的,这里介绍一个Knuth序列(Donald.Knuth发明),
h=3*h+1
以此来界定间隔序列会效率更高(别问我为啥,我也没搞懂,先记住)h尽可能取大,直到要到数组长度的三分之一了则停止,作为初始的gap值,采用Knuth序列的sort方法改成如下:
static void sort(int[] arr) {
int h = 1;
while (h <= arr.length / 3) {
h = h * 3 + 1;
} //得出h=4,真巧
for (int gap = h; gap > 0; gap = (gap - 1) / 3) {
for (int j = gap; j < arr.length; j++) {
for (int i = j; i > gap - 1; i -= gap) {
if (arr[i] < arr[i - gap]) {
swap(arr, i, i - gap);
System.out.println("gap=" + gap + " j=" + j + " i=" + i);
print(arr);
System.out.println("\n");
} else {
System.out.println("gap=" + gap + " j=" + j + " i=" + i);
print(arr);
System.out.println("\n");
}
}
}
}
}
四/😎时间复杂度、空间复杂度及稳定性
- 希尔排序时间复杂度的最好情况是O(n^1.3),最坏是O(n ^2)
- 希尔排序并没有用到额外的空间,因此空间复杂度是O(1)
- 希尔排序是不稳定的,就是说数值相同的元素经过希尔排序后可能会调换顺序。
看这样一个例子,数组int[] arr={6,1,2,6,4,3},用Knuth序列定义gap=4
在第一轮排序后橙色的6和4交换,就到了紫色6的后面,最后结果: