常見的排序算法詳解

前言

做爲程序員,時時刻刻都要與算法打交道,其中排序算法算是比較常見的一種。而在面試程序員崗位中, 不出意外,排序算法也是比較常見的考量之一。所以咱們有必要了解和掌握各類常見排序算法。
這個篇文章記錄了幾種常見的排序算法,並各類排序算法極端狀況的優劣,供學習和參考。java

介紹

對數據進行排序意味着以特定順序排列數據,一般是在相似數組的數據結構中。您可使用各類排序標準,常見的排序標準是從最小到最大排序數字,反之亦然,或按字典順序排序字符串。程序員

常見的幾種排序算法面試

  • 冒泡排序
  • 選擇排序
  • 插入排序
  • 歸併排序
  • 快速排序
  • 堆排序

算法介紹

1、冒泡排序

冒泡排序經過交換相鄰元素(若是它們不是所需的順序)來工做。此過程從數組的開頭重複,直到全部元素都按順序排列。算法

舉例 對4 2 1 5 3進行排序:api

  1. 4 2 1 5 3 : 前兩個元素的順序錯誤,因此咱們交換它們。
  2. 2 4 1 5 3 : 後兩個元素也是錯誤的順序,因此咱們交換。
  3. 2 1 4 5 3 : 這兩個是正確的順序,4 <5,因此咱們無論它們。
  4. 2 1 4 5 3 : 另外一次交換。

2 1 4 3 5 :這是一次迭代後獲得的數組。 由於在第一次傳遞期間至少發生了一次交換(實際上有三次),咱們須要再次遍歷整個數組並重復相同的過程。 經過重複這個過程,直到再也不進行交換,咱們將有一個排序的數組。數組

代碼實現數據結構

public void bubbleSort(int[] array) {  
    boolean sorted = false;
    int temp;
    while(!sorted) {
        sorted = true;
        for (int i = 0; i < array.length - 1; i++) {
            if (a[i] > a[i+1]) {
                temp = a[i];
                a[i] = a[i+1];
                a[i+1] = temp;
                sorted = false;
            }
        }
    }
}
複製代碼

2、選擇排序

選擇排序是每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,而後,再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。以此類推,直到所有待排序的數據元素排完性能

舉例 對4 2 1 5 3進行排序:學習

  1. 4 2 1 5 3 : 未排序狀態
  2. 1 4 2 5 3 : 選擇最小值1, 排入第一位
  3. 1 2 4 5 3 : 從第二位開始, 選擇最小值2, 排入第二位
  4. 1 2 3 4 5 : 從第三位開始,選擇最小值3, 排入第三位
  5. 1 2 3 4 5 : 從第四位開始, 選擇最小值4, 排入第四位
  6. 1 2 3 4 5 : 從第五位開始,選擇最小值5, 排入第五位

至此排序結束。測試

代碼實現

public void selectionSort(int[] array) {  
    for (int i = 0; i < array.length; i++) {
        int min = array[i];
        int minId = i;
        for (int j = i+1; j < array.length; j++) {
            if (array[j] < min) {
                min = array[j];
                minId = j;
            }
        }
        // 交換
        int temp = array[i];
        array[i] = min;
        array[minId] = temp;
    }
}
複製代碼

3、插入排序

插入排序是將數組劃分爲已排序和未排序的子數組。 排序部分的開頭長度爲1,對應於數組中的第一個(最左側)元素。咱們遍歷數組,在每次迭代中,咱們將數組的排序部分擴展一個元素。 在擴展時,咱們將新元素放置在已排序子陣列中的適當位置。咱們經過將全部元素向右移動直到遇到咱們沒必要移動的第一個元素來實現這一點。

舉例 對3 5 7 8 4 2 1 9 6進行排序:

  1. 3 5 7 8 4 2 1 9 6 : 咱們採起4並記住這是咱們須要插入的。從8> 4開始,咱們轉移。
  2. 3 5 7 x 8 2 1 9 6 : x的值不是相當重要的,由於它將被當即覆蓋(若是它是適當的位置則爲4或若是咱們移位則爲7)。從7> 4開始,咱們轉移。
  3. 3 5 x 7 8 2 1 9 6
  4. 3 x 5 7 8 2 1 9 6
  5. 3 4 5 7 8 2 1 9 6

在這個過程以後,排序的部分被一個元素擴展,咱們如今有五個而不是四個元素。每次迭代都會這樣作,最後咱們將對整個數組進行排序。

代碼實現

public void insertionSort(int[] array) {  
    for (int i = 1; i < array.length; i++) {
        int current = array[i];
        int j = i - 1;
        while(j >= 0 && current < array[j]) {
            array[j+1] = array[j];
            j--;
        }
       
        array[j+1] = current;
    }
}
複製代碼

4、歸併算法

合併排序/歸併算法使用遞歸來解決比先前提出的算法更有效的排序問題,特別是它使用分而治之的方法。 使用這兩個概念,咱們將整個數組分解爲兩個子數組,而後: 一、對數組的左半部分進行排序(遞歸) 二、對數組的右半部分進行排序(遞歸) 三、合併解決方案

舉例 對3 5 4 2 1 進行排序:

此樹表示遞歸調用的工做方式。標有向下箭頭的數組是咱們須要排序的數組,而向上箭頭的數組是咱們合併完後的數據。因此先按下遞歸向下箭頭數組到樹的底部,而後向上返回併合並。

在咱們的例子中,咱們有數組3 5 3 2 1,因此咱們把它分紅3 5 4和2 1。爲了對它們進行排序,咱們進一步將它們分紅它們的組 一旦咱們到達底部,咱們就開始合併並按照咱們的方式對它們進行排序

代碼實現

public void mergeSort(int[] array, int left, int right) {  
    if (right <= left) return;
    int mid = (left+right)/2;
    mergeSort(array, left, mid);
    mergeSort(array, mid+1, right);
    merge(array, left, mid, right);
}

private void merge(int[] array, int left, int mid, int right) {
    // 計算長度
    int lengthLeft = mid - left + 1;
    int lengthRight = right - mid;

    // 建立臨時數組
    int leftArray[] = new int [lengthLeft];
    int rightArray[] = new int [lengthRight];

    // 將須要排序的數組分別傳入左右兩個臨時數組裏
    for (int i = 0; i < lengthLeft; i++)
        leftArray[i] = array[left+i];
    for (int i = 0; i < lengthRight; i++)
        rightArray[i] = array[mid+i+1];

    // 左右臨時數組當前索引
    int leftIndex = 0;
    int rightIndex = 0;

    // 將左右臨時數組從新按從小到大順序寫入待排序數組中
    for (int i = left; i < right + 1; i++) {
        // 若是R和L中仍然有未複製的元素,則複製這兩個元素的最小值
        if (leftIndex < lengthLeft && rightIndex < lengthRight) {
            if (leftArray[leftIndex] < rightArray[rightIndex]) {
                array[i] = leftArray[leftIndex];
                leftIndex++;
            }
            else {
                array[i] = rightArray[rightIndex];
                rightIndex++;
            }
        }
        // 若是全部元素都已從rightArray複製,則複製left Array的其他部分
        else if (leftIndex < lengthLeft) {
            array[i] = leftArray[leftIndex];
            leftIndex++;
        }
        // 若是全部元素都已從leftArray複製,則複製right Array的其他部分
        else if (rightIndex < lengthRight) {
            array[i] = rightArray[rightIndex];
            rightIndex++;
        }
    }
}
複製代碼

5、快速排序

經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比另一部分的全部數據都要小,而後再按此方法對這兩部分數據分別進行快速排序,整個排序過程能夠遞歸進行,以此達到整個數據變成有序序列。

舉例 對6 1 2 7 9 3 4 5 10 8 進行排序: 首先取基準數, 每一輪的排序要義是將數組以基準數爲準,將數組分爲左右兩個部分, 左邊部分都比基準數小, 右邊部分都比基準數大。

  1. 6 1 2 7 9 3 4 5 10 8 : 取6爲基準數
  2. 6 1 2 7 9 3 4 5 10 8 : 先從左向右搜索,獲取比6大的數7,記錄下來。
  3. 6 1 2 7 9 3 4 5 10 8 : 接着從右向左搜索, 獲取比6小的數 5, 記錄下來。
  4. 6 1 2 5 9 3 4 7 10 8 : 將第二步和第三步記錄下來的兩個數進行交換。
  5. 6 1 2 5 9 3 4 7 10 8 : 以第二步搜索中止位置接着向右搜索, 獲取比6大的數9, 記錄下來。
  6. 6 1 2 5 9 3 4 7 10 8 : 以第三步搜索中止位置接着向左搜索, 獲取比6小的數4, 記錄下來。
  7. 6 1 2 5 4 3 9 7 10 8 : 將第五步和第六步記錄下來的兩個數進行交換。
  8. *** 3 1 2 5 4 6 9 7 10 8*** : 重複第六步, 發現無可搜索數據, 將最後一個數3 與基準數互換。

至此, 第一輪搜索結束, 基準數6將數組分割成兩部分, 接下來,將這兩部分按照1-8步驟, 分別進行排序, 直至沒法切分,則排序完成。

摘用網上圖片:

代碼實現

public void quickSort(int[] array, int begin, int end) {  
    if (end <= begin) return;
    int pivot = partition(array, begin, end);
    quickSort(array, begin, pivot-1);
    quickSort(array, pivot+1, end);
}

private int partition(int[] array, int begin, int end) {  
    int baseValue = array[begin];
    int leftIndex = begin + 1;
    int rightIndex = end;
    int temp;
    
    while(leftIndex != rightIndex) {
        //先從左邊向右搜索
        while(array[leftIndex] <= baseValue && leftIndex < rightIndex) {
            leftIndex ++;
        }
        //再從右邊向左搜索
        while(rightIndex] >= baseValue && leftIndex <    rightIndex) {
            rightIndex --;
        }
        
        //交換兩個數的位置
        if (leftIndex < rightIndex) {
            temp = array[leftIndex];
            array[leftIndex] = array[rightIndex];
            array[rightIndex] = temp;
        }
    }
    
    //最終將基準數歸位 
    array[begin] = array[leftIndex];
    array[leftIndex] = baseValue;

    //返回基準數位置
   return leftIndex;
}
複製代碼

6、堆排序

堆排序是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似徹底二叉樹的結構,並同時知足堆積的性質:即子結點的鍵值或索引老是小於(或者大於)它的父節點

堆分兩種:

  1. 大頂堆

每一個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆 數組書寫方式: 8 7 6 5 4 3 其中: arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

  1. 小頂堆

每一個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆 數組書寫方式: 3 4 5 6 7 8 其中: arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

堆排序的基本思路:

  1. 將無需序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
  2. 將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
  3. 從新調整結構,使其知足堆定義,而後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。

代碼實現

public void heapSort(int[] array) {  
    if (array.length == 0) return;

    // 構建大頂堆
    int length = array.length;
    for (int i = length / 2-1; i >= 0; i--) {
        heapify(array, length, i);
    }
       

    for (int i = length-1; i >= 0; i--) {
        int temp = array[0];
        array[0] = array[i];
        array[i] = temp;
        //從新構建大頂堆
        heapify(array, i, 0);
    }
}

private void heapify(int[] array, int length, int i) {  
    int leftChild = 2*i+1;
    int rightChild = 2*i+2;
    int largest = i;

    // 若是左節點大於父節點
    if (leftChild < length && array[leftChild] > array[largest]) {
        largest = leftChild;
    }

    // 若是右節點大於父節點
    if (rightChild < length && array[rightChild] > array[largest]) {
        largest = rightChild;
    }

    // 若是須要則交換
    if (largest != i) {
        int temp = array[i];
        array[i] = array[largest];
        array[largest] = temp;
        heapify(array, length, largest);
    }
}


複製代碼

算法比較

名稱 最好狀況 平均狀況 最壞狀況 額外空間 穩定性
冒泡排序 n n^2 n^2 1 穩定
選擇排序 n^2 n^2 n^2 1 不穩定
插入排序 n n^2 n^2 1 穩定
歸併排序 nlog n nlog n nlog n n 穩定
快速排序 nlog n nlog n n nlog n 不穩定
插入排序 n nlog n nlog n 1 不穩定

在10,000個整數的隨機數組副本上,以上訴幾種算法進行測試,結果以下:

時間(NS) 冒泡排序 插入排序 選擇排序 歸併 堆排序 快速排序
第一次運行 266089476 21973989 66603076 5511069 5283411 4156005
第二輪 323692591 29138068 80963267 8075023 6420768 7060203
第三次運行 303853052 21380896 91810620 7765258 8009711 7622817
第四次運行 410171593 30995411 96545412 6560722 5837317 2358377
第五次運行 315602328 26119110 95742699 5471260 14629836 3331834
第六次運行 286841514 26789954 90266152 9898465 4671969 4401080
第七次運行 384841823 18979289 72569462 5135060 10348805 4982666
八跑 393849249 34476528 107951645 8436103 10142295 13678772
第九跑 306140830 57831705 138244799 5154343 5654133 4663260
第十次運行 306686339 34594400 89442602 5601573 4675390 3148027

僅以此樣本爲例, 冒泡排序在性能方面是最差的, 堆排和快排性能最佳

總結

對數據集進行排序是一種很是常見的操做,不管是進一步分析它們,仍是使用依賴於排序數據的更有效算法來加速搜索,過濾數據等。


歡迎長按下圖關注公衆號: 終身幼稚園

相關文章
相關標籤/搜索