八大排序算法的原理圖解及代碼實現

前言

程序 = 數據結構 + 算法

設計優良程序的兩個要點:選用正確的存儲結構、採用合理的解決方案。

算法就是“解決方案”。而排序,是一種非常常見的業務場景,如:“前100名下單的打八折”、“成績前三的獲一等獎”、“績效最差的扣工資”等等。但排序的實現有很多不同的解決方案,不同的解決方案需要的運算時間和存儲空間不同。

這裏說的八大排序算法,都是內部排序(不需要額外的空間存儲數據,所有運算時存儲的數據都是放在內存中)。他們分別是:冒泡排序、堆排序、直接插入排序、歸併排序、快速排序、基數排序、簡單選擇排序、希爾排序。

接下來,我們來學習並歸納這八種排序的思想以及Java實現。

具體的Java實現以及測試代碼詳見於我的Github

由於這些排序裏面有一些公用的方法,預先新建工具類如下:

/**
 * 整數數組相關的工具類
 * @author zhenye 2019/1/24
 */
public class IntArrayUtil {

  /**
    * 初始化一個正整數數組
    * @param length 數組長度
    * @param maxNum 數組中元素的最大值
    * @return 正整數數組
    */
  public static int[] initIntArrays (int length, int maxNum) {
    if (length <= 0 || maxNum <= 0) {
      throw new RuntimeException("參數非法");
    }
    int[] nums = new int[length];
    Random random = new Random();
    for(int i = 0; i < length; i++) {
      nums[i] = random.nextInt(maxNum);
    }
    return nums;
  }

  /** 交換數組中下標i與下標j的值
    * @param intArrays 目標數組
    * @param i 數組下標i
    * @param j 數組下標j
    */
  public static void exchangeValue(int[] intArrays, int i, int j) {
    // 如果i==j,通過這種方法交換,會將當前位置的值重置爲0
    if(i == j) {
      return;
    }
    intArrays[i] = intArrays[i] ^ intArrays[j];
    intArrays[j] = intArrays[i] ^ intArrays[j];
    intArrays[i] = intArrays[i] ^ intArrays[j];
  }
}

冒泡排序

基本思想:從左往右比較相鄰的兩個元素,如果左邊元素比右邊元素大,交換左右元素。即每輪比較完後,最大值都能到最右邊。當所有的輪次比較完後,就是從小到大的順序。

圖解冒泡排序如下圖:
圖解冒泡排序

冒泡排序的java實現如下:

/**
 * 冒泡排序
 * @author zhenye 2019/1/24
 */
public class BubbleSort {

  public static void sort(int[] nums) {
    // 比較的輪次數
    for(int i = 0;i < nums.length;i++) {
      // 當前輪次需要比較的次數
      for(int j = 0; j < nums.length - i - 1;j++) {
        if(nums[j] > nums[j+1]) {
          IntArrayUtil.exchangeValue(nums, j, j+1);
        }
      }
    }
  }
}

冒泡排序的執行結果如下圖所示:
冒泡排序的執行結果圖

堆排序

基本思想:堆是一顆順序存儲的完全二叉樹。每個節點的值都不大於其子節點的值,這樣的稱之爲“小根堆”,反之則稱之爲“大根堆”。因此“大根堆”的堆頂元素就是這個堆的最大值。利用將無序序列初始化爲“大根堆”,取出其堆頂元素作爲最大值,然後將剩餘的元素看做無序序列,並再次變爲“大根堆”並取出其堆頂元素。重複迭代直到無序序列只有一個元素即排序完畢。

圖解堆排序如下圖:

  1. 如果將一個無序序列,初始化爲一個“大根堆”,如下圖:
    大根堆初始化

  2. 將堆頂元素與最後一個元素交換,將剩餘的n-1個元素的無序序列再次初始化爲
    “大根堆”,並重復。
    大根堆排序

堆排序的java實現如下:

/**
 * 堆排序
 * @author zhenye 2019/1/24
 */
public class HeapSort {

  public static void sort(int[] nums) {
    // 預先初始化爲大根堆
    initBigHeap(nums);
    for(int i = nums.length; i >= 1; i--) {
      // 堆頂元素爲最大值,交換到其應該在的位置
      IntArrayUtil.exchangeValue(nums, 0, i - 1);
      // 重新調整,保證數組的前i-1個元素符合大根堆
      adjustBigHeap(nums, 0, i - 1);
    }
  }

  /**
    * 將數組初始化爲大根堆
    * @param nums 待排數組
    */
  private static void initBigHeap(int[] nums) {
    // 從最後一個有子節點的節點往前遍歷,遍歷過程中保證當前節點的左右子節點的值均不大於該節點的值
    // 則遍歷到根節點(堆頂元素)時也保證其左右子節點的值均不大於該節點的值,則初始化成功。
    // 即從第i個元素的父節點(第i個元素的下標爲i - 1,其父節點的下標爲i/2 - 1)開始往前遍歷:
    for(int i = nums.length / 2 - 1;i >= 0; i--) {
      adjustBigHeap(nums,i,nums.length);
    }
  }

  /**
    * 保證數組nums的前count個元素中,下標爲parent的節點及其子孫節點符合大根堆要求
    * @param nums 待排數組
    * @param parentIndex 目標節點
    * @param count 待排元素個數(nums數組的前count個元素)
    */
  private static void adjustBigHeap(int[] nums, int parentIndex, int count) {
    int leftChildIndex = 2 * parentIndex + 1;
    int rightChildIndex = 2 * parentIndex + 2;
    while(leftChildIndex < count) {
      int toBeComparedIndex;
      // 如果j節點有右子節點,且右子節點比左子節點的值大,則j節點跟右子節點比較,否則跟左子節點比較
      if (rightChildIndex < count && nums[rightChildIndex] > nums[leftChildIndex]) {
        toBeComparedIndex = rightChildIndex;
      } else {
        toBeComparedIndex = leftChildIndex;
      }
      if (nums[toBeComparedIndex] > nums[parentIndex]) {
        IntArrayUtil.exchangeValue(nums, parentIndex, toBeComparedIndex);
        // 交換後,然後還要保證交換後的節點也符合大根堆要求
        adjustBigHeap(nums, toBeComparedIndex, count);
      } else {
        // 當父節點是最大時,停止遞歸
        break;
      }
    }
  }
}

堆排序的執行結果如下圖所示:
堆排序的執行結果圖

直接插入排序

基本思想:將n個待排序的元素分成一個有序表和一個無序表。開始時有序表只包含第一個元素,無序表包含後面的n-1個元素。排序過程中每次從無序表中取出第一個元素,並直接插入有序表中的合適位置。重複n-1次即排序完畢。

圖解直接插入排序如下圖:
圖解直接插入排序

直接插入排序的java實現如下:

/**
 * 直接插入排序
 * @author zhenye 2019/1/24
 */
public class InsertSort {

  public static void sort(int[] nums) {
    // i爲無序表中的元素下標,temp是待插入元素。
    int i,temp;
    for(i = 1;i < nums.length;i++) {
      temp = nums[i];
      // k記錄的是待插入元素應該在數組中位置的下標
      int k;
      // 比待插入元素大的元素右移,否則就是找到待插入元素的正確位置k
      for(k = i;k > 0 && nums[k - 1] > temp;k--) {
        nums[k] = nums[k - 1];
      }
      nums[k] = temp;
    }
  }
}

直接插入排序的執行結果如下圖所示:
![直接插入排序的執行結果圖

歸併排序

基本思想:採用分治策略將待排序列分成若干個子序列,先將每個子序列進行排序後,再把子序列進行合併即得到排好序的序列。

圖解歸併排序如下圖:
圖解歸併排序

具體歸併操作的實現細節圖解如下圖:
歸併操作實現細節圖

歸併排序的java實現:

/**
 * 歸併排序
 * @author zhenye 2019/1/24
 */
public class MergeSort {

  /**
    * 提供給開發者使用的歸併排序
    * @param nums 待排序數組
    */
  public static void sort(int[] nums) {
    mergeSort(nums, 0, nums.length - 1, new int[nums.length]);
  }

  /**
    * 歸併算法的具體實現
    * @param nums 待排序列
    * @param start 數組起始下標
    * @param end 數組截止下標
    * @param temp 臨時數組
    */
  private static void mergeSort(int[] nums, int start, int end, int[] temp) {
    // 當[start ... end]數組可以拆分(不只一個元素)時,需要再次拆分
    if(start < end) {
      // 拆分成[start ... mid],[mid + 1 ... end]兩個更小的數組)
      int mid = (start + end) / 2;
      mergeSort(nums, start, mid, temp);
      mergeSort(nums, mid + 1, end, temp);
      // 將兩個小的有序數組,合併爲一個大的有序數組
      merge(nums, start, mid, end, temp);
    }
  }

  /**
    * 將兩個小的有序數組,合併爲一個大的有序數組
    */
  private static void merge(int[] nums, int start, int mid, int end, int[] temp) {
    // 定義兩個旗幟i,j(旗幟i在數組[start ... mid]移動,旗幟j在數組[mid + 1 ... end]移動)
    int i = start;
    int j = mid + 1;
    int k = 0;
    while (i <= mid && j <= end) {
      if(nums[i] > nums[j]) {
        temp[k++] = nums[j++];
      } else {
        temp[k++] = nums[i++];
      }
    }
    while (i <= mid) {
      temp[k++] = nums[i++];
    }
    while (j <= end) {
      temp[k++] = nums[j++];
    }
    // 將臨時數組copy到原數組的相應位置
    for(k = 0;start <= end; start++, k++) {
      nums[start] = temp[k];
    }
  }
}

歸併排序的執行結果如下圖所示:
歸併排序的執行結果圖

快速排序

基本思想:首先在待排序列中隨機選取一個基準數(代碼實現時默認選取的是第一個元素),然後把待排序列中比這個基準數小的都放在其左邊,把待排序列中比這個基準數大的都放在其右邊(即找出並將該基準數放到待排序列的正確位置)。然後把該基準數的左右兩邊看做兩個待排序列重複上述操作,直至所有的基準數都找到並放到了正確的位置,則排序完畢。

圖解快排排序如下圖:
圖解快排排序

快速排序的java實現如下:

/**
 * 快速排序
 * @author zhenye 2019/1/24
 */
public class QuickSort {

  /**
    * 提供給開發者使用的快速排序
    * @param nums 待排序數組
    */
  public static void sort(int[] nums) {
    quickSort(nums, 0 , nums.length - 1);
  }

  /**
    * 對下標從start到end的正整數數組intArrays進行快速排序
    * @param intArrays 將要排序的數組intArrays
    * @param start 數組起始下標位置
    * @param end 數組截止下標位置
    */
  private static void quickSort(int[] intArrays, int start, int end) {
    if(start > end) {
      return;
    }
    // 將數組起始下標位置的元素,作爲基準數
    int base = intArrays[start];
    // 將i看作從左往右搜索的哨兵,j看作從右往左搜索的哨兵
    int left = start;
    int right = end;
    while(left != right) {
      // !!!因爲選取的基準數是左哨兵的起始位置,這裏一定要右邊的哨兵先走。
      /*
        這裏循環結束的條件有一個是左右哨兵相遇。
        如果左哨兵先行,出現由於左哨兵找到了右哨兵(相遇)導致循環結束的情況時,
        左右哨兵的當前元素是基於右哨兵上次循環結束的位置。
        即此時左右哨兵相遇位置的當前元素是比基準數大的!!!將該元素與基準數交換是不對的。
        */
      // 找到右邊比基準數小的元素下標(或哨兵相遇)
      while(left < right && intArrays[right] >= base) {
        right--;
      }
      // 找到左邊比基準數大的元素下標(或哨兵相遇)
      while (left < right && intArrays[left] <= base) {
        left++;
      }
      if (left < right) {
        // 交換,保證哨兵i左邊的元素都比基準數小,哨兵j右邊的元素都比基準數大
        IntArrayUtil.exchangeValue(intArrays, left, right);
      }
    }
    // 交換,保證基準數在正確的位置
    IntArrayUtil.exchangeValue(intArrays, start, right);
    // 下一次,迭代(兩個子序列分別進行快速排序)
    quickSort(intArrays, start, right - 1);
    quickSort(intArrays, right + 1, end);
  }
}

快速排序實現需要注意的地方:當選擇的基準數是左哨兵的起始位置時,尋找基準數的最終位置應該是右哨兵先移動。

快速排序的執行結果如下圖所示:
快速排序的執行結果圖

基數排序

基本思想:又稱“桶排序”。依次從低位到高位分解待排序列的元素並排序,當按最高位分解並排序了待排序列後即排序完畢。

圖解基數排序如下圖:
圖解基數排序

基數排序的java實現:

/**
 * 基數排序
 * @author zhenye 2019/1/24
 */
public class RadixSort {

  /**
    * 提供給開發人員使用的基數排序
    * @param nums 待排序列
    */
  public static void sort(int[] nums) {
    int maxBit = getMaxBit(nums);
    // temp臨時二維數組,列是餘數(0-9),行是0或者待排序列中元素的值
    int[][] temp = new int[10][nums.length];
    // order對應下標,即待排序列元素對應餘數的個數
    int[] order = new int[10];
    int n = 1;
    int m = 1;
    int k = 0;
    while(m <= maxBit) {
      for (int num : nums) {
        int reminder = (num / n) % 10;
        // 將餘數爲reminder的元素歸到二維數組temp的第reminder列,並記錄此時第reminder列元素的個數order[reminder]
        temp[reminder][order[reminder]] = num;
        order[reminder]++;
      }
      // 重排待排序列
      for(int i = 0; i < order.length;i++) {
        if (order[i] != 0) {
          for(int j = 0;j < order[i];j++) {
            nums[k] = temp[i][j];
            k++;
          }
        }
        order[i] = 0;
      }
      // 重置相關變量
      n *= 10;
      k = 0;
      m++;
    }
  }

  /**
    * 找到數組中最大值的位數(如:{32,43,101,2}的最大值是101,位數是3)
    * @param nums 給定數組
    * @return 數組中最大值位數
    */
  private static int getMaxBit(int[] nums) {
    int max = nums[0];
    for(int i = 1;i < nums.length;i++) {
      if(nums[i] > max){
        max = nums[i];
      }
    }
    int maxBit;
    for(maxBit = 1; max >= 10; maxBit++){
      max = max / 10;
    }
    return maxBit;
  }
}

基數排序有其侷限性,它要求排序的過濾條件能夠分解。

基數排序的執行結果如下圖所示:
基數排序的執行結果圖

簡單選擇排序

基本思想:從待排序列中找出值最小的元素,如果最小元素不是第一個元素,則將其餘第一個元素互換。然後從餘下n-1個元素找出最小元素,重複上述操作直至排序結束。

圖解簡單選擇排序如下圖:
圖解簡單選擇排序

簡單選擇排序的Java實現如下:

/**
 * 簡單選擇排序
 * @author zhenye 2019/1/24
 */
public class SelectSort {
  /**
    * 提供給開發者使用的簡單選擇排序
    * @param nums 待排序列
    */
  public static void sort(int[] nums) {
    int i,j;
    for(i = 0; i < nums.length; i++) {
      int minValueIndex = i;
      for (j = i;j < nums.length; j++) {
        if (nums[minValueIndex] > nums[j]) {
          minValueIndex = j;
        }
      }
      IntArrayUtil.exchangeValue(nums, i, minValueIndex);
    }
  }
}

簡單選擇排序的執行結果如下圖所示:
簡單選擇排序的執行結果圖

希爾排序

基本思想:先將整個待排序列(按相隔某個“增量”的下標)分割成若干個子序列,並對這些子序列進行直接插入排序。然後依次縮減這個“增量”,分割成子序列並再次進行直接插入排序。重複上述過程到“增量”爲1,即對整個待排序列進行了一次直接插入排序。

圖解希爾排序如下圖:圖解希爾排序

希爾排序的Java實現如下:

/**
 * 希爾排序
 * @author zhenye 2019/1/24
 */
public class ShellSort {
  /**
    * 提供給開發者使用的希爾排序
    * @param nums 待排序列
    */
  public static void sort(int[] nums) {
    // i爲無序表中的元素下標,j爲有序表中的元素下標,temp是待插入元素,increment爲增量。
    int i, j, temp, increment;
    for(increment = nums.length / 2;increment >= 1; increment = increment / 2){
      for(i = increment; i < nums.length; i++) {
        temp = nums[i];
        j = i - increment;
        for(;j >= 0 && temp < nums[j];j -= increment) {
          nums[j + increment] = nums[j];
        }
        nums[j + increment] = temp;
      }
    }
  }
}

希爾排序的執行結果如下圖所示:
希爾排序的執行結果圖

八種排序算法的性能測試

八種排序算法的性能測試代碼如下:

/**
  * 排序測試類
  */
public class SortTest {
  /**
    * 各排序算法的性能測試
    */
  @Test
  public void speedTest() {
    int[] initIntArrays = IntArrayUtil.initIntArrays(100000,10000);
    System.out.println("排序前,數組順序爲:" + Arrays.toString(initIntArrays));
    int[] toBubbleSort = initIntArrays.clone();
    Long toBubbleSortStart = System.currentTimeMillis();
    BubbleSort.sort(toBubbleSort);
    Long toBubbleSortEnd = System.currentTimeMillis();
    System.out.println("冒泡排序耗時:" + (toBubbleSortEnd - toBubbleSortStart) + "毫秒。");
    System.out.println("----------------------------------------------------");
    int[] toHeapSort = initIntArrays.clone();
    Long toHeapSortStart = System.currentTimeMillis();
    HeapSort.sort(toHeapSort);
    Long toHeapSortEnd = System.currentTimeMillis();
    System.out.println("堆排序耗時:" + (toHeapSortEnd - toHeapSortStart) + "毫秒。");
    System.out.println("----------------------------------------------------");
    int[] toInsertSort = initIntArrays.clone();
    Long toInsertSortStart = System.currentTimeMillis();
    InsertSort.sort(toInsertSort);
    Long toInsertSortEnd = System.currentTimeMillis();
    System.out.println("直接插入排序耗時:" + (toInsertSortEnd - toInsertSortStart) + "毫秒。");
    System.out.println("----------------------------------------------------");
    int[] toMergeSort = initIntArrays.clone();
    Long toMergeSortStart = System.currentTimeMillis();
    MergeSort.sort(toMergeSort);
    Long toMergeSortEnd = System.currentTimeMillis();
    System.out.println("歸併排序耗時:" + (toMergeSortEnd - toMergeSortStart) + "毫秒。");
    System.out.println("----------------------------------------------------");
    int[] toQuickSort = initIntArrays.clone();
    Long toQuickSortStart = System.currentTimeMillis();
    QuickSort.sort(toQuickSort);
    Long toQuickSortEnd = System.currentTimeMillis();
    System.out.println("快速排序耗時:" + (toQuickSortEnd - toQuickSortStart) + "毫秒。");
    System.out.println("----------------------------------------------------");
    int[] toRadixSort = initIntArrays.clone();
    Long toRadixSortStart = System.currentTimeMillis();
    RadixSort.sort(toRadixSort);
    Long toRadixSortEnd = System.currentTimeMillis();
    System.out.println("基數排序耗時:" + (toRadixSortEnd - toRadixSortStart) + "毫秒。");
    System.out.println("----------------------------------------------------");
    int[] toSelectSort = initIntArrays.clone();
    Long toSelectSortStart = System.currentTimeMillis();
    SelectSort.sort(toSelectSort);
    Long toSelectSortEnd = System.currentTimeMillis();
    System.out.println("簡單選擇排序耗時:" + (toSelectSortEnd - toSelectSortStart) + "毫秒。");
    System.out.println("----------------------------------------------------");
    int[] toShellSort = initIntArrays.clone();
    Long toShellSortStart = System.currentTimeMillis();
    ShellSort.sort(toShellSort);
    Long toShellSortEnd = System.currentTimeMillis();
    System.out.println("希爾排序耗時:" + (toShellSortEnd - toShellSortStart) + "毫秒。");
  }
}

我在本地電腦測試的效果圖如下:
排序算法性能比較的測試效果圖

各排序算法的複雜度如下:
算法複雜度圖

彙總說明

  • 當業務場景簡單,數據量較小時,由於計算機運算能力強,各排序算法之間性能差別不大,推薦直接採用穩定易實現的冒泡排序或插入排序
  • 當業務場景複雜,數據量較大時,考慮到要注重性能的優化,推薦使用快速排序、堆排序或歸併排序

碼字畫圖不易,如果您覺得有用,恭請點贊!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章