主頁 > 後端開發 > 1T資料快速排序!十種經典排序演算法總結

1T資料快速排序!十種經典排序演算法總結

2020-10-15 20:33:25 後端開發

1 冒泡排序

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

一個優化冒泡排序的方法就是如果在一次回圈的程序中沒有發生交換,則可以立即退出當前回圈,因為此時已經排好序了(也就是時間復雜度最好情況下是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]) {
                //這里交換兩個資料并沒有使用中間變數,而是使用異或的方式來實作
                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 選擇排序

每次回圈都會找出當前回圈中最小的元素,然后和此次回圈中的隊首元素進行交換,

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 插入排序

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

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 希爾排序

希爾排序可以認為是插入排序的改進版本,首先按照初始增量來將陣列分成多個組,每個組內部使用插入排序,然后縮小增量來重新分組,組內再次使用插入排序...重復以上步驟,直到增量變為1的時候,這個時候整個陣列就是一個分組,進行最后一次完整的插入排序即可結束,

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

希爾排序執行示意圖:

img

具體的實作代碼如下:

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 堆排序

堆排序的程序是首先構建一個大頂堆,大頂堆首先是一棵完全二叉樹,其次它保證堆中某個節點的值總是不大于其父節點的值,

因為大頂堆中的最大元素肯定是根節點,所以每次取出根節點即為當前大頂堆中的最大元素,取出后剩下的節點再重新構建大頂堆,再取出根節點,再重新構建…重復這個程序,直到資料都被取出,最后取出的結果即為排好序的結果,

public class MaxHeap {

    /**
     * 排序陣列
     */
    private int[] nodeArray;
    /**
     * 陣列的真實大小
     */
    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處節點的值大于其父節點的值,則交換兩個節點值,同時將index指向其父節點,繼續向上回圈判斷
        while (index > 0 && nodeArray[index] > nodeArray[parent(index)]) {
            swap(index, parent(index));
            index = parent(index);
        }
    }

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

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

    private void extractMax() {
        //將堆頂元素和最后一個元素進行交換
        swap(0, size - 1);
        //此時并沒有洗掉元素,而只是將size-1,剩下的元素重新構建成大頂堆
        size--;
        //重新構建大頂堆
        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;
    }
}

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


6 歸并排序

歸并排序使用的是分治的思想,首先將陣列不斷拆分,直到最后拆分成兩個元素的子陣列,將這兩個元素進行排序合并,再向上遞回,不斷重復這個拆分和合并的遞回程序,最后得到的就是排好序的結果,

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

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”的方式,是為了防止資料溢位
        int mid = left + ((right - left) >>> 1);
        // 拆分子陣列
        mergeSort(array, left, mid);
        mergeSort(array, mid + 1, right);
        // 對子陣列進行合并
        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為需要對比的兩個陣列的指標,k為存放temp陣列的指標
    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++];
        }
    }
    // 把剩余的陣列直接放到temp陣列中
    while (p1 <= mid) {
        temp[k++] = array[p1++];
    }
    while (p2 <= right) {
        temp[k++] = array[p2++];
    }
    // 復制回原陣列
    for (int i = 0; i < temp.length; i++) {
        array[i + left] = temp[i];
    }
}

7 快速排序

快速排序的核心是要有一個基準資料temp,一般取陣列的第一個位置元素,然后需要有兩個指標left和right,分別指向陣列的第一個和最后一個元素,

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

拿left位置元素和基準資料進行比較,如果小于等于,則將left指標右移,比較下一位元素;而如果大于就將left指標處資料賦給right指標處,right指標-1,之后開始比較right指標處資料…重復這個程序,

直到left和right指標相等時,說明這一次比較程序完成,此時將先前存放進temp中的基準資料賦值給當前left和right指標共同指向的位置處,即可完成這一次排序操作,

之后遞回排序基礎資料的左半部分和右半部分,遞回的程序和上面講述的程序是一樣的,只不過陣列范圍不再是原來的全部陣列了,而是現在的左半部分或右半部分,當全部的遞回程序結束后,最終結果即為排好序的結果,

快速排序執行示意圖:

img

正如上面所說的,一般取第一個元素作為基準資料,但如果當前資料為從大到小排列好的資料,而現在要按從小到大的順序排列,則資料分攤不均勻,時間復雜度會退化為O(n^2),而不是正常情況下的O(nlog_2n),此時采取一個優化手段,即取最左邊、最右邊和最中間的三個元素的中間值作為基準資料,以此來避免時間復雜度為O(n^2)的情況出現,當然也可以選擇更多的錨點或者隨機選擇的方式來進行選取,

還有一個優化的方法是:像快速排序、歸并排序這樣的復雜排序方法在資料量大的情況下是比選擇排序、冒泡排序和插入排序的效率要高的,但是在資料量小的情況下反而要更慢,所以我們可以選定一個閾值,這里選擇為47(和原始碼中使用的一樣),當需要排序的資料量小于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) {
    // 如果當前需要排序的資料量小于等于THRESHOLD則走插入排序的邏輯,否則繼續走快速排序
    if (end - start <= THRESHOLD - 1) {
        return insertSort(array);
    }

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

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

    // temp存放的是array中需要比較的基準資料
    int temp = array[start];

    while (left < right) {
        // 首先從right指標開始比較,如果right指標位置處資料大于temp,則將right指標左移
        while (left < right && array[right] >= temp) {
            right--;
        }
        // 如果找到一個right指標位置處資料小于temp,則將right指標處資料賦給left指標處
        if (left < right) {
            array[left] = array[right];
            left++;
        }
        // 然后從left指標開始比較,如果left指標位置處資料小于temp,則將left指標右移
        while (left < right && array[left] <= temp) {
            left++;
        }
        // 如果找到一個left指標位置處資料大于temp,則將left指標處資料賦給right指標處
        if (left < right) {
            array[right] = array[left];
            right--;
        }
    }
    // 當left和right指標相等時,此時回圈跳出,將之前存放的基準資料賦給當前兩個指標共同指向的資料處
    array[left] = temp;
    // 一次替換后,遞回交換基準資料左邊的資料
    if (start < left - 1) {
        array = quickSort(array, start, left - 1);
    }
    // 之后遞回交換基準資料右邊的資料
    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 計數排序

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

計數排序會創建一個臨時的陣列,里面存放每個數出現的次數,比如一個待排序的陣列是[3, 3, 5, 2, 7, 4, 2],那么這個臨時陣列中記錄的資料就是[2, 2, 1, 1, 0, 1],表示2出現了兩次、3出現了兩次、4出現了一次、5出現了一次、6出現了零次、7出現了一次,那么最后只需要遍歷這個臨時陣列中的計數值就可以了,

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

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

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

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

9 桶排序

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

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

    for (int value : array) {
        //計算應該放到哪個桶中(可以自定義實作)
        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) {
        //對每個桶進行內部排序,我這里使用的是快速排序,也可以使用別的排序演算法,當然也可以繼續遞回去做桶排序
        quickSort(bucket);
        if (bucket == null) {
            continue;
        }
        //將不為null的桶中的資料按順序寫回到array陣列中
        for (Integer integer : bucket) {
            array[index++] = integer;
        }
    }
    return array;
}

10 基數排序

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

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

基數排序執行示意圖:

img

具體的實作代碼如下:

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

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

/**
 * 獲取指定位數上的數字是多少
 */
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 復雜度及穩定性

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

(其中:

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

img


12 小彩蛋:猴子排序

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

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

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;
}

更多內容請關注微信公眾號:奇客時間

奇客時間

原創不易,未得準許,請勿轉載,翻版必究

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/173109.html

標籤:Java

上一篇:Java復習,Java知識點以及Java面試題(六)

下一篇:springboot專案在linux服務器部署啟動腳本shell撰寫

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more