排序算法 Java實現

選擇排序

核心思想

選擇最小元素,與第一個元素交換位置;剩下的元素中選擇最小元素,與當前剩餘元素的最前邊的元素交換位置。java

分析

選擇排序的比較次數與序列的初始排序無關,比較次數都是N(N-1)/2算法

移動次數最多隻有n-1次。shell

所以,時間複雜度爲O(N^2),不管輸入是否有序都是如此,輸入的順序只決定了交換的次數,可是比較的次數不變。數組

選擇排序是不穩定的,好比5 6 5 3的狀況。緩存

代碼

public class SelectionSort {
    public void selectionSort(int[] nums){
        if(nums==null)
            return;
        for(int i=0;i<nums.length;i++) {
            int index = i;
            for (int j = i; j < nums.length; j++) {
                if (nums[j] < nums[index]) {
                    index = j;
                }
            }
            swap(nums, i, index);
        }
    }
}
複製代碼

冒泡排序:

核心思想

從左到右不斷交換相鄰逆序的元素,這樣一趟下來把最大的元素放到了最右側。不斷重複這個過程,知道一次循環中沒有發生交換,說明已經有序,退出。bash

分析

  • 當原始序列有序,比較次數爲 n-1 ,移動次數爲0,所以最好狀況下時間複雜度爲 O(N)
  • 當逆序排序時,比較次數爲 N(N-1)/2,移動次數爲 3N(N-1)/2,所以最壞狀況下時間複雜度爲 O(N^2)
  • 平均時間複雜度爲 O(N^2)。

元素兩兩交換時,相同元素先後順序沒有改變,所以具備穩定性。函數

代碼

public class BubbleSort {
    public void bubbleSort(int[] nums){
        for(int i=nums.length-1;i>0;i--){
            boolean sorted=false;
            for(int j=0;j<i;j++){
                if(nums[j]>nums[j+1]){
                    Sort.swap(nums,j,j+1);
                    sorted=true;
                }
            }
            if(!sorted)
                break;
        }
    }
複製代碼

插入排序

核心思想

每次將當前元素插入到左側已經排好序的數組中,使得插入以後左側數組依然有序。ui

分析

由於插入排序每次只能交換相鄰元素,令逆序數量減小1,所以交換次數等於逆序數量。spa

所以,插入排序的複雜度取決於數組的初始順序。操作系統

  • 數組已經有序,須要 N-1 次比較和0次交換,時間複雜度爲 O(N)。
  • 數組徹底逆序,須要 N(N-1)/2 次比較和交換 N(N-1)/2 次,時間複雜度爲 O(N^2)
  • 平均狀況下,時間複雜度爲 O(N^2)

插入排序具備穩定性

代碼

public class InsertionSort {
    public void insertionSort(int[] nums){
        for(int i=1;i<nums.length;i++){
            for(int j=i;j>0;j--){
                if(nums[j]<nums[j-1])
                    swap(nums,j,j-1);
                else
                    break;//已經放到正確位置上了
            }
        }
    }
}
複製代碼

希爾排序

對於大規模的數組,插入排序很慢,由於它只能交換相鄰的元素,每次只能將逆序數量減小1。

核心思想

希爾排序爲了解決插入排序的侷限性,經過交換不相鄰的元素,每次將逆序數量減小大於1。希爾排序使用插入排序對間隔爲 H 的序列進行排序,不斷減小 H 直到 H=1 ,最終使得整個數組是有序的。

時間複雜度

希爾排序的時間複雜度難以肯定,而且 H 的選擇也會改變其時間複雜度。

希爾排序的時間複雜度是低於 O(N^2) 的,高級排序算法只比希爾排序快兩倍左右。

穩定性

希爾排序不具有穩定性。

代碼

public class ShellSort {
    public void shellSort(int[] nums){
        int N=nums.length;
        int h=1;

        while(h<N/3){
            h=3*h+1;
        }

        while(h>=1){
            for(int i=h;i<N;i++){
                for(int j=i;j>0;j--){
                    if(nums[j]<nums[j-1]){
                        swap(nums,j,j-1);
                    }else{
                        break;//已經放到正確位置上了
                    }
                }
            }
        }
    }
}
複製代碼

歸併排序

核心思想

將數組分爲兩部分,分別進行排序,而後進行歸併。

歸併方法

public void merge(int[] nums, int left, int mid, int right) {
        int p1 = left, p2 = mid + 1;
        int[] tmp = new int[right-left+1];
        int cur=0;
        
        //兩個指針分別指向左右兩個子數組,選擇更小者放入輔助數組
        while(p1<=mid&&p2<=right){
            if(nums[p1]<nums[p2]){
                tmp[cur++]=nums[p1++];
            }else{
                tmp[cur++]=nums[p2++];
            }
        }
        
        //將還有剩餘的數組放入到輔助數組
        while(p1<=mid){
            tmp[cur++]=nums[p1++];
        }
        while(p2<=right){
            tmp[cur++]=nums[p2++];
        }

        //拷貝
        for(int i=0;i<tmp.length;i++){
            nums[left+i]=tmp[i];
        }
    }
複製代碼

代碼實現

遞歸方法:自頂向下

經過遞歸調用,自頂向下將一個大數組分紅兩個小數組進行求解。

public void up2DownMergeSort(int[] nums, int left, int right) {
        if(left==right)
            return;
        int mid=left+(right-left)/2;
        mergeSort(nums,left,mid);
        mergeSort(nums,mid+1,right);
        merge(nums,left,mid,right);
    }
複製代碼

非遞歸:自底向上

public void down2UpMergeSort(int[] nums) {
        int N = nums.length;
       
        for (int sz = 1; sz < N; sz += sz) {
            for (int lo = 0; lo < N - sz; lo += sz + sz) {
                merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
            }
        }
    }
複製代碼

分析

把一個規模爲N的問題分解成兩個規模分別爲 N/2 的子問題,合併的時間複雜度爲 O(N)。T(N)=2T(N/2)+O(N)。

獲得其時間複雜度爲 O(NlogN),而且在最壞、最好和平均狀況下時間複雜度相同。

歸併排序須要 O(N) 的空間複雜度。

歸併排序具備穩定性。

快速排序

核心思想

快速排序經過一個切分元素 pivot 將數組分爲兩個子數組,左子數組小於等於切分元素,右子數組大於等於切分元素,將子數組分別進行排序,最終整個排序。

partition

取 a[l] 做爲切分元素,而後從數組的左端向右掃描直到找到第一個大於等於它的元素,再從數組的右端向左掃描找到第一個小於它的元素,交換這兩個元素。不斷進行這個過程,就能夠保證左指針 i 的左側元素都不大於切分元素,右指針 j 的右側元素都不小於切分元素。當兩個指針相遇時,將切分元素 a[l] 和 a[j] 交換位置。

private int partition(int[] nums, int left, int right) {
        int p1=left,p2=right;
        int pivot=nums[left];
        while(p1<p2){
            while(nums[p1++]<pivot&&p1<=right);
            while(nums[p2--]>pivot&&p2>=left);
            swap(nums,p1,p2);
        }
        swap(nums,left,p2);
        return p2;
    }
複製代碼

代碼實現

public void sort(T[] nums, int l, int h) {
        if (h <= l)
            return;
        int j = partition(nums, l, h);
        sort(nums, l, j - 1);
        sort(nums, j + 1, h);
    }
複製代碼

分析

最好的狀況下,每次都正好將數組對半分,遞歸調用次數最少,複雜度爲 O(NlogN)。

最壞狀況下,是有序數組,每次只切分了一個元素,時間複雜度爲 O(N^2)。爲了防止這種狀況,在進行快速排序時須要先隨機打亂數組。

不具備穩定性。

改進

  1. 切換到插入排序:遞歸的子數組規模小時,用插入排序。
  2. 三數取中:最好的狀況下每次取中位數做爲切分元素,計算中位數代價比較高,採用取三個元素,將中位數做爲切分元素。

三路快排

對於有大量重複元素的數組,將數組分爲小於、等於、大於三部分,對於有大量重複元素的隨機數組能夠在線性時間內完成排序。

public void threeWayQuickSort(int[] nums,int left,int right){
        if(right<=left)
            return;

        int lt=left,cur=left+1,gt=right;
        int pivot=nums[left];
        while(cur<=gt){
            if(nums[cur]<pivot){
                swap(nums,lt++,cur++);
            }else if(nums[cur]>pivot){
                swap(nums,cur,gt--);
            }else{
                cur++;
            }
        }
        threeWayQuickSort(nums,left,lt-1);
        threeWayQuickSort(nums,gt+1,right);
    }
複製代碼

基於 partition 的快速查找

利用 partition() 能夠在線性時間複雜度找到數組的第 K 個元素。

假設每次能將數組二分,那麼比較的總次數爲 (N+N/2+N/4+..),直到找到第 k 個元素,這個和顯然小於 2N。

public int select(int[] nums, int k) {
    int l = 0, h = nums.length - 1;
    while (h > l) {
        int j = partition(nums, l, h);

        if (j == k) {
            return nums[k];

        } else if (j > k) {
            h = j - 1;

        } else {
            l = j + 1;
        }
    }
    return nums[k];
}
複製代碼

堆排序

堆能夠用數組來表示,這是由於堆是徹底二叉樹,而徹底二叉樹很容易就存儲在數組中。位置 k 的節點的父節點位置爲 k/2,而它的兩個子節點的位置分別爲 2k 和 2k+1。在這裏,從下標爲1的索引開始 的位置,是爲了更清晰地描述節點的位置關係。

上浮和下沉

當一個節點比父節點大,不斷交換這兩個節點,直到將節點放到位置上,這種操做稱爲上浮。

private void shiftUp(int k) {
        while (k > 1 && heap[k / 2] < heap[k]) {
            swap(k / 2, k);
            k = k / 2;
        }
    }
複製代碼

當一個節點比子節點小,不斷向下進行比較和交換,當一個基點有兩個子節點,與最大節點進行交換。這種操做稱爲下沉。

private void shiftDown(int k){
        while(2*k<=size){
            int j=2*k;
            if(j<size&&heap[j]<heap[j+1])
                j++;
            if(heap[k]<heap[j])
                break;
            swap(k,j);
            k=j;
        }
    }
複製代碼

堆排序

把最大元素和當前堆中數組的最後一個元素交換位置,而且不刪除它,那麼就能夠獲得一個從尾到頭的遞減序列。

構建堆 創建堆最直接的方法是從左到右遍歷數組進行上浮操做。一個更高效的方法是從右到左進行下沉操做。葉子節點不須要進行下沉操做,能夠忽略,所以只須要遍歷一半的元素便可。

交換堆頂和最壞一個元素,進行下沉操做,維持堆的性質。

public class HeapSort {
    public void sort(int[] nums){
        int N=nums.length-1;
        for(int k=N/2;k>=1;k--){
            shiftDown(nums,k,N);
        }

        while(N>1){
            swap(nums,1,N--);
            shiftDown(nums,1,N);
        }
        System.out.println(Arrays.toString(nums));
    }

    private void shiftDown(int[] heap,int k,int N){
        while(2*k<=N){
            int j=2*k;
            if(j<N&&heap[j]<heap[j+1])
                j++;
            if(heap[k]>=heap[j])
                break;
            swap(heap,k,j);
            k=j;
        }
    }

    private void swap(int[] nums,int i,int j){
        int t=nums[i];
        nums[i]=nums[j];
        nums[j]=t;
    }
}
複製代碼

分析

創建堆的時間複雜度是O(N)。

一個堆的高度爲 logN, 所以在堆中插入元素和刪除最大元素的複雜度都是 logN。

在堆排序中,對N個節點進行下沉操做,複雜度爲 O(NlogN)。

現代操做系統不多使用堆排序,由於它沒法利用局部性原理進行緩存,也就是數組元素不多和相鄰的元素進行比較和交換。

比較

排序算法 最好時間複雜度 平均時間複雜度 最壞時間複雜度 空間複雜度 穩定性 適用場景
冒泡排序 O(N) O(N^2) O(N^2) O(1) 穩定
選擇排序 O(N) O(N^2) O(N^2) O(1) 不穩定 運行時間和輸入無關,數據移動次數最少,數據量較小的時候適用。
插入排序 O(N) O(N^2) O(N^2) O(1) 穩定 數據量小、大部分已經被排序
希爾排序 O(N) O(N^1.3) O(N^2) O(1) 不穩定
快速排序 O(NlogN) O(NlogN) O(N^2) O(logN)-O(N) 不穩定 最快的通用排序算法,大多數狀況下的最佳選擇
歸併排序 O(NlogN) O(NlogN) O(NlogN) O(N) 穩定 須要穩定性,空間不是很重要
堆排序 O(NlogN) O(NlogN) O(NlogN) O(1) O(1) 不穩定
  • 當規模較小,如小於等於50,採用插入或選擇排序。
  • 當元素基本有序,選擇插入、冒泡或隨機的快速排序。
  • 當規模較大,採用 O(NlogN)排序算法。
  • 當待排序的關鍵字隨機分佈時,快速排序的平均時間最短。
  • 當須要保證穩定性的時候,選用歸併排序。

非比較排序

以前介紹的算法都是基於比較的排序算法,下邊介紹兩種不是基於比較的算法。

計數排序

已知數據範圍 x1 到 x2, 對範圍中的元素進行排序。能夠使用一個長度爲 x2-x1+1 的數組,存儲每一個數字對應的出現的次數。最終獲得排序後的結果。

桶排序

桶排序假設待排序的一組數均勻獨立的分佈在一個範圍中,並將這一範圍劃分紅幾個桶。而後基於某種映射函數,將待排序的關鍵字 k 映射到第 i 個桶中。接着將各個桶中的數據有序的合併起來,對每一個桶中的元素能夠進行排序,而後輸出獲得一個有序序列。

相關文章
相關標籤/搜索