死磕算法第三彈——排序算法(2)

本文整理來源 《輕鬆學算法——互聯網算法面試寶典》/趙燁 編著java

插入排序

什麼是插入排序

插入排序分爲兩種,一種是直接插入排序,一種是二分法插入排序。這兩種排序實際上都是插入排序,惟一的不一樣就是插入的方式不同。面試

插入排序就是往數列裏面插入數據元素。通常咱們認爲插入排序就是往一個已經排好順序的待排序的數列中插入一個數以後,數列依然有序。算法

二分插入排序應該也是用來分治法的思想去排序的。實際上二分就是使用二分查找來找到這個插入的位置,剩下的插入的思想其實和直接插入排序同樣。shell

因此完成插入排序,就是須要找到這個待插入元素的位置。數組

插入排序的原理

插入排序實際上把待排序的數列分紅了兩部分,一部分已排好序,另外一部分待排序。性能

直接插入排序的整個執行過程:測試

  1. 首先須要明確待排序的數列由兩部分組成,一部分是已排好序的部分,另外一部分是待排序的部分。
  2. 接着咱們每次選擇待排序的部分的第1個元素,分別與前面的元素進行比較。當大於前面的元素時,能夠直接進入已排序的部分;當小於前面的元素時,須要把這個元素拿出來,將前面的元素後移一位,繼續與前面的元素相比,知道比較完數組的第1個元素或者出現一個元素小於咱們拿出來的這個元素,這時中止比較、移動,直到把這個元素盛開到當時的空位上。
  3. 一直重複步驟2,當排序的部分已經沒有元素可進行插入時,中止操做,當前的數列爲已經排好序的數列。

插入排序的實現

首先外層是一個大循環,循環這個待排序的部分數列,內層是分別與前1個元素進行比較、移動,直到找到位置進行插入位置。優化

public class InsertSort {

    private int[] array;

    public InsertSort(int[] array) {
        this.array = array;
    }

    public void sort() {
        if (array == null) {
            throw new RuntimeException("array is null");
        }
        int length = array.length;
        if (length > 0) {
            for (int i = 1; i < length; i++) {
                int temp = array[i];
                int j = i;
                for (; j > 0 && array[j - 1] > temp; j--) {
                    array[j] = array[j - 1];
                }
                array[j] = temp;
            }
        }
    }

    public void print(){
        for (int anArray : array) {
            System.out.println(anArray);
        }
    }
}

測試代碼this

public class InsertSortTest {

    @Test
    public void main(){
        int[] arrays = {5,9,1,9,5,3,7,6,1};
        InsertSort insertSort = new InsertSort(arrays);
        insertSort.sort();
        insertSort.print();
    }

}

插入排序的特色及性能

插入排序的操做很簡單,並且咱們經過實例及原理能夠知道,插入排序在數列近似有序時,效率會很是高,由於這樣會減小比較和移動的次數。code

插入排序的時間複雜度是$O({n}^2)$ ,咱們會發現這個實現是雙重嵌套循環,外層執行n遍,內層在最壞的狀況下執行n遍,並且除了比較操做還有移動操做。最好的狀況是數列近似有序,這時一部份內層循環只須要比較及移動較少的次數就能夠完成排序。若是數列自己已經排好序,那麼插入排序也能夠達到線性實現複雜度及$O(n^2)$,因此咱們應該明確認識到,使用插入排序算法進行排序時,數列越近似有序,性能越高。

插入排序的空間複雜度時$O(1)$,是常量級,因爲在採用插入排序時,咱們只須要使用一個額外的空間來存儲這個"拿出來"的元素,因此插入排序只須要額外的一個空間去作排序,這是常量級的空間消耗。

插入排序時穩定的,因爲數組內部本身排序,把後面的部分按先後順序一點點地比較、移動,能夠保持相對順序不變,因此插入排序是穩定的排序算法。

希爾排序

插入排序算法主要是比較和移動的兩個操做,會致使時間複雜度很大。可是插入排序在序列自己有序時可以達到$O(n)$的時間複雜度,也就是說實際上若是序列自己有必定的有序性,那麼使用插入排序的效率會更高,若是序列自己很短,那麼插入排序的效率會很高。

什麼是希爾排序

希爾排序也是一種插入排序算法,也叫作縮小增量排序,是直接插入排序的一種更高效的改進算法。

希爾排序在插入排序的基礎上,主要經過兩點來改進排序算法:一是在插入排序在對近似有序的數列進行排序時,排序的性能會比較好;二是插入排序的性能比較低效,及每次只能將數據移動一位。

希爾排序的原理

希爾排序的基本思想是:把待排序的數列按照必定的增量分割成多個數列。可是這個子數列不是連續的,二是經過前面提到的增量,按照必定相隔的增量進行分割的,而後對各個子數列進行插入排序,接着增量逐漸減少,而後依然對每部分進行插入排序,在減少到1以後直接使用插入排序處理數列。

特別強調,這裏選擇增量的要求是每次都要減小,知道最後一次變爲1爲止。首選增量通常爲$\frac{n}{2}$ ,n爲待排序的數列長度,而且每次增量都爲上次的$\frac{1}{2}$

希爾排序的實現

希爾排序實際上只是插入排序的改進,在算法實現上,咱們須要額外操做的只有對增量的處理及對數列的分塊處理。

public class ShellSort {
    private int[] array;

    public ShellSort(int[] array) {
        this.array = array;
    }

    public void sort() {
        int temp;
        for (int k = array.length / 2; k > 0; k /= 2) {
            for (int i = k; i < array.length; i++) {
                for (int j = i; j >= k; j -= k) {
                    if (array[j - k] > array[j]) {
                        temp = array[j - k];
                        array[j - k] = array[j];
                        array[j] = temp;
                    }
                }
            }
        }
    }

    public void print() {
        for (int anArray : array) {
            System.out.println(anArray);
        }
    }
}
public class ShellSortTest {

    @Test
    public void main(){
        int[] arrays = {5,9,1,9,5,3,7,6,1};
        ShellSort shellSort= new ShellSort(arrays);
        shellSort.sort();
        shellSort.print();
    }

}

希爾排序的特色及性能

其實希爾排序只使用了一種增量的方式去改進插入排序,從上述對該算法的描述及實例中,咱們可以清楚的知道實際上希爾排序在內部仍是使用插入排序進行處理的。可是這個增量確實有它 意義,無論數列有多長,剛開始時增量會很大,可是數列總體已經開始趨於有序了,因此插入排序的速度仍是會愈來愈快的。

在時間複雜度上,因爲增量的序列不必定,因此時間複雜度也不肯定。這在數學上還沒法給出確切的結果。咱們能夠採用每次除以2的方式,可是研究,有如下幾種推薦序列:

  1. N/3+1,N/3^2+1,N/3^3+1••••••(聽說在序列數N<100000時最優)
  2. $2^{k}-1$,$2^{(k-1)}-1$,$2^{k-2}-1$••••••(設k爲總趟數)
  3. 其餘的還有質數

對於每次除以2的增量選擇,希爾排序的最好狀況固然是自己有序,每次區分都不用排序,時間複雜度是$O(n)$;可是在最壞的狀況下依然每次都須要移動,時間複雜度與直接插入排序在最壞狀況下的時間複雜度同樣$O(n^{2})$

可是通常認爲希爾排序的平均時間複雜度時$O({n}^{1.3})$。固然,希爾排序的時間複雜度與其增量序列有關,通常咱們知道希爾排序會比插入排序快一些,這就足夠了。

在希爾排序的實現中仍然使用了插入排序,只是進行了分組,並無使用其餘空間,因此希爾排序的空間複雜度一樣是$O(1)$,是常量級的。

在希爾排序中會進行分組、排序,因此一樣的元素,其相對位置可能會發生變化,這是由於一樣值的元素若不在一個組中,則有可能後面的元素會被移動到前面。因此希爾排序是不穩定的算法。

希爾排序的適用場景

在使用希爾排序時,須要選擇合適的增量序列做爲排序輔助,而這也是一個比較複雜的抉擇。因此希爾排序在實際使用中並不經常使用。

簡單選擇排序

什麼是選擇排序

選擇排序是一種很是簡單的排序算法,就是在序列中依次選擇最大(或者最小)的數,並將其放到待排序的數列的起始位置。

簡單選擇排序的原理

簡單選擇排序的原理很是簡單,即在待排序的數列中尋找最大(或者最小)的一個數,與第1個元素進行交換,接着在剩餘的待排序的數列中繼續找最大(最小)的一個數,與第2個元素交換。依次類推,一直到待排序的數列中只有一個元素爲止。

也就是說,簡單選擇排序可分爲兩部分,一部分是選擇待排序的數列中最小的一個數,另外一部分是讓這個數與待排序的數列部分的第1個數進行交換,直到待排序的數列只有一個元素,至此整個數列有序。

簡單排序的實現

public class SelectSort {

    private int[] array;

    public SelectSort(int[] array) {
        this.array = array;
    }

    public void sort() {
        int length = array.length;
        for (int i = 0; i < length; i++) {
            int minIndex = i;
            for (int j = i + 1; j < array.length; j++) {
                if (array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                int temp = array[minIndex];
                array[minIndex] = array[i];
                array[i] = temp;
            }
        }
    }

    public void print() {
        for (int anArray : array) {
            System.out.println(anArray);
        }
    }
}

測試代碼

public class SelectSortTest {

    @Test
    public void main(){
        int[] arrays = {5,9,1,9,5,3,7,6,1};
        SelectSort selectSort = new SelectSort(arrays);
        selectSort.sort();
        selectSort.print();
    }

}

選擇排序的特色及性能

因爲在簡單選擇排序中,咱們通常在本來的待排序的數組上排序並交換,基本上使用的都是常量級的額外空間,因此其空間複雜度時$O(1)$

在最好的狀況下,每次都要找的最大(或者最小)的元素就是待排序的數列的第1個元素,也就是說數列自己有序,這樣咱們只須要一次遍歷且不須要交換,便可實現一趟排序;而在最壞的狀況下,每次在數列中要找的元素都不是第1個元素,每次都須要交換。比較的次數只與數列的長度有關,而在外部遍歷整個數列,也與長度有關,因此這樣的雙重循環無論在什麼狀況下,時間複雜度都是$O(n^{2})$,可是因爲選擇有序不須要一個一個地往前移動,而是直接交換,而比較所消耗的CPU要比交換所消耗的CPU小一些,因此選擇排序的時間複雜度相對於冒泡排序會好一些。

簡單選擇排序優化

經過選擇排序的思想,咱們知道選擇排序的一個重要步驟是在待排序的數列中尋找最大(或者最小)的一個元素,那麼如何尋找這個元素就成爲一個能夠優化的點。

另外,咱們每次都要尋找兩個值中的一個最大值,一個是最小值。這時若是須要將數列的最後一個元素進行交換。這樣咱們一次就能尋找兩個元素進行交換,把最大值與待排序的數列的最後一個元素進行交換。這樣咱們一次就可以尋找兩個元素,使外層循環的時間縮短了一半,性能也提升了不少。經過一次遍歷就能夠找到兩個最值,而且沒有其餘性能損耗。

簡單選擇排序

簡單選擇排序並不很常見,它只是選擇排序的一個思想基礎,選擇排序還有其餘方案能夠實現。

小結

類別 排序方法 時間複雜度(平均|最好|最壞) 空間複雜度(輔助存儲) 穩定性
插入排序 直接插入 $O(n^2)$ $O(n)$ $O(n^2)$
插入排序 希爾排序 $O(n^{1.3})$ $O(n)$ $O(n^2)$
選擇排序 簡單選擇 $O(n^2)$ $O(n^2)$ $O(n^2)$
選擇排序 堆排序 $O(nlogn)$ $O(nlogn)$ $O(nlogn)$
交換排序 冒泡排序 $O(n^2)$ $O(n)$ $O(n^2)$
交換排序 快速排序 $O(nlogn)$ $O(nlogn)$ $O(nlogn)$

通常狀況下在選擇排序算法時有限選擇快速排序,雖然堆排序的空間複雜度更低,可是堆排序沒有快速排序簡單。

相關文章
相關標籤/搜索