1. 程式人生 > >基本排序演算法-java實現

基本排序演算法-java實現

最近重新學習了排序演算法,之前每次看完當時理解了,但是過一段時間就又忘了,尤其是程式碼,如果放一段時間有很多base case不知道怎麼寫了,所以還是應該詳細的解讀一下再不斷了敲程式碼才能理解比較深刻。

1.氣泡排序(bubble sort)

氣泡排序是一種簡單的排序演算法。其基本思想是迭代地輸出序列中的第一個到最後一個元素進行兩兩比較,當需要時交換這兩個元素的位置。氣泡排序得名於鍵值小的元素如同氣泡一樣逐漸漂浮到序列的頂端。
1)時間複雜度 O(n*n)
2)空間複雜度O(1)
3 )穩定性:是

程式碼
private static void bubbleSort(int[] arr) {

  for (int i = arr.length - 1; i > 0; i--) {
        for (int j = 0; j < i; j++) {
            if (arr[i] < arr[j]) {
                swap(arr, i, j);
            }
        }
    }
}

ps:https://visualgo.net/zh 這個網站可以看到動態的資料結構過程,所有排序方法可以輸入例項來檢視具體排序實現過程,推薦給大家。

2.選擇排序(selection sort)

選擇排序是一種原地排序(in-place)排序演算法,適用於小檔案,由於選擇操作是基於鍵值的且交換操作只在需要時才執行,所以選擇排序適用於數值較大和鍵值較小的檔案。
1)優點:
-容易實現
-原地排序(不需要額外的儲存空間)
2)缺點:
-擴充套件性較差
時間複雜度O(n*n)
3)演算法:先尋找序列中的最小值,用當前位置交換最小值,重複上述過程直到整個序列排序完成。

程式碼:

public static void selectionSort(int[] arr) {

    for (int i = 0; i < arr.length - 1; i++) {
        int minIndex = i;
        for (int j = i + 1; j < arr.length; j++) {
            minIndex = arr[j] < arr[minIndex] ? j : minIndex;
        }
        swap(arr, i, minIndex);
    }
}

3 .插入排序(insertion sort)

插入排序(insertion sort)是一種簡單且有效的比較排序方法,在每次迭代過程中演算法隨機地在序列中一出一個元素,並將該元素插入待排序序列的正確位置。重複該過程,直到所有蒜素都被選擇一次。
1)優點
-實現簡單
-資料量少時效率高
-適應性(adaptive):如果輸入的序列已經排序,則時間複雜度為O(n+d), d為反轉的次數
-穩定性(stable):鍵值相同時它能夠保持輸入資料的原有次序
-原地(in-place):僅需要常量 O(1)的輔助記憶體空間
-即使(online):插入排序能夠在接收序列的同時對其進行排序
2)演算法插入演算法主要是每個arr[i]和之前排好序的佇列進行比較,並在合適的位置插入。

程式碼:
public static void insertionSort(int[] arr) {

    if (arr == null || arr.length < 2) {
        return;
    }
    for (int i = 1; i < arr.length; i++) {
        for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
            swap(arr, j, j + 1);
        }
    }
}

example:
6 8 1 4 5 3 7 2-原序列
6 8 1 4 5 3 7 2(考慮索引位置0)
6 8 1 4 5 3 7 2 (考慮索引位置0-1)
1 6 8 4 5 3 7 2(考慮索引位置0-2,在6和8之間插入1)
1 4 6 8 5 3 7 2(重複上述過程直到序列排序完成)
1 4 5 6 8 3 7 2
1 3 4 5 6 7 8 2
1 2 3 4 5 6 7 8 (排序完成)

4)歸併排序

對於我來說,排序演算法難點就在於歸併,快排和堆排序,因為這些排序方法比較複雜,而且牽扯到遞迴,而遞迴底層使用遞迴棧實現所以空間複雜度不好估計,而時間複雜度這三種排序方法都是O(nlogn),我記得MIT的演算法公開課中都是用遞迴樹來解決這些排序方法。

歸併排序(merge sort)是分治的一個例項,其思想在於分(divide)和治(conquer)。首先將原始的待排序序列分開成若干個子序列,然後直到每個組都只有一個數值,然後合併過程,需要建立一個輔助空間進行存放,存放值為兩個待排序子序列中較小的值,一直這樣重複的遞迴下去,最後直到還剩兩個子序列時,合併的結果就是最終的排好序的。
【例子】
原陣列 65214387-divide
6521 4387-divide
65 21 43 87-divide
6 5 2 1 4 3 8 7-divide
56 12 34 78-conquer 比較大小併合並
1256 3478 -conquer 比較大小併合並
12345678-conquer 比較大小併合並

【例子2】
3 7 5 9
合併:
設定輔助陣列:
3 (5比3大,3放入陣列中)
3 5 (7比5大,5放入陣列中)
3 5 7 (9比7大,7放入陣列中)
3 5 7 9 (數列1結束,直接將數列2放入輔助陣列末尾,即9放入陣列中)
完成一次合併排序。
下一次,3 5 7 9 可以和 1 4 6 8進行合併排序。

private static void mergesort(int[] arr, int l, int r) {

    if (l == r)
        return;
    int mid = (l + r) / 2;
    mergesort(arr, l, mid);
    mergesort(arr, mid + 1, r);
    merge(arr, l, mid, r);
}

private static void merge(int[] arr, int l, int mid, int r) {
    int[] help = new int[r - l + 1];
    int i = 0;
    int p1 = l;
    int p2 = mid + 1;
    while (p1 <=mid && p2 <= r) {// 把較小的數先移到新陣列中  
        help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
    }
    while (p1 <= mid) { // 把左邊剩餘的數移入陣列  
        help[i++] = arr[p1++];
    }
    while (p2 <= r) { // 把右邊邊剩餘的數移入陣列  
        help[i++] = arr[p2++];
    }
    for (int j = 0; j < help.length; j++) { // 把新陣列中的數覆蓋arr陣列  
        arr[l + j] = help[j];
    }

}

注意:
1-判斷條件(p1 <=mid && p2 <= r)一定要有等於,否則輔助陣列會有0
2-輔助陣列的大小為r - l + 1,然後傳遞給原序列大小為l + j

分析:
根據maste公式

時間複雜度為O(nlogn),其實也可以畫一個遞迴樹,然後求樹的高度和葉子節點數。

5)快速排序(quick sort)

快速排序(quick sort)是分治演算法技術中的一個例項,也稱為分割槽交換排序。主要通過遞迴呼叫元素進行排序,是基於比較的排序方法。

劃分:陣列a[low..high]被分為兩個非空子陣列a[low…q]和a[q+1…high],使得a[low..q]中的每一個元素都小於等於a[q+1…high]中的元素,劃分中需要索引q的位置。傳統快排是一般選擇陣列最左邊的元素為軸點(pivot),還有隨機化快速排序是隨機的選擇軸點,這兩種都只有一個最大區間或者最小區間,而改良版的快排序有最大和最小兩個區間來排序和等於pivot的區間,這樣序列待排序量減小,在工程上會大大減小時間。

過程:
1)如果陣列中僅有一個元素或者沒有元素需要被排序,返回。
2)選擇陣列中的一個元素作為樞軸點(pivot),通常最左邊的元素為軸點。
3)把陣列分為兩部分—一部分元素小於pivot,一部分大於pivot。
4)對兩部分遞迴呼叫

程式碼:

private static void quick(int[] arr, int low, int high) {

    while (low < high) {
        int[] pivot = partition(arr, low, high);
        quick(arr, low, pivot[0] - 1);
        quick(arr, pivot[1] + 1, high);

    }


}

private static int[] partition(int[] arr, int l, int r) {

    int less = l - 1;
    int more = r;
    while (l < more) {
        if (arr[l] < arr[r]) {
            swap(arr, ++less, l++);
        } else if (arr[l] > arr[r]) {
            swap(arr, --more, l);
        } else {
            l++;
        }
    }
    swap(arr, more, r);
    return new int[]{less + 1, more};
}

注意:在java語言中Arrays.sort()函式對陣列進行排序,如果是簡單陣列如1,2,3,4等底層進行快排,如果為複雜情況如對student進行排序則預設為歸併排序,因為歸併排序具有穩定性。

6)堆排序(heapsort)

我認為堆排序的難點在於有樹的結構,所以對初學者來說很難使用,但是熟練以後,堆排序是非常神奇的排序過程,時間複雜度為O(nlogn),很多面試題需要排好序再進行操作時都會呼叫堆排序過程。

過程:
1.首先可將待排序陣列變成一個樹結構再變成大根堆,因為大根堆是一棵完全二叉樹,所以樹某一節點i的左孩子為2*i+1,右孩子為2*i+2,於是先迭代出大根堆。
2.然後有了大根堆下面進行下沉工作(heapify),過程為取堆的頭節點,因為是大根堆,所以頭節點為最大節點,與最後一個節點進行交換,然後序列長度減1,證明最大的樹已經放到隊尾,並減小了遞迴範圍。
3.於是對現有的樹進行變成大根堆的操作,直到變成了大根堆再重複上面的操作。
時間複雜度:O(Nlog(N))
額外空間複雜度:O(1)
是否可實現穩定性:否

程式碼:
public static void heapInsert(int[] arr, int index) {//建立大根堆

    while (arr[index] > arr[(index - 1) / 2]) {
        swap(arr, index, (index - 1) / 2);
        index = (index - 1) / 2;
    }
}

public static void heapify(int[] arr, int index, int size) {//下沉操作
    int left = index * 2 + 1;
    while (left < size) {
        int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
        largest = arr[largest] > arr[index] ? largest : index;
        if (largest == index) {
            break;
        }
        swap(arr, largest, index);
        index = largest;
        left = index * 2 + 1;
    }
}