解讀排序演算法

crazysunj發表於2017-11-12

演算法

演算法(Algorithm)是指解題方案的準確而完整的描述,是一系列解決問題的清晰指令,演算法代表著用系統的方法描述解決問題的策略機制。

簡單點說,演算法就是解決問題的方法。確切來說它是相對於計算機程式的,大多數情況並不與具體某一種程式語言有關,但今天我們採用java語言實現演算法示例。原諒我是一隻Android小菜鳥,就算不原諒,你又能拿我怎麼滴?哈哈,開個玩笑,回到正題,方法有千千萬萬種,相信大家王者榮耀上分也是想了好多辦法,嘗試不同的位置,不同英雄,每個英雄不同的打法。最後發現,毫無卵用(手動滑稽)。那是你沒找到好的方法,比如你可以找代練啊,而我們的主題演算法的優劣主要取決於時間複雜度和空間複雜度。那麼有人問了,什麼是時間複雜度,什麼又是空間複雜度呢?

時間複雜度

時間複雜度是指執行演算法所需要的計算工作量。我們見得更多的是這樣的寫法,O(1)、O(n)。到底什麼意思呢?我相信更多的小夥伴想要的是這樣的解讀方式。

O(1)

int a=1;
int b=2;
int c=a+b;複製程式碼

類似於這種,執行語句的頻度均為1,即使有上千萬條,時間複雜度還是O(1),只不過執行時間是一個很大的常數。

O(n)

for(int i=0;i<n;i++){
    int a=1;
    int b=2;
    int c=a+b;
}複製程式碼

在一個規模為n的迴圈中,不管是n次,還是n-1次,對於時間複雜度都是線性的,取n,記為O(n)。

O(log2n)

int i = 1;
while (i <= n){
    i = i*2;
}複製程式碼

這個可能理解上有點困難,但也很簡單,每次執行i都會乘以2,其實就是2的x次冪小於等於n,求得x為log2n,所以時間複雜度O(log2n)。

O(n^2)

for (i = 0; i < n; ++i){
    for (j = 0; j < n; j++){
        printf ("%d\n", j);
    }     
}複製程式碼

類似於這種在規模為n的迴圈中又巢狀了一層規模為n的迴圈,那麼時間複雜度就為O(n^2),同理n的多次冪就是多層巢狀。

空間複雜度

空間複雜度是對一個演算法在執行過程中臨時佔用儲存空間大小的量度。一般來說我們不考慮空間複雜度,大多情況下為O(1),像遞迴可能會達到O(n)。

排序演算法穩定性

穩定性就是一組元素,其中有重複的元素,比如2113,我們把前面的1記為1a,後面的1記為1b,那麼原始資料為21a1b3,如果排序後重復元素的相對位置不變,那麼,這個排序是穩定的。如上例子,排序完應該是1a1b23才是穩定的,而不是1b1a23。

排序演算法

排序演算法是演算法的入門知識,但思想可以用於很多演算法中。什麼是排序?排序就是將一組物件按照某種邏輯順序重新排列的過程。其實在我們的api中提供了很多優秀的排序演算法,那我們為什麼還要去學習它?原因很簡單,它屬於入門,有助於你理解其它更高大上的演算法,同時它也是我們解決其他問題的第一步。懂了它,你又向大佬靠近了一步。最重要的是面試官看你排序演算法這麼6,心裡想這個肯定是個大佬,一定要留住他,到時候就是你裝逼的時候了。

我都懶得說排序演算法有幾種了,因為我根本不知道,一種排序演算法可能對應多種變體,這次我給大家介紹8種常見的經典排序演算法。

直接插入排序

思想

直接插入排序很好理解,就是從一組元素中取一個元素(肯定是有序的,就一個嘛,稱有序元素組),然後在剩下的元素中每次取一個元素使勁地往有序的元素組插,插到你滿意為止。

是不是很好理解?如果覺得還是有點抽象,沒有關係,每個演算法,我都會分為3個步驟講解,思想-拆解分析-java程式碼實現-執行結果。

為了方便起見,排序的原始資料為5201314,很正規,有重複元素,沒毛病。這裡給大家一個小意見,像碰到/2或者說*2這種,用位運算更佳哦,但本文為了好理解採用了前者,哈哈。

拆解分析

我們在實現直接插入排序的時候往往取第一個元素成立有序元素組,然後它後面的元素一個一個瘋狂插。

  1. 5:取第一個,5
  2. 2-5:取5後面的2,插入5使之成為有序陣列
  3. 0-2-5:取2後面的0,插入2-5,0比5小,往前走,0又比2小,往前走,沒辦法,最小了,結束,以下以此類推
  4. 0-1-2-5:取0後面的1,插入0-2-5
  5. 0-1-2-3-5:取1後面的3,插入0-1-2-5
  6. 0-1-1-2-3-5:取3後面的1,插入0-1-2-3-5
  7. 0-1-1-2-3-4-5:取1後面的4,插入0-1-1-2-3-5

很有層次感是不是?在插的時候也有小技巧的,要溫柔,要循循漸進。因為每當我們插入一個元素,都是有序的,有序的說明什麼?越後面肯定越大,所以我們只要從後面開始比較就行了(你非要從前面插,我也沒辦法),我們從後一直往前比較,直到碰到小於或等於插入的元素為止,然後我們乖乖的插到它後面就行了。

java程式碼實現

public static void insertSort(int[] array) {
    //從第2個開始往前插
    for (int i = 1, n = array.length; i < n; i++) {
        int temp = array[i];//儲存第i個值
        int j = i - 1;//從有序陣列的最後一個開始
        for (; j >= 0 && array[j] > temp; j--) {
            array[j + 1] = array[j];//從後往前比較,大於temp的值都得後移
        }
        array[j + 1] = temp;//碰到小於或等於的數停止,由於多減了1,所以加上1後,賦值為插入值temp
    }
    System.out.println("直接插入排序後:" + Arrays.toString(array));
}複製程式碼

從程式碼的實現來分析,運用我們剛剛學的知識,通常情況下最外層是一個n規模的迴圈,內部又有一個規模為n的迴圈,因此平均時間複雜度為O(n^2);最壞的情況是內部的迴圈全部走一遍,比如我們插入了一個最小的值,因此最差時間複雜度為O(n^2);最好的情況就是裡面的迴圈不用走,比如我們插入了一個最大的值,因此最好時間複雜度為O(n)。其中空間複雜度為O(1)。由於我們是碰到小於或等於的數才停止,所以並不影響重複元素的相對位置,因此直接插入排序是穩定的。

執行結果

希爾排序

希爾排序也是插入排序的一種,是直接插入排序演算法的一種更高效的改進版本。

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

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

說了這麼多,其實就是插得不夠理想,根據直接插入排序的時間複雜度,最好的情況可以達到線性的程度,一般情況下,卻不是這樣的,每次插入一個,可能要移動大量資料,我們希望在執行直接插入前,能夠儘量的保持有序。

思想

取一個增量d1<n,使得距離為d1的元素分在一組,每組進行直接插入排序,然後再取d2<d1,進行排序,直到所有元素都在一組,即增量為1。

拆解分析

java程式碼實現

public static void shellSort(int[] array) {
    for (int n = array.length, d = n / 2; d > 0; d /= 2) {//取增量為長度的一半,每次減半,直到d=1,但是d=1必須得排序,因此最後的判斷為d>0
        for (int x = 0; x < d; x++) {//分組
            for (int i = x + d; i < n; i += d) {//每組進行直接插入排序
                int temp = array[i];
                int j = i - d;
                for (; j >= 0 && array[j] > temp; j = j - d) {
                    array[j + d] = array[j];
                }
                array[j + d] = temp;
            }
        }
    }
    System.out.println("希爾排序後:" + Arrays.toString(array));
}複製程式碼

希爾排序的最好與最壞時間複雜度同直接插入排序,平均時間複雜度為O(n^1.3),不要問我1.3怎麼來的,這跟增量的取值有關係,空間複雜度為O(1),由於在最後一次直接排序前,經過分組排序,所以可能重複元素的相對位置會交換,因此它是不穩定的

執行結果

簡單選擇排序

我記得當初老師讓我們寫一個排序演算法,我第一個想的就是這個,可能大多數都是這個?很厲害了是不是?至少也是有名的排序演算法。

思想

在一組元素中,暴力找出最小的元素與第一個位置的元素交換,然後從剩下的元素中,選取最小的,與第二個位置交換,以此類推。

拆解分析

  1. 5-2-0-1-3-1-4,原始資料,遍歷所有元素找到最小元素0與第一個位置交換
  2. 0-2-5-1-3-1-4,然後從剩下的元素2-4中找出最小元素1,這個1是原始資料的第一個1,因為遍歷取最小的時候,只有比當前小的才被記錄為最小值,最後1與第二個位置元素交換
  3. 0-1-5-2-3-1-4,從5-4中找出1,與第三個位置元素交換
  4. 0-1-1-2-3-5-4,從2-4中找出2,與第四個位置元素交換
  5. 0-1-1-2-3-5-4,從3-4中找出3,與第五個位置元素交換
  6. 0-1-1-2-3-5-4,從5-4中找出4,與第六個位置元素交換
  7. 0-1-1-2-3-4-5,最後一個5與最後一個位置元素交換,排序結束

java程式碼實現

public static void selectSort(int[] array) {
    for (int i = 0, n = array.length; i < n; i++) {
        int j = i + 1;
        int temp = array[i];
        int position = i;
        for (; j < n; j++) {
            if (array[j] < temp) {
                temp = array[j];
                position = j;
            }
        }
        array[position] = array[i];
        array[i] = temp;
    }
    System.out.println("簡單選擇排序後:" + Arrays.toString(array));
}複製程式碼

從程式碼上看,簡單選擇排序似乎沒有什麼最好最壞的時候,總是這麼暴力,時間複雜度總是為O(n^2),空間複雜度為O(1),由於每次取到最小值後都要與前面位置元素交換,因此破壞了元素的相對位置,所以它是不穩定的。

執行結果

堆排序

說堆排序之前,必須說一下堆的概念:
完全二叉樹中任一非葉子結點的關鍵字均不大於(或不小於)其左右孩子(若存在)結點的關鍵字。而我們這裡取不小於,也稱之為大根堆。

思想

利用大根堆的性質,每次把元素組建堆,取出最大值,放入最後,直到最後一位,排序完成。

拆解分析

以此類推,直到完成最後一個,排序完成。

java程式碼實現

public static void heapSort(int[] array) {
    //從第一個非葉子結點開始,建堆
    int n = array.length;
    int startIndex = (n - 1 - 1) / 2;
    for (int i = startIndex; i >= 0; i--) {
        maxHeapify(array, n, i);
    }

    //末尾與頭交換,交換後調整最大堆
    for (int i = n - 1; i > 0; i--) {
        int temp = array[0];
        array[0] = array[i];
        array[i] = temp;
        maxHeapify(array, i, 0);
    }

    System.out.println("堆排序後:" + Arrays.toString(array));
}


/**
 * 建立最大堆
 *
 * @param array    元素組
 * @param heapSize 需要建立最大堆的大小,一般在sort的時候用到,因為最大值放在末尾,末尾就不再歸入最大堆了
 * @param index    當前需要建立最大堆的位置
 */
private static void maxHeapify(int[] array, int heapSize, int index) {
    int left = index * 2 + 1;//左子節點
    int right = left + 1;//右子節點

    int largest = index;
    if (left < heapSize && array[index] < array[left]) {
        largest = left;
    }
    if (right < heapSize && array[largest] < array[right]) {
        largest = right;
    }
    //得到最大值後可能需要交換,如果交換了,其子節點可能就不符合堆要求了,需要重新調整
    if (largest != index) {
        int temp = array[index];
        array[index] = array[largest];
        array[largest] = temp;
        maxHeapify(array, heapSize, largest);
    }
}複製程式碼

從程式碼直接看時間複雜度其實挺難,我這裡直接給答案,堆排序的平均、最差、最壞時間複雜度都是O(nlog2n),因為它永遠都是一個套路,對這個時間複雜度怎麼來的,可以網上搜搜,我相信你是棒棒的。空間複雜度為O(1),因為它需要建堆還要重新調整堆,肯定是沒法保證元素的相對位置的,所以它是不穩定的。

執行結果

氣泡排序

氣泡排序可以想象一下,魚吐泡泡,一個一個泡泡往上冒。

思想

元素之間兩兩比較,最小數往上冒,或者最大數向下沉,直到排序完成。

拆解分析

我們這裡以往上冒為例。

  1. 5-2-0-1-3-1-4,1與4比較,小者1排前面
  2. 5-2-0-1-3-1-4,3與1比較,小者1排前面
  3. 5-2-0-1-1-3-4,1與1比較,小者1排前面
  4. 5-2-0-1-1-3-4,0與1比較,小者0排前面
  5. 5-2-0-1-1-3-4,2與0比較,小者0排前面
  6. 5-0-2-1-1-3-4,5與0比較,小者0排前面
  7. 0-5-2-1-1-3-4,此時完成第0趟冒泡,同理完成剩下的冒泡

java程式碼實現

public static void bubbleSort(int[] array) {
    int n = array.length;
    for (int i = 0; i < n - 1; i++) {
        for (int j = n - 1 - 1; j >= i; j--) {
            if (array[j + 1] < array[j]) {
                int temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
            }
        }
        System.out.println("第" + i + "趟:" + Arrays.toString(array));
    }
    System.out.println("氣泡排序後:" + Arrays.toString(array));
}複製程式碼

如果你單純從上面的程式碼看,平均、最好、最差時間複雜度都是O(n^2),如果看過其它文章的同學可能會說最好時間複雜度應該是O(n),那是因為加入了標誌位,具體程式碼我不貼了,空間複雜度是O(1),由於一直是兩兩比較,並沒有改變相對位置的操作,所以是穩定的。

執行結果

快速排序

思想

選擇一個基數,一般我們選擇第一個數,然後把大於該數的放右邊,小於該數的放左邊,然後分別對左右兩邊用同樣的方法處理,直到排序結束。

拆解分析

java程式碼實現

public static void quickSort(int[] array) {
    _quickSort(array, 0, array.length - 1);
    System.out.println("快速排序後:" + Arrays.toString(array));
}


private static int getMiddle(int[] array, int low, int high) {
    int tmp = array[low];    //陣列的第一個作為基數
    while (low < high) {    //直到指標重合一趟完成
        while (low < high && array[high] >= tmp) {
            high--;
        }

        array[low] = array[high];   //找到比基數小的
        while (low < high && array[low] <= tmp) {
            low++;
        }

        array[high] = array[low];   //找到比基數大的
    }
    array[low] = tmp;              //基數歸位
    System.out.println(Arrays.toString(array));
    return low;                  //返回基數的位置
}


private static void _quickSort(int[] array, int low, int high) {
    if (low < high) {
        int middle = getMiddle(array, low, high);  //基於第一個數將array陣列進行一分為二
        _quickSort(array, low, middle - 1);      //左邊進行遞迴排序
        _quickSort(array, middle + 1, high);      //右邊進行遞迴排序
    }
}複製程式碼

關於快速排序的時間複雜度,表示三言兩語真的說不清楚,感興趣的朋友可以翻閱相關書籍,有能力的可以自己證明- -!最好時間複雜度為O(nlog2n),情況為能夠正好根據基數平均劃分元素組,最差時間複雜度為O(n^2),情況為元素組呈正序或者逆序狀態,平均時間複雜度為O(nlog2n),因為快速排序是遞迴進行的,需要牽涉到遞迴深度,空間複雜度為O(nlog2n),準確來說是平均空間複雜度,由於快速排序在跟基數比較的時候,可能會交換而破壞了元素之間的相對位置,因此快速排序是不穩定的。

執行結果

歸併排序

思想

採用經典分治思想,將一個元素組劃分多個有序的小元素組,然後將這些小元素組合併成一個有序的元素組。

拆解分析

java程式碼實現

public static void mergeSort(int[] array) {
    sort(array, 0, array.length - 1);
    System.out.println("歸併排序:" + Arrays.toString(array));
}

private static void sort(int[] array, int left, int right) {
    if (left < right) {
        //找出中間索引
        int center = (left + right) / 2;
        //對左邊陣列進行遞迴
        sort(array, left, center);
        //對右邊陣列進行遞迴
        sort(array, center + 1, right);
        //合併
        merge(array, left, center, right);
    }
}

private static void merge(int[] array, int left, int center, int right) {
    int[] tmpArr = new int[array.length];
    int mid = center + 1;
    int third = left;//third記錄中間陣列的索引
    int tmp = left;//複製時用到的索引
    while (left <= center && mid <= right) {
        //從兩個陣列中取出最小的放入中間陣列
        if (array[left] <= array[mid]) {
            tmpArr[third++] = array[left++];
        } else {
            tmpArr[third++] = array[mid++];
        }
    }

    //剩餘部分依次放入中間陣列
    while (mid <= right) {
        tmpArr[third++] = array[mid++];
    }

    while (left <= center) {
        tmpArr[third++] = array[left++];
    }

    //將中間陣列中的內容複製回原陣列
    while (tmp <= right) {
        array[tmp] = tmpArr[tmp++];
    }
    System.out.println(Arrays.toString(array));
}複製程式碼

由於歸併排序就一個套路而且合併的時候是從左往右,因此不會破壞元素的相對位置,是穩定的,同時它的最好、最壞、平均時間複雜度都是O(nlog2n),簡單來說它是基於完全二叉樹的,其深度為log2n,每次合併操作都是一個n級規模,因此為nlog2n,而空間複雜度除了深度log2n以外,我們還需要臨時陣列,因此空間複雜度為O(n)=O(n)+O(log2n)。

執行結果

由於是遞迴,可能與理想輸出有所差距。

基數排序

思想

將一組元素進行桶分配,啥意思?比如數字250,百位是2,十位是5,個位是0,而這些個位,十位等就是所謂的桶。

拆解分析

由於測試資料全是個位數,所以只要進行一次就結束了,如果有更高位的,將一直進行到最高位。

java程式碼實現

public static void radixSort(int[] array) {
    int max = array[0];
    final int length = array.length;
    for (int i = 1; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        }
    }
    int time = 0;//陣列最大值位數
    while (max > 0) {
        max /= 10;
        time++;
    }

    int k = 0; //重新放入陣列的索引
    int n = 1; //位值,如1,10,100
    int m = 1; //當前在哪一位
    int[][] temp = new int[10][length]; //陣列的第一維表示該位數值,二維表示具體的值
    int[] order = new int[10]; //陣列order[i]用來表示該位是i的數的個數
    while (m <= time) {

        for (int num : array) {
            int lsd = (num / n) % 10;//獲取該位的基數0-9
            temp[lsd][order[lsd]] = num;
            order[lsd]++;
        }

        for (int i = 0; i < 10; i++) {
            if (order[i] != 0) {
                for (int j = 0; j < order[i]; j++) {
                    array[k] = temp[i][j];//基於m位的重新放入陣列中
                    k++;
                }
            }
            order[i] = 0;//復位
        }
        System.out.println("第" + m + "位排序:" + Arrays.toString(array));
        n *= 10;
        k = 0;//復位
        m++;
    }

    System.out.println("基數排序後:" + Arrays.toString(array));
}複製程式碼

直接給答案,最優時間複雜度為O(d(r+n))最差時間複雜度為O(d(r+n))平均時間複雜度為O(d(r+n))空間複雜度為O(rd+n),其中r代表關鍵字基數,d代表長度,n代表關鍵字個數,由於是分配且從左往右,因此是穩定的。

執行結果

程式碼已經貼出來了,由於測試資料確實比較簡單,大家可以自己使用複雜的原始資料進行測試。

總結

到這裡,8種常見的排序演算法介紹的差不多了,如果你心裡想的是,臥槽,這麼簡單,我馬上可以在我的小本本上寫出來,那麼我也沒白寫這篇文章。如果你是一臉懵逼,我心裡可能想的是,臥槽,竟然沒糊弄過去。哈哈,不管怎樣,演算法並不是什麼高階的東西,其實你天天都在寫演算法,只不過。。。嘿嘿。演算法可能有高低之分,但適合自己的才是最好的。儘量領會其中的思想,來完善你的演算法吧。而這篇文章只不過是拋磚引玉,同我上篇文章帶你領略clean架構的魅力,你看,是不是很多架構的文章?我發現我越來越自戀了,哈哈,但真的很希望更強的大佬能分享學習心得,我完全贊成打賞這種形式,甚至是你的小圈圈,但也得用點心啊。小弟,我心是用了,但是能力可能不足,如有錯誤,麻煩提出來,我及時修改。最後,感謝一直支援我的人!誒,差點沒堅持下來。

哦,對了,我猜小夥伴又要吐槽我的作圖,其實我覺得挺好的,不是嗎?

傳送門

Github:github.com/crazysunj/

部落格:crazysunj.com/

參考資料:blog.csdn.net/qy1387/arti…

相關文章