排序演算法之冒泡,選擇,插入和希爾

騎摩托馬斯發表於2017-12-28

氣泡排序(Bubble Sort)

氣泡排序的核心部分是雙重巢狀迴圈,持續比較相鄰元素,大的挪到後面,因此大的會逐步往後挪,故稱之為冒泡。

演算法思路

  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個
  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數
  3. 針對所有的元素重複以上的步驟,除了最後一個
  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較

排序演算法之冒泡,選擇,插入和希爾

圖片源自Visualgo

實現

Java

public class BubbleSort {

    public static void main(String[] args) {
        int[] unsortedArray = new int[]{6, 5, 3, 1, 8, 7, 2, 4};
        bubbleSort(unsortedArray);
        System.out.println("After sorted: ");
        for (int number : unsortedArray) {
            System.out.print(" " + number);
        }

    }

    private static void bubbleSort(int[] array) {
        if (array == null || array.length == 0) { // 非法檢查
            return;
        }

        int i, temp, len = array.length;
        boolean changed;
        
        do {
            changed = false;
            len -= 1;
            for (i = 0; i < len; i++) {
                if (arr[i] > arr[i + 1]) {
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    changed = true;
                }
            }
        } while (changed);
    }
}


複製程式碼

Python

#!/usr/bin/env python
# coding=utf-8


def bubble_sort(arrayList):
    length = len(arrayList)

    for i in range(length - 1):
        count = 0

        for j in range(length - 1 - i):
            if (arrayList[j] > arrayList[j + 1]):
                arrayList[j], arrayList[j + 1] = arrayList[j + 1], arrayList[j]
                count += 1

        if count == 0:
            break


if __name__ == "__main__":
    arrayList = [6, 5, 3, 1, 8, 7, 2, 4]
    print("orgin array list: {0}".format(arrayList))
    bubble_sort(arrayList)
    print("after sorted list: {0}".format(arrayList))
複製程式碼

時間複雜度和空間複雜度

最好情況下:正序有序,則只需要比較n次。故為 O(n)

最壞情況下:逆序有序,則需要比較 (n-1)+(n-2)+……+1,故為 O(n^2)

因為需要一個臨時變數來交換元素位置,(另外遍歷序列時自然少不了用一個變數來做索引),所以其空間複雜度為 O(1)

穩定性

排序過程中只交換相鄰兩個元素的位置。因此,當兩個數相等時,是沒必要交換兩個數的位置的。所以它們的相對位置並沒有改變,氣泡排序演算法是穩定的。

總結

如果有 n 個數進行排序,只需將 n - 1 個數歸位,也就是說要進行 n - 1 趟操作。而“每一趟”都需要從第 1 位開始進行相鄰兩個數的比較,將較小的一個數放在後面,比較完畢後向後挪一位繼續比較下面相鄰數的大小,重複此步驟,直到最後一個尚未歸位的數,已經歸位的數則無需再進行比較。

氣泡排序的核心部分是雙重巢狀迴圈,不難看出氣泡排序的時間複雜度是 O(n^2)。這是一個非常高的時間複雜度。所以說氣泡排序除了它迷人的名字之外,似乎沒有什麼值得推薦的

選擇排序(Selection Sort)

選擇排序就是找到陣列中最小元素將其和陣列第一個元素交換位置,然後在剩下的元素中找到最小元素並將其與陣列第二個元素進行交換,以此類推,直至整個陣列排序結束。

演算法思路

  1. 找到陣列中最小元素並將其和陣列第一個元素交換位置
  2. 在剩下的元素中找到最小元素並將其與陣列第二個元素交換,直至整個陣列排序

排序演算法之冒泡,選擇,插入和希爾

圖片源自Visualgo

實現

Java

public class SelectionSort {

    public static void main(String[] args) {
        int[] unsortedArray = new int[]{6, 5, 3, 1, 8, 7, 2, 4};
        selectionSort(unsortedArray);
        System.out.println("After sorted: ");
        for (int number : unsortedArray) {
            System.out.print(" " + number);
        }
    }

    private static void selectionSort(int[] array) {
        if (array == null || array.length == 0) {
            return;
        }

        int length = array.length;
        int min;
        int temp;
        for (int i = 0; i < length - 1; i++) {

            min = i;
            for (int j = length - 1; j > i; j--) {
                if (array[min] > array[j]) {
                    min = j;
                }
            }

            if (array[i] > array[min]) {
                temp = array[i];
                array[i] = array[min];
                array[min] = temp;
            }
        }
    }
}
複製程式碼

Kotlin

object SelectionSortKt {

    fun selectionSort(array: IntArray) {
        if (array.isEmpty()) {
            return
        }

        val size = array.size
        var min: Int

        for (i in 0 until size - 1) {

            min = i
            (size - 1 downTo i)
                    .asSequence()
                    .filter { array[min] > array[it] }
                    .forEach { min = it }

            if (array[min] < array[i]) {
                val temp = array[i]
                array[i] = array[min]
                array[min] = temp
            }
        }
    }
}

fun main(args: Array<String>) {
    val unsortedArray = intArrayOf(6, 5, 3, 1, 8, 7, 2, 4)
    SelectionSortKt.selectionSort(unsortedArray)
    println("After sorted: ")
    unsortedArray.forEach {
        print(" $it")
    }
}
複製程式碼

Python3

#!/usr/bin/env python
# coding=utf-8


def selection_sort(arrayList):
    length = len(arrayList)
    minIndex = 0

    for i in range(length - 1):
        minIndex = i

        for j in range(length - 1, i, - 1):
            if (arrayList[minIndex] > arrayList[j]):
                minIndex = j

        if (arrayList[i] > arrayList[minIndex]):
            arrayList[i], arrayList[minIndex] = arrayList[minIndex], arrayList[
                i]


if __name__ == "__main__":
    arrayList = [6, 5, 3, 1, 8, 7, 2, 4]
    print("orgin array list: {0}".format(arrayList))
    selection_sort(arrayList)
    print("after sorted list: {0}".format(arrayList))

複製程式碼

時間複雜度

最好情況:交換 0 次,但是每次都要找到最小的元素,因此大約必須遍歷 N*N 次,因此為 O(N^2)

最壞情況 & 平均情況下:O(N^2)

穩定性

由於每次都是選取未排序序列 A 中的最小元素 x 與 A 中的第一個元素交換,因此跨距離了,很可能破壞了元素間的相對位置,因此選擇排序是不穩定的

總結

選擇排序是和氣泡排序差不多的一種排序。和氣泡排序交換相連資料不一樣的是,選擇排序只有在確定了最小的資料之後,才會發生交換

插入排序(Insertion Sort)

通過構建有序序列,對於未排序序列,從後向前掃描(對於單向連結串列則只能從前往後遍歷),找到相應位置並插入。實現上通常使用 in-place 排序(需用到 O(1) 的額外空間)

演算法思路

  1. 從第一個元素開始,該元素可以認為已經被排序
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
  3. 如果該元素(已排序)大於新元素,將該元素移到下一位置
  4. 重複步驟 3,直到找到已排序的元素小於或者等於新元素的位置
  5. 將新元素插入到該位置後
  6. 重複步驟 2~5

排序演算法之冒泡,選擇,插入和希爾

圖片源自Visualgo

實現

Java

public class InsertionSort {

    public static void main(String[] args) {
        int[] unsortedArray = new int[]{6, 5, 3, 1, 8, 7, 2, 4};
        insertionSort(unsortedArray);
        System.out.println("After sorted: ");
        for (int number : unsortedArray) {
            System.out.print(" " + number);
        }
    }

    private static void insertionSort(int[] array) {
        if (array == null || array.length == 0) {
            return;
        }

        int length = array.length;
        int i,j;
        for (i = 1; i < length; i ++) {

            int holder = array[i];

            for (j = i; j > 0 && array[j - 1] > holder; j--) {
                    array[j] = array[j - 1];
            }

            // 以下是錯誤的程式碼邏輯,因為必須是滿足 array[j - 1] > holder 才應該 j--
            // 下面程式碼是每次都會執行 j--
//            for (j = i; j > 0; j--) {
//                if (array[j - 1] > holder) {
//                    array[j] = array[j - 1];
//                }
//            }

            array[j] = holder;
        }
    }
}
複製程式碼

Kotlin

object InsertionSortKt {

    fun insertionSort(array: IntArray) {
        if (array.isEmpty()) {
            return
        }

        val size = array.size
        for (i in 1 until size) {

            val holder = array[i]
            var j = i
            while (j > 0 && array[j - 1] > holder) {
                array[j] = array[j - 1]
                j--
            }

            array[j] = holder
        }
    }
}

fun main(args: Array<String>) {
    val unsortedArray = intArrayOf(6, 5, 3, 1, 8, 7, 2, 4)
    InsertionSortKt.insertionSort(unsortedArray)
    println("After sorted: ")
    unsortedArray.forEach {
        print(" $it")
    }
}
複製程式碼

Python3

#!/usr/bin/env python
# coding=utf-8


def insertion_sort(arrayList):
    length = len(arrayList)

    for i in range(1, length):
        holder = arrayList[i]
        j = i
        while(j > 0 and arrayList[j - 1] > holder):
            arrayList[j] = arrayList[j - 1]
            j -= 1

        arrayList[j] = holder


if __name__ == "__main__":
    arrayList = [6, 5, 3, 1, 8, 7, 2, 4]
    print("orgin array list: {0}".format(arrayList))
    insertion_sort(arrayList)
    print("after sorted list: {0}".format(arrayList))
複製程式碼

時間複雜度

最好的情況:正序有序(從小到大),這樣只需要比較 n 次,不需要移動。因此時間複雜度為 O(n)

最壞的情況:逆序有序,這樣每一個元素就需要比較 n 次,共有 n 個元素,因此實際複雜度為 O(n^2)

平均情況:O(n^2)

希爾排序

核心:基於插入排序,使陣列中任意間隔為 h 的元素都是有序的,即將全部元素分為 h 個區域使用插入排序。其實現可類似於插入排序但使用不同增量。更高效的原因是它權衡了子陣列的規模和有序性

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的資料操作時,效率高,即可以達到線性排序的效率
  • 但插入排序一般來說是低效的,因為插入排序每次只能將資料移動一位

演算法實現

通過將比較的全部元素分為幾個區域來提升插入排序的效能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步。然後演算法再取越來越小的步長進行排序,演算法的最後一步就是普通的插入排序,但是到了這步,需排序的資料幾乎是已排好的了(此時插入排序較快)

假設有陣列 array = [80, 93, 60, 12, 42, 30, 68, 85, 10],首先取 d1 = 4,將陣列分為 4 組,如下圖中相同顏色代表一組:

排序演算法之冒泡,選擇,插入和希爾

然後分別對 4 個小組進行插入排序,排序後的結果為:

排序演算法之冒泡,選擇,插入和希爾

然後,取 d2 = 2,將原陣列分為 2 小組,如下圖:

排序演算法之冒泡,選擇,插入和希爾

然後分別對 2 個小組進行插入排序,排序後的結果為:

排序演算法之冒泡,選擇,插入和希爾

最後,取 d3 = 1,進行插入排序後得到最終結果:

排序演算法之冒泡,選擇,插入和希爾

圖片摘自常見排序演算法 - 希爾排序 (Shell Sort)

步長序列

步長的選擇是希爾排序的重要部分。只要最終步長為1任何步長序列都可以工作。演算法最開始以一定的步長進行排序。然後會繼續以一定步長進行排序,最終演算法以步長為1進行排序。當步長為1時,演算法變為插入排序,這就保證了資料一定會被排序。

以下是效能比較好的步長序列

  1. Shell's original sequence: N/2 , N/4 , ..., 1 (repeatedly divide by 2);
  2. Hibbard's increments: 1, 3, 7, ..., 2k - 1 ;
  3. Knuth's increments: 1, 4, 13, ..., (3k - 1) / 2 ;
  4. Sedgewick's increments: 1, 5, 19, 41, 109, ....
    It is obtained by interleaving the elements of two sequences:
    1, 19, 109, 505, 2161,….., 9(4k – 2k) + 1, k = 0, 1, 2, 3,…
    5, 41, 209, 929, 3905,…..2k+2 (2k+2 – 3 ) + 1, k = 0, 1, 2, 3, …

已知的最好步長序列是由Sedgewick提出的(1, 5, 19, 41, 109,...)。這項研究也表明比較在希爾排序中是最主要的操作,而不是交換。用這樣步長序列的希爾排序比插入排序要快,甚至在小陣列中比快速排序和堆排序還快,但是在涉及大量資料時希爾排序還是比快速排序慢。

實現

Java

public class ShellSort {

    public static void main(String[] args) {
        int[] unsortedArray = new int[]{6, 5, 3, 1, 8, 7, 2, 4};
        shellSort(unsortedArray);
        System.out.println("After sorted: ");
        for (int number : unsortedArray) {
            System.out.print(" " + number);
        }

    }

    private static void shellSort(int[] array) {
        if (array == null || array.length == 0) {
            return;
        }

        int length = array.length;

        int gap = length / 2;
        int i, j;

        for (; gap > 0; gap /= 2) { // Shell's original sequence: N/2 , N/4 , ..., 1 (repeatedly divide by 2)
            for (i = gap; i < length; i += gap) {

                int temp = array[i];
                for (j = i; j > 0 && array[j - gap] > temp; j -= gap) {
                    array[j] = array[j - gap];
                }

                array[j] = temp;
            }
        }
    }
}
複製程式碼

Kotlin

object ShellSortKt {

    fun shellSort(array: IntArray) {

        if (array.isEmpty()) {
            return
        }

        val size = array.size
        var gap = size / 2

        while (gap > 0) {

            for (i in gap until size step gap) {

                val temp = array[i]
                var j = i

                while (j > 0 && array[j - gap] > temp) {
                    array[j] = array[j - gap]
                    j -= gap
                }

                array[j] = temp
            }

            gap /= 2
        }
    }
}

fun main(args: Array<String>) {
    val unsortedArray = intArrayOf(6, 5, 3, 1, 8, 7, 2, 4)
    ShellSortKt.shellSort(unsortedArray)
    println("After sorted: ")
    unsortedArray.forEach {
        print(" $it")
    }
}
複製程式碼

Python3

#!/usr/bin/env python
# coding=utf-8


def shell_sort(arrayList):
    length = len(arrayList)

    gap = length // 2
    while (gap > 0):

        for i in range(gap, length, gap):
            holder = arrayList[i]
            j = i
            while (j > 0 and arrayList[j - gap] > holder):
                arrayList[j] = arrayList[j - gap]
                j -= gap

            arrayList[j] = holder

        gap //= 2


if __name__ == "__main__":
    arrayList = [6, 5, 3, 1, 8, 7, 2, 4]
    print("orgin array list: {0}".format(arrayList))
    shell_sort(arrayList)
    print("after sorted list: {0}".format(arrayList))
複製程式碼

總結

一個更好理解的希爾排序實現:將陣列列在一個表中並對列排序(用插入排序)。重複這過程,不過每次用更長的列來進行。最後整個表就只有一列了。將陣列轉換至表是為了更好地理解這演算法,演算法本身僅僅對原陣列進行排序(通過增加索引的步長,例如是用 i += step_size 而不是 i++)

參考

相關文章