希爾排序同之前介紹的直接插入排序一起屬於插入排序的一種。希爾排序算法是按其設計者希爾(Donald Shell)的名字命名,該算法由1959年公佈,是插入排序的一種更高效的改進版本。它的作法不是每次一個元素挨一個元素的比較。而是初期選用大跨步(增量較大)間隔比較,使記錄跳躍式接近它的排序位置;然後增量縮小;最後增量爲 1 ,這樣記錄移動次數大大減少,提高了排序效率。希爾排序對增量序列的選擇沒有嚴格規定。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
- 但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位
基本思想:先將整個待排序序列按固定步長(增量)劃分成多個子序列,對每個子序列使用直接插入排序進行排序,然後依次縮減步長(增量)再進行排序,直至步長(增量)爲1時,即對全部序列進行一次直接插入排序,保證整個序列被排序。因爲直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率上比前兩種方法有較大提高。
實例分析:(摘自一篇博客)
假設有數組 array = [80, 93, 60, 12, 42, 30, 68, 85, 10],首先取 d1 = 4,將數組分爲 4 組,如下圖中相同顏色代表一組:
然後分別對 4 個小組進行插入排序,排序後的結果爲:
然後,取 d2 = 2,將原數組分爲 2 小組,如下圖:
然後分別對 2 個小組進行插入排序,排序後的結果爲:
最後,取 d3 = 1,進行插入排序後得到最終結果:
實現要點:首先需要合理選擇一個起始步長(增量)d,一般選擇d=n/2(n爲序列長度),以後每次d=d/2,直至d=1。當選擇了一個步長(增量)之後,整個序列被劃分成d個子序列,每個子序列的元素爲i,i+d,i+2d,…,然後對每個子序列使用直接插入排序進行排序,這裏需要注意子序列的元素的間隔是d。當d=1時再進行最後一次直接插入排序,完成之後整個序列也就完成了排序。
Java實現:
package com.vicky.sort;
import java.util.Arrays;
import java.util.Random;
/**
* <p>
* 希爾排序
*
* 基本思想:
* 先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(
* 增量足夠小)時,再對全體元素進行一次直接插入排序。因爲直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,
* 因此希爾排序在時間效率上比前兩種方法有較大提高。
*
* 時間複雜度:根據增量(步長)的不同,最壞情況下的時間複雜度不同。
* 步長序列 Best Worst
* n/2(i) O(n) O(n(2))
* 2(k)-1 O(n) O(n(3/2))
* 2(i)3(j) O(n) O(nlog(2)n)
* 來源:https://zh.wikipedia.org/wiki/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F(維基百科)
*
* 空間複雜度:θ(1)
*
* 穩定性:不穩定
* </p>
*
* @author Vicky
* @date 2015-8-12
*/
public class ShellSort {
/**
* 排序
*
* @param data
* 待排序的數組
*/
public static <T extends Comparable<T>> void sort(T[] data) {
long start = System.nanoTime();
if (null == data) {
throw new NullPointerException("data");
}
if (data.length == 1) {
return;
}
int d = data.length / 2;// 增量
while (d >= 1) {
for (int i = 0; i < d; i++) {
// 對同一組元素進行直接插入排序data[i], data[i+d],..., data[i+nd]
int st = i + d;// 取第二個元素作爲分界點
for (; st < data.length; st += d) {
T temp = data[st];
int j = st - d;
while (j >= 0 && temp.compareTo(data[j]) < 0) {
data[j + d] = data[j];
j -= d;
}
data[j + d] = temp;
}
}
d = d / 2;
}
System.out.println("use time:" + (System.nanoTime() - start) / 1000000);
}
public static void main(String[] args) {
Random ran = new Random();
Integer[] data = new Integer[1000];
for (int i = 0; i < data.length; i++) {
data[i] = ran.nextInt(10000);
}
ShellSort.sort(data);
System.out.println(Arrays.toString(data));
}
}
代碼使用了泛型,同時附帶一個測試。可以將希爾排序的性能與直接插入排序進行對比,當數據量超過1000時,希爾排序的性能明顯比直接插入排序快很多倍。
效率分析:
(1)時間複雜度:希爾排序是直接插入排序的一種改進,故其最優情況下的時間複雜度爲O(n)。最壞情況下的時間複雜度根據步長(增量)的選擇而不同。下面是摘自維基百科對希爾排序步長選擇的介紹:
Donald Shell最初建議步長選擇爲n/2並且對步長取半直到步長達到1,雖然這樣取可以比O(n^2)類的算法(插入排序)更好,但這樣仍然有減少平均時間和最差時間的餘地。可能希爾排序最重要的地方在於當用較小步長排序後,以前用的較大步長仍然是有序的。比如,如果一個數列以步長5進行了排序然後再以步長3進行排序,那麼該數列不僅是以步長3有序,而且是以步長5有序。如果不是這樣,那麼算法在迭代過程中會打亂以前的順序,那就不會以如此短的時間完成排序了。已知的最好步長序列是由Sedgewick提出的(1, 5, 19, 41, 109,…),該序列的項來自9 * 4^i - 9 * 2^i + 1和2^{i+2} * (2^{i+2} - 3) + 1這兩個算式[1]。這項研究也表明“比較在希爾排序中是最主要的操作,而不是交換。”用這樣步長序列的希爾排序比插入排序和堆排序都要快,甚至在小數組中比快速排序還快,但是在涉及大量數據時希爾排序還是比快速排序慢。
步長序列 | Best複雜度 | Worst複雜度 |
---|---|---|
n/2^i | O(n) | O(n^2) |
2^k-1 | O(n) | O(n^{3/2}) |
2^i3^j | O(n) | O(nlog^{2}n) |
(2)空間複雜度
首先從空間來看,它只需要一個元素的輔助空間,用於元素的位置交換O(1)
(3)穩定性:
不穩定。 根據步長的不同,相同元素的位置會有變化,所以希爾排序不是穩定排序。
參考文章:
常見排序算法 - 希爾排序 (Shell Sort)
希爾排序
[演算法] 希爾排序法(Shell Sort)