排序算法系列——希爾排序

希爾排序同之前介紹的直接插入排序一起屬於插入排序的一種。希爾排序算法是按其設計者希爾(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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章