1. 程式人生 > 實用技巧 >面試題: java中常見的排序演算法的實現及比較

面試題: java中常見的排序演算法的實現及比較

目錄

1.氣泡排序

1.1 氣泡排序普通版

  每次冒泡過程都是從數列的第一個元素開始,然後依次和剩餘的元素進行比較,若小於相鄰元素,則交換兩者位置,同時將較大元素作為下一個比較的基準元素,繼續將該元素與其相鄰的元素進行比較,直到數列的最後一個元素 . 示意圖如下:

/**
 * 氣泡排序:
 *  依次比較相鄰的元素,若發現逆順序,則交換。小的向前換,大的向後換,
 *  本次迴圈完畢之後再次從頭開始掃描,直到某次掃描中沒有元素交換,
 *  說明每個元素都不比它後面的元素大,至此排序完成。
 */
import java.util.Arrays;
public class BubbleSort {
    public static void main(String[] args) {
        int[] arr = new int[]{9, 2, 1, 0, 5, 3, 6, 4, 8, 7};
        System.out.println("排序前:" + Arrays.toString(arr));
        sort(arr);
        System.out.println("排序後:" + Arrays.toString(arr));
    }
    public static void sort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {  //第一層for迴圈,用來控制冒泡的次數
            for (int j = 0; j < arr.length - 1; j++) { //第二層for迴圈,用來控制冒泡一層層到最後
                //如果前一個數比後一個數大,兩者調換 ,意味著泡泡向上走了一層
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
}

執行結果:

排序前:[9, 2, 1, 0, 5, 3, 6, 4, 8, 7]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1.2 冒泡排序升級版

  在這個版本中,改動了兩點 . 第一點是加入了一個布林值,判斷第二層迴圈中的調換有沒有執行,如果沒有進行兩兩調換,說明後面都已經排好序了,已經不需要再迴圈了,直接跳出迴圈,排序結束 ; 第二點是第二層迴圈不再迴圈到arr.length - 1,因為外面的i迴圈遞增一次,說明陣列最後就多了一個排好序的大泡泡.第二層迴圈也就不需要到最末尾一位了,可以提前結束迴圈

/**
     * 升級版氣泡排序:
     * 加入一個布林變數,如果內迴圈沒有交換值,說明已經排序完成,提前終止
     */
import java.util.Arrays;
public class BubbleSort {
    public static void main(String[] args) {
        int[] arr = new int[]{9, 2, 1, 0, 5, 3, 6, 4, 8, 7};
        System.out.println("排序前:" + Arrays.toString(arr));
        plusSort(arr);
        System.out.println("排序後:" + Arrays.toString(arr));
    }
    
    public static void plusSort(int[] arr){
        if(arr != null && arr.length > 1){
            for(int i = 0; i < arr.length - 1; i++){
                // 初始化一個布林值
                boolean flag = true;
                for(int j = 0; j < arr.length - i - 1 ; j++){
                    if(arr[j] > arr[j+1]){
                        // 調換
                        int temp;
                        temp = arr[j];
                        arr[j] = arr[j+1];
                        arr[j+1] = temp;
                        // 改變flag
                        flag = false;
                    }
                }
                if(flag){
                    break;
                }
            }
        }
    }
}

執行結果:

排序前:[9, 2, 1, 0, 5, 3, 6, 4, 8, 7]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

2.選擇排序

  選擇排序也是一種簡單直觀的排序演算法,實現原理比較直觀易懂:首先在未排序數列中找到最小元素,然後將其與數列的首部元素進行交換,然後,在剩餘未排序元素中繼續找出最小元素,將其與已排序數列的末尾位置元素交換。以此類推,直至所有元素圴排序完畢

/**
 * 選擇排序:
 *  每一次從待排序的資料元素中選出最小(或最大)的一個元素,
 *  存放在序列的起始位置,直到全部待排序的資料元素排完。
 */
import java.util.Arrays;
public class SelectSort {
    public static void main(String[] args) {
        int[] arr = new int[] {3,4,5,7,1,2,0,9,3,6,8};
        System.out.println("排序前:"+Arrays.toString(arr));
        selectSort(arr);
        System.out.println("排序後:"+Arrays.toString(arr));
    }
    public static void selectSort(int[] arr) {
        for(int i=0;i<arr.length;i++) {
            int minIndex=i;
            for(int j=i+1;j<arr.length;j++) {
                if(arr[minIndex]>arr[j]) {
                    minIndex=j;
                }
            }
            if(i!=minIndex) {
                int temp=arr[i];
                arr[i]=arr[minIndex];
                arr[minIndex]=temp;
            }
        }
    }
}

執行結果:

排序前:[3, 4, 5, 7, 1, 2, 0, 9, 3, 6, 8]
排序後:[0, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9]

3.插入排序

  一次插入排序的操作過程:將待插元素,依次與已排序好的子數列元素從後到前進行比較,如果當前元素值比待插元素值大,則將移位到與其相鄰的後一個位置,否則直接將待插元素插入當前元素相鄰的後一位置,因為說明已經找到插入點的最終位置

/**
 * 插入排序:
 *  從第一個元素開始,該元素可以認為已經被排序
 *  取出下一個元素,在已經排序的元素序列中從後向前掃描
 *  如果該元素(已排序)大於新元素,將該元素移到下一位置
 *  重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
 *  將新元素插入到該位置後
 *  重複上面步驟
 */
import java.util.Arrays;
public class InsertSort {
    public static void main(String[] args) {
        int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
        System.out.println("排序前:"+Arrays.toString(arr));
        insertSort(arr);
        System.out.println("排序後:"+Arrays.toString(arr));
    }
    public static void insertSort(int[] arr) {
        for(int i=1;i<arr.length;i++) {
            if(arr[i]<arr[i-1]) {
                int temp=arr[i];
                int j;
                for(j=i-1;j>=0&&temp<arr[j];j--)
                    arr[j+1]=arr[j];
                arr[j+1]=temp;
            }
        }
    }
}

執行結果:

排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

4.快速排序

  快速排序演算法利用的是一趟快速排序,基本內容是選擇一個數作為準基數,然後利用這個準基數將遺傳資料分為兩個部分,第一部分比這個準基數小,都放在準基數的左邊,第二部分都比這個準基數大,放在準基數的右邊.

import java.util.Arrays;
/**
 * 快速排序:
 *  快速排序演算法利用的是一趟快速排序,基本內容是選擇一個數作為準基數,
 *  然後利用這個準基數將遺傳資料分為兩個部分,第一部分比這個準基數小,
 *  都放在準基數的左邊,第二部分都比這個準基數大,放在準基數的右邊.
 */
public class QuickSort {
    public static void main(String[] args) {
        int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
        System.out.println("排序前:"+ Arrays.toString(arr));
        quickSort(arr,0,arr.length-1);
        System.out.println("排序後:"+Arrays.toString(arr));
    }

    public static void quickSort(int[] arr,int begin,int end) {
        //先定義兩個引數接收排序起始值和結束值
        int a = begin;
        int b = end;
        //先判斷a是否大於b

        if (a >= b) {
            //沒必要排序
            return;
        }
        //基準數,預設設定為第一個值
        int x = arr[a];
        //迴圈
        while (a < b) {
            //從後往前找,找到一個比基準數x小的值,賦給arr[a]
            //如果a和b的邏輯正確--a<b ,並且最後一個值arr[b]>x,就一直往下找,直到找到後面的值大於x
            while (a < b && arr[b] >= x) {
                b--;
            }
            //跳出迴圈,兩種情況,一是a和b的邏輯不對了,a>=b,這時候排序結束.二是在後面找到了比x小的值
            if (a < b) {
                //將這時候找到的arr[b]放到最前面arr[a]
                arr[a] = arr[b];
                //排序的起始位置後移一位
                a++;
            }
            //從前往後找,找到一個比基準數x大的值,放在最後面arr[b]
            while (a < b && arr[a] <= x) {
                a++;
            }
            if (a < b) {
                arr[b] = arr[a];
                //排序的終止位置前移一位
                b--;
            }
        }
        //跳出迴圈 a < b的邏輯不成立了,a==b重合了,此時將x賦值回去arr[a]
        arr[a] = x;
        //呼叫遞迴函式,再細分再排序
        quickSort(arr,begin,a-1);
        quickSort(arr,a+1,end);
    }
}

執行結果:

排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

5.歸併排序

  歸併排序,簡單的說把一串數,從中平等分為兩份,再把兩份再細分,直到不能細分為止,這就是分而治之的分的步驟. 再從最小的單元,兩兩合併,合併的規則是將其按從小到大的順序放到一個臨時陣列中,再把這個臨時陣列替換原陣列相應位置

import java.util.Arrays;

/**
 * 歸併排序:
 * 歸併操作的工作原理如下:
 * 第一步:申請空間,使其大小為兩個已經 排序序列之和,該空間用來存放合併後的序列
 * 第二步:設定兩個 指標,最初位置分別為兩個已經排序序列的起始位置
 * 第三步:比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置重複步驟3直到某一指標超出序列尾
 * 將另一序列剩下的所有元素直接複製到合併序列尾
 *
 */
public class MergeSort {


    public static void main(String[] args) {
        int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
        System.out.println("排序前:"+ Arrays.toString(arr));
        mergeSort(arr, 0, arr.length-1);
        System.out.println("排序後:"+ Arrays.toString(arr));

    }

    public static void mergeSort(int[] a,int s,int e){
        int m = (s + e) / 2;
        if (s < e){
            mergeSort(a,s,m);
            mergeSort(a,m+1,e);
            //歸併
            merge(a,s,m,e);
        }
    }

    private static void merge(int[] a, int s, int m, int e) {
        //初始化一個從起始s到終止e的一個數組
        int[] temp = new int[(e - s) + 1];
        //左起始指標
        int l = s;
        //右起始指標
        int r = m+1;
        int i = 0;
        //將s-e這段資料在邏輯上一分為二,l-m為一個左邊的陣列,r-e為一個右邊的陣列,兩邊都是有序的
        //從兩邊的第一個指標開始遍歷,將其中小的那個值放在temp陣列中
        while (l <= m && r <= e){
            if (a[l] < a[r]){
                temp[i++] = a[l++];
            }else{
                temp[i++] = a[r++];
            }
        }

        //將兩個陣列剩餘的數放到temp中
        while (l <= m){
            temp[i++] = a[l++];
        }
        while (r <= e){
            temp[i++] = a[r++];
        }

        //將temp陣列覆蓋原陣列
        for (int n = 0; n < temp.length; n++) {
            a[s+n] = temp[n];
        }

    }
}

執行結果:

排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

6.希爾排序

  **希爾排序(Shell’s Sort)是插入排序的一種又稱“縮小增量排序”。希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序演算法排序;隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止 . 希爾排序實質上是一種分組插入的方法 . **

/**
 * 希爾排序:
 * 希爾排序(Shell’s Sort)是插入排序的一種又稱“縮小增量排序”。
 * 希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序演算法排序;
 * 隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止
 */
import java.util.Arrays;
public class ShellSort {
    public static void main(String[] args) {
        int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
        System.out.println("排序前:"+Arrays.toString(arr));
        shellSort(arr);
        System.out.println("排序後:"+Arrays.toString(arr));
    }
    public static void shellSort(int[] arr) {
        int k = 1;
        for (int d = arr.length / 2; d > 0; d /= 2) {
            for (int i = d; i < arr.length; i++) {
                for (int j = i - d; j >= 0; j -= d) {
                    if (arr[j] > arr[j + d]) {
                        int temp = arr[j];
                        arr[j] = arr[j + d];
                        arr[j + d] = temp;
                    }
                }
            }
            System.out.println( Arrays.toString(arr));
            k++;
        }
    }
}

執行結果:

排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
[5, 1, 0, 7, 4, 9, 3, 2, 8, 6]
[0, 1, 3, 2, 4, 6, 5, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

7.基數排序

  基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先順序排序。最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。

/**
 * 基數排序:
 * 取得陣列中的最大數,並取得位數;
 * arr為原始陣列,從最低位開始取每個位組成radix陣列;
 * 對radix進行計數排序(利用計數排序適用於小範圍數的特點);
 */

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class RadixSort {
    public static void main(String[] args) {
        int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
        System.out.println("排序前:"+ Arrays.toString(arr));
        radixSort(arr);
        System.out.println("排序後:"+Arrays.toString(arr));
    }
    /**
     * 基數排序
     */
    public static void radixSort(int[] a) {
        int max = a[0];
        for (int i = 1; i < a.length; i++) {
            if (a[i] > max) {
                max = a[i];
            }
        }
        int time = 0;
        while (max > 0) {
            max /= 10;
            time++;
        }
        List<ArrayList<Integer>> queue = new ArrayList<ArrayList<Integer>>();
        for (int i = 0; i < 10; i++) {
            ArrayList<Integer> queue1 = new ArrayList<Integer>();
            queue.add(queue1);
        }
        for (int i = 0; i < time; i++) {
            for (int j = 0; j < a.length; j++) {
                int x = a[j] % (int) Math.pow(10, i+1)/(int)Math.pow(10, i);
                ArrayList<Integer> queue2 = queue.get(x);
                queue2.add(a[j]);
                queue.set(x, queue2);
            }
            int count = 0;
            for (int k = 0; k < 10; k++) {
                while (queue.get(k).size()>0) {
                    ArrayList<Integer> queue3 = queue.get(k);
                    a[count] = queue3.get(0);
                    queue3.remove(0);
                    count++;
                }
            }
        }
    }
}

執行結果:

排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

8.堆排序

  堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點

/**
 * 堆排序
 * 將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆為初始的無序區;
 * 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
 * 由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調整為新堆,
 * 然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。
 * 不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成。
 */

import java.util.Arrays;

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
        System.out.println("排序前:"+ Arrays.toString(arr));
        heapSort(arr);
        System.out.println("排序後:"+Arrays.toString(arr));
    }

    /**
     * 堆排序
     */
    public static void heapSort(int[] a) {
        int len = a.length;
        for (int i = 0; i < len - 1; i++) {
            buildMaxHeap(a,len - 1 - i);
            swap(a,0,len - 1 - i);
        }
    }

    private static void buildMaxHeap(int[] data, int lastIndex) {
        //從lastIndex處節點(最後一個節點)的父節點開始
        for (int i = (lastIndex - 1)/2; i >= 0; i--) {
            //k儲存正在判斷的節點
            int k = i ;
            //如果當前K節點的子節點存在
            while(k * 2 + 1 <= lastIndex) {
                //k節點的左子節點的索引
                int biggerIndex = 2 * k +1;
                //如果biggerIndex小於lastIndex,即biggerIndex +1代表的K節點的右子節點存在
                if(biggerIndex < lastIndex) {
                    //如果右子節點的值較大
                    if(data[biggerIndex] < data[biggerIndex + 1]) {
                        biggerIndex++;
                    }
                }
                //如果K節點的值小於其較大的子節點的值
                if(data[k] < data[biggerIndex]) {
                    //交換他們
                    swap(data, k, biggerIndex);
                    k = biggerIndex;
                } else {
                    break;
                }
            }
        }
    }

    private static void swap(int[] a, int i, int j) {
        int tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
    }
}

執行結果

排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

各種演算法的比較

排序法 平均時間 最小時間 最大時間 穩定度 額外空間 備註
氣泡排序 O(n2) O(n) O(n2) 穩定 O(1) n小時較好
選擇排序 O(n2) O(n2) O(n2) 不穩定 O(1) n小時較好
插入排序 O(n2) O(n) O(n2) 穩定 O(1) 大部分已排序時較好
基數排序 O(logRB) O(n) O(logRB) 穩定 O(n) B是真數(0-9),R是基數(個十百)
Shell排序 O(nlogn) - O(ns) 1<s<2 不穩定 O(1) s是所選分組
快速排序 O(nlogn) O(n2) O(n2) 不穩定 O(logn) n大時較好
歸併排序 O(nlogn) O(nlogn) O(nlogn) 穩定 O(n) 要求穩定性時較好
堆排序 O(nlogn) O(nlogn) O(nlogn) 不穩定 O(1) n大時較好

速度: 快速排序>>歸併排序>>>>>插入排序>>選擇排序>>氣泡排序
並且可以看到,選擇排序,氣泡排序在資料量越來越大的情況下,耗時已經呈指數型上漲,而不是倍數上漲

(1)若n較小(如n≤50),可採用直接插入或直接選擇排序。
 當記錄規模較小時,直接插入排序較好;否則因為直接選擇移動的記錄數少於直接插人,應選直接選擇排序為宜。
(2)若檔案初始狀態基本有序(指正序),則應選用直接插人、冒泡或隨機的快速排序為宜;
(3)若n較大,則應採用時間複雜度為O(nlgn)的排序方法:快速排序、堆排序或歸併排序。
 快速排序是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;
 堆排序所需的輔助空間少於快速排序,並且不會出現快速排序可能出現的最壞情況。這兩種排序都是不穩定的。
 若要求排序穩定,則可選用歸併排序。