1T數(shù)據(jù)快速排序!十種經(jīng)典排序算法總結(jié)

1 冒泡排序

每次循環(huán)都比較前后兩個元素的大小,如果前者大于后者,則將兩者進行交換。這樣做會將每次循環(huán)中最大的元素替換到末尾,逐漸形成有序集合。將每次循環(huán)中的最大元素逐漸由隊首轉(zhuǎn)移到隊尾的過程形似“冒泡”過程,故因此得名。

一個優(yōu)化冒泡排序的方法就是如果在一次循環(huán)的過程中沒有發(fā)生交換,則可以立即退出當前循環(huán),因為此時已經(jīng)排好序了(也就是時間復雜度最好情況下是
O(n)

的由來)。

public int[] bubbleSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    for (int i = 0; i < array.length - 1; i++) {
        boolean flag = false;
        for (int j = 0; j < array.length - 1 - i; j++) {
            if (array[j] > array[j + 1]) {
                //這里交換兩個數(shù)據(jù)并沒有使用中間變量,而是使用異或的方式來實現(xiàn)
                array[j] = array[j] ^ array[j + 1];
                array[j + 1] = array[j] ^ array[j + 1];
                array[j] = array[j] ^ array[j + 1];

                flag = true;
            }
        }
        if (!flag) {
            break;
        }
    }
    return array;
}

2 選擇排序

每次循環(huán)都會找出當前循環(huán)中最小的元素,然后和此次循環(huán)中的隊首元素進行交換。

public int[] selectSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    for (int i = 0; i < array.length; i++) {
        int minIndex = i;
        for (int j = i + 1; j < array.length; j++) {
            if (array[j] < array[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex > i) {
            array[i] = array[i] ^ array[minIndex];
            array[minIndex] = array[i] ^ array[minIndex];
            array[i] = array[i] ^ array[minIndex];
        }
    }
    return array;
}

3 插入排序

插入排序的精髓在于每次都會在先前排好序的子集合中插入下一個待排序的元素,每次都會判斷待排序元素的上一個元素是否大于待排序元素,如果大于,則將元素右移,然后判斷再上一個元素與待排序元素...以此類推。直到小于等于比較元素時就是找到了該元素的插入位置。這里的等于條件放在哪里很重要,因為它是決定插入排序穩(wěn)定與否的關鍵。

public int[] insertSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    for (int i = 1; i < array.length; i++) {
        int temp = array[i];
        int j = i - 1;
        while (j >= 0 && array[j] > temp) {
            array[j + 1] = array[j];
            j--;
        }
        array[j + 1] = temp;
    }
    return array;
}

4 希爾排序

希爾排序可以認為是插入排序的改進版本。首先按照初始增量來將數(shù)組分成多個組,每個組內(nèi)部使用插入排序。然后縮小增量來重新分組,組內(nèi)再次使用插入排序...重復以上步驟,直到增量變?yōu)?的時候,這個時候整個數(shù)組就是一個分組,進行最后一次完整的插入排序即可結(jié)束。

在排序開始時的增量較大,分組也會較多,但是每個分組中的數(shù)據(jù)較少,所以插入排序會很快。隨著每一輪排序的進行,增量和分組數(shù)會逐漸變小,每個分組中的數(shù)據(jù)會逐漸變多。但因為之前已經(jīng)經(jīng)過了多輪的分組排序,而此時的數(shù)組會趨近于一個有序的狀態(tài),所以這個時候的排序也是很快的。而對于數(shù)據(jù)較多且趨向于無序的數(shù)據(jù)來說,如果只是使用插入排序的話效率就并不高。所以總體來說,希爾排序的執(zhí)行效率是要比插入排序高的。

希爾排序執(zhí)行示意圖:

img

具體的實現(xiàn)代碼如下:

public int[] shellSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    int gap = array.length >>> 1;
    while (gap > 0) {
        for (int i = gap; i < array.length; i++) {
            int temp = array[i];
            int j = i - gap;
            while (j >= 0 && array[j] > temp) {
                array[j + gap] = array[j];
                j = j - gap;
            }
            array[j + gap] = temp;
        }
        gap >>>= 1;
    }
    return array;
}

5 堆排序

堆排序的過程是首先構(gòu)建一個大頂堆,大頂堆首先是一棵完全二叉樹,其次它保證堆中某個節(jié)點的值總是不大于其父節(jié)點的值。

因為大頂堆中的最大元素肯定是根節(jié)點,所以每次取出根節(jié)點即為當前大頂堆中的最大元素,取出后剩下的節(jié)點再重新構(gòu)建大頂堆,再取出根節(jié)點,再重新構(gòu)建…重復這個過程,直到數(shù)據(jù)都被取出,最后取出的結(jié)果即為排好序的結(jié)果。

public class MaxHeap {

    /**
     * 排序數(shù)組
     */
    private int[] nodeArray;
    /**
     * 數(shù)組的真實大小
     */
    private int size;

    private int parent(int index) {
        return (index - 1) >>> 1;
    }

    private int leftChild(int index) {
        return (index << 1) + 1;
    }

    private int rightChild(int index) {
        return (index << 1) + 2;
    }

    private void swap(int i, int j) {
        nodeArray[i] = nodeArray[i] ^ nodeArray[j];
        nodeArray[j] = nodeArray[i] ^ nodeArray[j];
        nodeArray[i] = nodeArray[i] ^ nodeArray[j];
    }

    private void siftUp(int index) {
        //如果index處節(jié)點的值大于其父節(jié)點的值,則交換兩個節(jié)點值,同時將index指向其父節(jié)點,繼續(xù)向上循環(huán)判斷
        while (index > 0 && nodeArray[index] > nodeArray[parent(index)]) {
            swap(index, parent(index));
            index = parent(index);
        }
    }

    private void siftDown(int index) {
        //左孩子的索引比size小,意味著索引index處的節(jié)點有左孩子,證明此時index節(jié)點不是葉子節(jié)點
        while (leftChild(index) < size) {
            //maxIndex記錄的是index節(jié)點左右孩子中最大值的索引
            int maxIndex = leftChild(index);
            //右孩子的索引小于size意味著index節(jié)點含有右孩子
            if (rightChild(index) < size && nodeArray[rightChild(index)] > nodeArray[maxIndex]) {
                maxIndex = rightChild(index);
            }
            //如果index節(jié)點值比左右孩子值都大,則終止循環(huán)
            if (nodeArray[index] >= nodeArray[maxIndex]) {
                break;
            }
            //否則進行交換,將index指向其交換的左孩子或右孩子,繼續(xù)向下循環(huán),直到葉子節(jié)點
            swap(index, maxIndex);
            index = maxIndex;
        }
    }

    private void add(int value) {
        nodeArray[size] = value;
        size++;
        //構(gòu)建大頂堆
        siftUp(size - 1);
    }

    private void extractMax() {
        //將堆頂元素和最后一個元素進行交換
        swap(0, size - 1);
        //此時并沒有刪除元素,而只是將size-1,剩下的元素重新構(gòu)建成大頂堆
        size--;
        //重新構(gòu)建大頂堆
        siftDown(0);
    }

    public int[] heapSort(int[] array) {
        if (array == null || array.length < 2) {
            return array;
        }
        nodeArray = new int[array.length];
        for (int value : array) {
            add(value);
        }
        for (int i = 0; i < array.length; i++) {
            extractMax();
        }
        return nodeArray;
    }
}

上面的經(jīng)典實現(xiàn)中,如果需要變動節(jié)點時,都會來一次父子節(jié)點的互相交換操作(包括刪除節(jié)點時首先做的要刪除節(jié)點和最后一個節(jié)點之間的交換操作也是如此)。如果仔細思考的話,就會發(fā)現(xiàn)這其實是多余的。在需要交換節(jié)點的時候,只需要siftUp操作時的父節(jié)點或siftDown時的孩子節(jié)點重新移到當前需要比較的節(jié)點位置上,而比較節(jié)點是不需要移動到它們的位置上的。此時直接進入到下一次的判斷中,重復siftUp或siftDown過程,直到最后找到了比較節(jié)點的插入位置后,才會將其插入進去。這樣做的好處是可以省去一半的節(jié)點賦值的操作,提高了執(zhí)行的效率。同時這也就意味著,需要將要比較的節(jié)點作為參數(shù)保存起來,而在ScheduledThreadPoolExecutor源碼中也正是這么實現(xiàn)的(《較真兒學源碼系列-ScheduledThreadPoolExecutor(逐行源碼帶你分析作者思路)》)。


6 歸并排序

歸并排序使用的是分治的思想,首先將數(shù)組不斷拆分,直到最后拆分成兩個元素的子數(shù)組,將這兩個元素進行排序合并,再向上遞歸。不斷重復這個拆分和合并的遞歸過程,最后得到的就是排好序的結(jié)果。

合并的過程是將兩個指針指向兩個子數(shù)組的首位元素,兩個元素進行比較,較小的插入到一個temp數(shù)組中,同時將該數(shù)組的指針右移一位,繼續(xù)比較該數(shù)組的第二個元素和另一個元素…重復這個過程。這樣temp數(shù)組保存的便是這兩個子數(shù)組排好序的結(jié)果。最后將temp數(shù)組復制回原數(shù)組的位置處即可。

public int[] mergeSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    return mergeSort(array, 0, array.length - 1);
}

private int[] mergeSort(int[] array, int left, int right) {
    if (left < right) {
        //這里沒有選擇“(left + right) / 2”的方式,是為了防止數(shù)據(jù)溢出
        int mid = left + ((right - left) >>> 1);
        // 拆分子數(shù)組
        mergeSort(array, left, mid);
        mergeSort(array, mid + 1, right);
        // 對子數(shù)組進行合并
        merge(array, left, mid, right);
    }
    return array;
}

private void merge(int[] array, int left, int mid, int right) {
    int[] temp = new int[right - left + 1];
    // p1和p2為需要對比的兩個數(shù)組的指針,k為存放temp數(shù)組的指針
    int p1 = left, p2 = mid + 1, k = 0;
    while (p1 <= mid && p2 <= right) {
        if (array[p1] <= array[p2]) {
            temp[k++] = array[p1++];
        } else {
            temp[k++] = array[p2++];
        }
    }
    // 把剩余的數(shù)組直接放到temp數(shù)組中
    while (p1 <= mid) {
        temp[k++] = array[p1++];
    }
    while (p2 <= right) {
        temp[k++] = array[p2++];
    }
    // 復制回原數(shù)組
    for (int i = 0; i < temp.length; i++) {
        array[i + left] = temp[i];
    }
}

7 快速排序

快速排序的核心是要有一個基準數(shù)據(jù)temp,一般取數(shù)組的第一個位置元素。然后需要有兩個指針left和right,分別指向數(shù)組的第一個和最后一個元素。

首先從right開始,比較right位置元素和基準數(shù)據(jù)。如果大于等于,則將right指針左移,比較下一位元素;如果小于,就將right指針處數(shù)據(jù)賦給left指針處(此時left指針處數(shù)據(jù)已保存進temp中),left指針+1,之后開始比較left指針處數(shù)據(jù)。

拿left位置元素和基準數(shù)據(jù)進行比較。如果小于等于,則將left指針右移,比較下一位元素;而如果大于就將left指針處數(shù)據(jù)賦給right指針處,right指針-1,之后開始比較right指針處數(shù)據(jù)…重復這個過程。

直到left和right指針相等時,說明這一次比較過程完成。此時將先前存放進temp中的基準數(shù)據(jù)賦值給當前l(fā)eft和right指針共同指向的位置處,即可完成這一次排序操作。

之后遞歸排序基礎數(shù)據(jù)的左半部分和右半部分,遞歸的過程和上面講述的過程是一樣的,只不過數(shù)組范圍不再是原來的全部數(shù)組了,而是現(xiàn)在的左半部分或右半部分。當全部的遞歸過程結(jié)束后,最終結(jié)果即為排好序的結(jié)果。

快速排序執(zhí)行示意圖:

img

正如上面所說的,一般取第一個元素作為基準數(shù)據(jù),但如果當前數(shù)據(jù)為從大到小排列好的數(shù)據(jù),而現(xiàn)在要按從小到大的順序排列,則數(shù)據(jù)分攤不均勻,時間復雜度會退化為
O(n^2)

,而不是正常情況下的
O(nlog_2n)
。此時采取一個優(yōu)化手段,即取最左邊、最右邊和最中間的三個元素的中間值作為基準數(shù)據(jù),以此來避免時間復雜度為
O(n^2)
的情況出現(xiàn),當然也可以選擇更多的錨點或者隨機選擇的方式來進行選取。

還有一個優(yōu)化的方法是:像快速排序、歸并排序這樣的復雜排序方法在數(shù)據(jù)量大的情況下是比選擇排序、冒泡排序和插入排序的效率要高的,但是在數(shù)據(jù)量小的情況下反而要更慢。所以我們可以選定一個閾值,這里選擇為47(和源碼中使用的一樣)。當需要排序的數(shù)據(jù)量小于47時走插入排序,大于47則走快速排序。

private static final int THRESHOLD = 47;

public int[] quickSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    return quickSort(array, 0, array.length - 1);
}

private int[] quickSort(int[] array, int start, int end) {
    // 如果當前需要排序的數(shù)據(jù)量小于等于THRESHOLD則走插入排序的邏輯,否則繼續(xù)走快速排序
    if (end - start <= THRESHOLD - 1) {
        return insertSort(array);
    }

    // left和right指針分別指向array的第一個和最后一個元素
    int left = start, right = end;

    /*
    取最左邊、最右邊和最中間的三個元素的中間值作為基準數(shù)據(jù),以此來盡量避免每次都取第一個值作為基準數(shù)據(jù)、
    時間復雜度可能退化為O(n^2)的情況出現(xiàn)
     */
    int middleOf3Indexs = middleOf3Indexs(array, start, end);
    if (middleOf3Indexs != start) {
        swap(array, middleOf3Indexs, start);
    }

    // temp存放的是array中需要比較的基準數(shù)據(jù)
    int temp = array[start];

    while (left < right) {
        // 首先從right指針開始比較,如果right指針位置處數(shù)據(jù)大于temp,則將right指針左移
        while (left < right && array[right] >= temp) {
            right--;
        }
        // 如果找到一個right指針位置處數(shù)據(jù)小于temp,則將right指針處數(shù)據(jù)賦給left指針處
        if (left < right) {
            array[left] = array[right];
            left++;
        }
        // 然后從left指針開始比較,如果left指針位置處數(shù)據(jù)小于temp,則將left指針右移
        while (left < right && array[left] <= temp) {
            left++;
        }
        // 如果找到一個left指針位置處數(shù)據(jù)大于temp,則將left指針處數(shù)據(jù)賦給right指針處
        if (left < right) {
            array[right] = array[left];
            right--;
        }
    }
    // 當left和right指針相等時,此時循環(huán)跳出,將之前存放的基準數(shù)據(jù)賦給當前兩個指針共同指向的數(shù)據(jù)處
    array[left] = temp;
    // 一次替換后,遞歸交換基準數(shù)據(jù)左邊的數(shù)據(jù)
    if (start < left - 1) {
        array = quickSort(array, start, left - 1);
    }
    // 之后遞歸交換基準數(shù)據(jù)右邊的數(shù)據(jù)
    if (right + 1 < end) {
        array = quickSort(array, right + 1, end);
    }
    return array;
}

private int middleOf3Indexs(int[] array, int start, int end) {
    int mid = start + ((end - start) >>> 1);
    if (array[start] < array[mid]) {
        if (array[mid] < array[end]) {
            return mid;
        } else {
            return array[start] < array[end] ? end : start;
        }
    } else {
        if (array[mid] > array[end]) {
            return mid;
        } else {
            return array[start] < array[end] ? start : end;
        }
    }
}

private void swap(int[] array, int i, int j) {
    array[i] = array[i] ^ array[j];
    array[j] = array[i] ^ array[j];
    array[i] = array[i] ^ array[j];
}

8 計數(shù)排序

以上的七種排序算法都是比較排序,也就是基于元素之間的比較來進行排序的。而下面將要介紹的三種排序算法是非比較排序,首先是計數(shù)排序。

計數(shù)排序會創(chuàng)建一個臨時的數(shù)組,里面存放每個數(shù)出現(xiàn)的次數(shù)。比如一個待排序的數(shù)組是[3, 3, 5, 2, 7, 4, 2],那么這個臨時數(shù)組中記錄的數(shù)據(jù)就是[2, 2, 1, 1, 0, 1]。表示2出現(xiàn)了兩次、3出現(xiàn)了兩次、4出現(xiàn)了一次、5出現(xiàn)了一次、6出現(xiàn)了零次、7出現(xiàn)了一次。那么最后只需要遍歷這個臨時數(shù)組中的計數(shù)值就可以了。

public int[] countingSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    //記錄待排序數(shù)組中的最大值
    int max = array[0];
    //記錄待排序數(shù)組中的最小值
    int min = array[0];
    for (int i : array) {
        if (i > max) {
            max = i;
        }
        if (i < min) {
            min = i;
        }
    }
    int[] temp = new int[max - min + 1];
    //記錄每個數(shù)出現(xiàn)的次數(shù)
    for (int i : array) {
        temp[i - min]++;
    }
    int index = 0;
    for (int i = 0; i < temp.length; i++) {
        //當輸出一個數(shù)之后,當前位置的計數(shù)就減一,直到減到0為止
        while (temp[i]-- > 0) {
            array[index++] = i + min;
        }
    }
    return array;
}

從上面的實現(xiàn)中可以看到,計數(shù)排序僅適合數(shù)據(jù)跨度不大的場景。如果最大值和最小值之間的差距比較大,生成的臨時數(shù)組就會比較長。比如說一個數(shù)組是[2, 1, 3, 1000],最小值是1,最大值是1000。那么就會生成一個長度為1000的臨時數(shù)組,但是其中絕大部分的空間都是沒有用的,所以這就會導致空間復雜度變得很高。

計數(shù)排序是穩(wěn)定的排序算法,但在上面的實現(xiàn)中并沒有體現(xiàn)出這一點,上面的實現(xiàn)沒有維護相同元素之間的先后順序。所以需要做些變換:將臨時數(shù)組中從第二個元素開始,每個元素都加上前一個元素的值。還是拿之前的[3, 3, 5, 2, 7, 4, 2]數(shù)組來舉例。計完數(shù)后的臨時數(shù)組為[2, 2, 1, 1, 0, 1],此時做上面的變換,每個數(shù)都累加前面的一個數(shù),結(jié)果為[2, 4, 5, 6, 6, 7]。這個時候臨時數(shù)組的含義就不再是每個數(shù)出現(xiàn)的次數(shù)了,此時記錄的是每個數(shù)在最后排好序的數(shù)組中應該要存放的位置+1(如果有重復的就記錄最后一個)。對于上面的待排序數(shù)組來說,最后排好序的數(shù)組應該為[2, 2, 3, 3, 4, 5, 7]。也就是說,此時各個數(shù)最后一次出現(xiàn)的索引位為:1, 3, 4, 5, 6,分別都+1后就是2, 4, 5, 6, 7,這不就是上面做過變換之后的數(shù)組嗎?(沒有出現(xiàn)過的數(shù)字不管它)所以,此時從后往前遍歷原數(shù)組中的每一個值,將其減去最小值后,找到其在變換后的臨時數(shù)組中的索引,也就是找到了最后排好序的數(shù)組中的位置了。當然,每次找到臨時數(shù)組中的索引后,這個位置的數(shù)需要-1。這樣如果后續(xù)有重復的該數(shù)字的話,就會插入到當前位置的前一個位置了。由此也說明了遍歷必須是從后往前遍歷,以此來維護相同數(shù)字之間的先后順序。

public int[] stableCountingSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    //記錄待排序數(shù)組中的最大值
    int max = array[0];
    //記錄待排序數(shù)組中的最小值
    int min = array[0];
    for (int i : array) {
        if (i > max) {
            max = i;
        }
        if (i < min) {
            min = i;
        }
    }
    int[] temp = new int[max - min + 1];
    //記錄每個數(shù)出現(xiàn)的次數(shù)
    for (int i : array) {
        temp[i - min]++;
    }
    //將temp數(shù)組進行轉(zhuǎn)換,記錄每個數(shù)在最后排好序的數(shù)組中應該要存放的位置+1(如果有重復的就記錄最后一個)
    for (int j = 1; j < temp.length; j++) {
        temp[j] += temp[j - 1];
    }
    int[] sortedArray = new int[array.length];
    //這里必須是從后往前遍歷,以此來保證穩(wěn)定性
    for (int i = array.length - 1; i >= 0; i--) {
        sortedArray[temp[array[i] - min] - 1] = array[i];
        temp[array[i] - min]--;
    }
    return sortedArray;
}

9 桶排序

上面的計數(shù)排序在數(shù)組最大值和最小值之間的差值是多少,就會生成一個多大的臨時數(shù)組,也就是生成了一個這么多的桶,而每個桶中就只插入一個數(shù)據(jù)。如果差值比較大的話,會比較浪費空間。那么我能不能在一個桶中插入多個數(shù)據(jù)呢?當然可以,而這就是桶排序的思路。桶排序類似于哈希表,通過一定的映射規(guī)則將數(shù)組中的元素映射到不同的桶中,每個桶內(nèi)進行內(nèi)部排序,最后將每個桶按順序輸出就行了。桶排序執(zhí)行的高效與否和是否是穩(wěn)定的取決于哈希散列的算法以及內(nèi)部排序的結(jié)果。需要注意的是,這個映射算法并不是常規(guī)的映射算法,要求是每個桶中的所有數(shù)都要比前一個桶中的所有數(shù)都要大。這樣最后輸出的才是一個排好序的結(jié)果。比如說第一個桶中存1-30的數(shù)字,第二個桶中存31-60的數(shù)字,第三個桶中存61-90的數(shù)字...以此類推。下面給出一種實現(xiàn):

public int[] bucketSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    //記錄待排序數(shù)組中的最大值
    int max = array[0];
    //記錄待排序數(shù)組中的最小值
    int min = array[0];
    for (int i : array) {
        if (i > max) {
            max = i;
        }
        if (i < min) {
            min = i;
        }
    }
    //計算桶的數(shù)量(可以自定義實現(xiàn))
    int bucketNumber = (max - min) / array.length + 1;
    List<Integer>[] buckets = new ArrayList[bucketNumber];
    //計算每個桶存數(shù)的范圍(可以自定義實現(xiàn)或者不用實現(xiàn))
    int bucketRange = (max - min + 1) / bucketNumber;

    for (int value : array) {
        //計算應該放到哪個桶中(可以自定義實現(xiàn))
        int bucketIndex = (value - min) / (bucketRange + 1);
        //延遲初始化
        if (buckets[bucketIndex] == null) {
            buckets[bucketIndex] = new ArrayList<>();
        }
        //放入指定的桶
        buckets[bucketIndex].add(value);
    }
    int index = 0;
    for (List<Integer> bucket : buckets) {
        //對每個桶進行內(nèi)部排序,我這里使用的是快速排序,也可以使用別的排序算法,當然也可以繼續(xù)遞歸去做桶排序
        quickSort(bucket);
        if (bucket == null) {
            continue;
        }
        //將不為null的桶中的數(shù)據(jù)按順序?qū)懟氐絘rray數(shù)組中
        for (Integer integer : bucket) {
            array[index++] = integer;
        }
    }
    return array;
}

10 基數(shù)排序

基數(shù)排序不是根據(jù)一個數(shù)的整體來進行排序的,而是將數(shù)的每一位上的數(shù)字進行排序。比如說第一輪排序,我拿到待排序數(shù)組中所有數(shù)個位上的數(shù)字來進行排序;第二輪排序我拿到待排序數(shù)組中所有數(shù)十位上的數(shù)字來進行排序;第三輪排序我拿到待排序數(shù)組中所有數(shù)百位上的數(shù)字來進行排序...以此類推。每一輪的排序都會累加上一輪所有前幾位上排序的結(jié)果,最終的結(jié)果就會是一個有序的數(shù)列。

基數(shù)排序一般是對所有非負整數(shù)進行排序的,但是也可以有別的手段來去掉這種限制(比如都加一個固定的數(shù)或者都乘一個固定的數(shù),排完序后再恢復等等)。基數(shù)排序和桶排序很像,桶排序是按數(shù)值的區(qū)間進行劃分,而基數(shù)排序是按數(shù)的位數(shù)進行劃分。同時這兩個排序都是需要依靠其他排序算法來實現(xiàn)的(如果不算遞歸調(diào)用桶排序本身的話)。基數(shù)排序每一輪的內(nèi)部排序會使用到計數(shù)排序來實現(xiàn),因為每一位上的數(shù)字無非就是0-9,是一個小范圍的數(shù),所以使用計數(shù)排序很合適。

基數(shù)排序執(zhí)行示意圖:

img

具體的實現(xiàn)代碼如下:

public int[] radixSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    //記錄待排序數(shù)組中的最大值
    int max = array[0];
    for (int i : array) {
        if (i > max) {
            max = i;
        }
    }
    //獲取最大值的位數(shù)
    int maxDigits = 0;
    while (max != 0) {
        max /= 10;
        maxDigits++;
    }
    //用來計數(shù)排序的臨時數(shù)組
    int[] temp = new int[10];
    //用來存放每輪排序后的結(jié)果
    int[] sortedArray = new int[array.length];
    for (int d = 1; d <= maxDigits; d++) {
        //每次循環(huán)開始前都要清空temp數(shù)組中的值
        replaceArray(temp, null);
        //記錄每個數(shù)出現(xiàn)的次數(shù)
        for (int a : array) {
            temp[getNumberFromDigit(a, d)]++;
        }
        //將temp數(shù)組進行轉(zhuǎn)換,記錄每個數(shù)在最后排好序的數(shù)組中應該要存放的位置+1(如果有重復的就記錄最后一個)
        for (int j = 1; j < temp.length; j++) {
            temp[j] += temp[j - 1];
        }
        //這里必須是從后往前遍歷,以此來保證穩(wěn)定性
        for (int i = array.length - 1; i >= 0; i--) {
            int index = getNumberFromDigit(array[i], d);
            sortedArray[temp[index] - 1] = array[i];
            temp[index]--;
        }
        //一輪計數(shù)排序過后,將這次排好序的結(jié)果賦值給原數(shù)組
        replaceArray(array, sortedArray);
    }
    return array;
}

private final static int[] sizeTable = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000};

/**
 * 獲取指定位數(shù)上的數(shù)字是多少
 */
private int getNumberFromDigit(int number, int digit) {
    if (digit < 0) {
        return -1;
    }
    return (number / sizeTable[digit - 1]) % 10;
}

private void replaceArray(int[] originalArray, int[] replaceArray) {
    if (replaceArray == null) {
        for (int i = 0; i < originalArray.length; i++) {
            originalArray[i] = 0;
        }
    } else {
        for (int i = 0; i < originalArray.length; i++) {
            originalArray[i] = replaceArray[i];
        }
    }
}

11 復雜度及穩(wěn)定性

排序算法 時間復雜度 空間復雜度 穩(wěn)定性
平均情況 最好情況 最壞情況
冒泡排序
O(n^2)
O(n)
O(n^2)
O(1)
穩(wěn)定
選擇排序
O(n^2)
O(n^2)
O(n^2)
O(1)
不穩(wěn)定
插入排序
O(n^2)
O(n)
O(n^2)
O(1)
穩(wěn)定
希爾排序 取決于增量的選擇
O(1)
不穩(wěn)定
堆排序
O(nlog_2n)
O(nlog_2n)
O(nlog_2n)
O(1)
不穩(wěn)定
歸并排序
O(nlog_2n)
O(nlog_2n)
O(nlog_2n)
O(n)
穩(wěn)定
快速排序
O(nlog_2n)
O(nlog_2n)
O(n^2)
O(log_2n)
不穩(wěn)定
計數(shù)排序
O(n+k)
O(n+k)
O(n+k)
O(k)
穩(wěn)定
桶排序 取決于桶散列的結(jié)果和內(nèi)部排序算法的時間復雜度
O(n+l)
穩(wěn)定
基數(shù)排序
O(d*(n+r))
O(d*(n+r))
O(d*(n+r))
O(n+r)
穩(wěn)定

(其中:

  1. k表示計數(shù)排序中最大值和最小值之間的差值;
  2. l表示桶排序中桶的個數(shù);
  3. d表示基數(shù)排序中最大值的位數(shù),r表示是多少進制;
  4. 希爾排序的時間復雜度很大程度上取決于增量gap sequence的選擇,不同的增量會有不同的時間復雜度。文中使用的“gap=length/2”和“gap=gap/2”是一種常用的方式,也被稱為希爾增量,但其并不是最優(yōu)的。其實希爾排序增量的選擇與證明一直都是個數(shù)學難題,而下圖列出的是迄今為止大部分的gap sequence選擇的方案)
img

12 小彩蛋:猴子排序

在幾十年的計算機科學發(fā)展中,誕生了很多優(yōu)秀的算法,大家都在為了能開發(fā)出更高效的算法而努力。但是在這其中也誕生了一些僅供娛樂的搞笑算法,猴子排序就是其中的一種。

猴子排序的實現(xiàn)很簡單,隨機找出兩個元素進行交換,直到隨機交換到最后能正確排好序的時候才會停止。

public int[] bogoSort(int[] array) {
    if (array == null || array.length < 2) {
        return array;
    }
    Random random = new Random();
    while (!inOrder(array)) {
        for (int i = 0; i < array.length; i++) {
            int swapPosition = random.nextInt(i + 1);
            if (swapPosition == i) {
                continue;
            }
            array[i] = array[i] ^ array[swapPosition];
            array[swapPosition] = array[i] ^ array[swapPosition];
            array[i] = array[i] ^ array[swapPosition];
        }
    }
    return array;
}

private boolean inOrder(int[] array) {
    for (int i = 0; i < array.length - 1; i++) {
        if (array[i] > array[i + 1]) {
            return false;
        }
    }
    return true;
}

原創(chuàng)不易,未得準許,請勿轉(zhuǎn)載,翻版必究

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。