跳转至

希尔排序

一、什么是希尔排序?

  • 希尔排序是一种基于插入排序进行了优化后的排序算法
  • 希尔排序比普通插入排序更适用于大规模数组排序

二、为什么要用希尔排序?

普通的插入排序存在一个问题:

  • 对于大规模乱序数组排序很慢

而它这么慢的原因是:

  • 插入排序只是交换相邻元素,因此元素只能一点点从一端移动到另一端

比如说,数组 [7, 2, 3, 5, 6, 4, 1],插入排序只能一点点地将 1 移动到最前面。

希尔排序为了加快这种情况的排序速度,对插入排序进行了优化:

  • 对数组元素按照指定间隔进行跳跃式的分组
  • 对分组内进行局部排序,交换不相邻的元素
  • 最终用插入排序将局部有序数组进行排序

比如说,数组 [7, 2, 3, 5, 6, 4, 1]。

希尔排序会将其划分为多个组,假设按照间隔为 3 分组,可分成 3 组:

   _______________________________________________
  |                       |                       |
  v                       v                       v
 ___     ___     ___     ___     ___     ___     ___
| 7 |   | 2 |   | 3 |   | 5 |   | 6 |   | 4 |   | 1 |
           _______________________
          |                       |
          v                       v
 ___     ___     ___     ___     ___     ___     ___
| 7 |   | 2 |   | 3 |   | 5 |   | 6 |   | 4 |   | 1 |
                   _______________________
                  |                       |
                  v                       v
 ___     ___     ___     ___     ___     ___     ___
| 7 |   | 2 |   | 3 |   | 5 |   | 6 |   | 4 |   | 1 |

然后分别对每个组内进行排序:

               [7, 5, 1] --> [1, 5, 7]

   _______________________________________________
  |                       |                       |
  v                       v                       v
 ___     ___     ___     ___     ___     ___     ___
| 1 |   | 2 |   | 3 |   | 5 |   | 6 |   | 4 |   | 7 |
              [2, 6] --> [2, 6]

           _______________________
          |                       |
          v                       v
 ___     ___     ___     ___     ___     ___     ___
| 1 |   | 2 |   | 3 |   | 5 |   | 6 |   | 4 |   | 7 |

                      [3, 4] --> [3, 4]

                   _______________________
                  |                       |
                  v                       v
 ___     ___     ___     ___     ___     ___     ___
| 1 |   | 2 |   | 3 |   | 5 |   | 6 |   | 4 |   | 7 |

可以看到,1 已经直接移动到最前面了,从一端移动到另一端的速度很快。

而且,在局部有序的情况下,整个数组看起来也基本是有序的了。

希尔排序就是采用间隔分组排序的方式,来优化对大规模的数组排序。

三、如何实现希尔排序?

希尔排序的基本思想是按照间隔进行跳跃式分组,并对组内进行排序。

其排序过程是这样:

  1. 按照间隔 h 对数组进行跳跃式分组
  2. 对每个分组内的元素进行排序
  3. 缩小间隔 h,重新回到第 1 步,直到 h = 1 为止

这里面有 2 个核心的点:

  • 分组内的排序
  • 间隔 h 的选择和缩小

当间隔 h = 1 时,其实就是普通的插入排序了。

3.1 分组内排序

一般来说,希尔排序的实现中,分组内排序采用的是插入排序。

比如对这个分组进行插入排序:

   _______________________________________________
  |                       |                       |
  v                       v                       v
 ___     ___     ___     ___     ___     ___     ___
| 7 |   | 2 |   | 3 |   | 5 |   | 6 |   | 4 |   | 1 |
   _______________________________________________
  |                       |                       |
  v                       v                       v
 ___     ___     ___     ___     ___     ___     ___
| 5 |   | 2 |   | 3 |   | 7 |   | 6 |   | 4 |   | 1 |           -- (5 插到 7 前)
   _______________________________________________
  |                       |                       |
  v                       v                       v
 ___     ___     ___     ___     ___     ___     ___
| 1 |   | 2 |   | 3 |   | 5 |   | 6 |   | 4 |   | 7 |           -- (1 插到 5 前)

所以说希尔排序本质就是插入排序,只是插入的方式有点不同。

插入排序是相邻元素之间的交换,而希尔排序是跳跃式的交换。

3.2 增量序列

希尔排序中的间隔 h 变化序列,一般称为增量序列。

比如说,开始间隔 h = n / 2,后面以 h = h / 2 缩小增量:

n/2 n/4 n/8 ... 1

那么这样的一个序列,就称为增量序列。

  • 目前来说,希尔排序的增量序列的选择还没有最优的方案

因为增量序列的选择和证明还是一个数学难题。

除了上面取中的增量序列,在《算法》一书中还给出了 2 种增量序列:

1, 4, 13, 40, 121, 364 ... (h = 3 * h + 1)
1, 5, 19, 41, 109, 209, 505, 929, 2161, 3905, 8929, 16001, 36289, 64769, 146305, 260609

希尔排序的性能会受到增量序列的影响,但是目前暂时没有办法证明哪个增量序列是最优的。

四、算法分析

4.1 时间复杂度

  • 目前尚未能准确算出来复杂度
  • 只能知道大概范围:nlogn < O < n^2

4.2 空间复杂度

  • 空间复杂度:O(1)
  • 原地算法

4.3 稳定性

  • 非稳定排序算法

总结

  • 希尔排序是一种基于插入排序进行了优化后的排序算法
  • 希尔排序比普通插入排序更适用于大规模数组排序
  • 希尔排序的关键点包括增量序列的选择和分组内排序
  • 目前还没有找到最优的增量序列
  • 暂时无法证明希尔排序的时间复杂度

参考

https://www.cnblogs.com/chengxiao/p/6104371.html

附录

/**
 * 希尔排序
 * <p>
 * 时间复杂度:暂时无法分析,但是达不到 O(n^2)
 * <p>
 * 空间复杂度:O(1)
 * <p>
 * 稳定性:不稳定
 */
public void shellSort(int[] arr) {
    int n = arr.length;

    // 递增序列:1, 4, 13, 40, 121, ...
    int h = 1;
    while (h < n / 3) {
        h = 3 * h + 1;
    }

    // 增量序列倒序递减至 1
    while (h >= 1) {
        // 局部插入排序
        for (int i = h; i < n; i++) {
            int x = arr[i];
            int j = i;
            for (; j >= h && x < arr[j - h]; j -= h) {
                arr[j] = arr[j - h];
            }
            arr[j] = x;
        }
        // 递减 h 间隔
        h /= 3;
    }
}