1. 程式人生 > >7種經典排序演算法Java程式碼實現(冒泡+選擇+插入+歸併+快排+堆排序+希爾)

7種經典排序演算法Java程式碼實現(冒泡+選擇+插入+歸併+快排+堆排序+希爾)

氣泡排序

氣泡排序的原理是:比較兩個相鄰的元素,將值較大的元素交換到至右端(比較+移動)

  1. public static void sortMpao(int[] num) {
  2.     for(int i = 1; i < num.length; i++) { //控制迴圈多少趟(n-1趟)
  3.         for(int j = 1; j <= num.length - i; j++) { //控制每一趟的迴圈次數(n-i次)
  4.             if(num[j - 1] > num[j]) {
  5.                 int temp = num[j - 1];
  6.                 num[j - 1] = num[j];
  7.                 num[j] = temp;
  8.             }
  9.       }
  10.    }
  11. }

時間複雜度:O(n^2)

空間複雜度:O(n^2)

 

選擇排序

選擇排序原理:每一趟從待排序的記錄中選出最小的元素,順序放在已排好序的序列最後,直到全部記錄排序完畢。也即是在 n - i + 1(i = 1, 2, ..., n - 1) 個記錄中選取關鍵字最小的記錄作為有序陣列的第 i 個記錄。

  1. public static void sortXze(int[] nums) {
  2.     for(int i = 0; i < nums.length - 1; i++) { //n - 1 趟迴圈,第 i 趟排序
  3.         int k = i; //用於記錄最小數值的位置
  4.         for(int j = k + 1; j < nums.length; j++) { //選擇最小值記錄位置,然後交換(min操作,依次和min值進行比較並更新min值)
  5.             if(nums[j] < nums[k]) {
  6.                 k = j; //記錄下目前找到的最小值所在的位置
  7.             }
  8.         }
  9.         //在內層迴圈結束,也即是找到了本輪迴圈的最小的數之後,再進行交換
  10.         if(i != k) {
  11.             int temp = nums[i];
  12.             nums[i] = nums[k];
  13.             nums[k] = temp;
  14.         }
  15.     }
  16. }

時間複雜度:O(n^2)

插入排序

插入排序原理:把n個待排序的元素看成是一個有序表和一個無序表,開始時有序表中只有一個元素,無序表中有 n - 1 個元素。排序過程中,即每次從無序表中取出第一個元素,將它插入到有序表中,使之成為新的有序表,重複 n - 1 次完成整個排序過程

  1. public static void sortCru(int[] nums) {
  2.     for(int i = 1; i < nums.length; i++) { //迴圈 n - 1 趟,標記帶插入元素
  3.  
  4.         int temp = nums[i]; //temp為本次迴圈待插入到有序表中的數
  5.         int j = i - 1; //因為for迴圈外還要使用變數j,用於標記插入位置下標,j = i - 1,表示有序表最後一位數
  6.         for(; j >= 0 && nums[j] > temp; j--) {
  7.             nums[j + 1] = nums[j]; //依次將大於temp的元素往後移動一位,為插入temp作準備
  8.         }
  9.         //找到了合適的位置,要麼是temp > nums[j],要麼是j = -1,則temp插入下標為 j + 1
  10.         nums[j + 1] = temp;
  11.     }
  12. }

時間複雜度:O(n^2)

空間複雜度:O(1)

穩定性:相同元素的相對位置不會改變

歸併排序

演算法原理:歸併排序是將多個有序資料表合併成一個有序資料表。如果參與合併的只有兩個有序表,則稱為二路合併,而對於一個原始的待排序數列,往往可以通過分割(二分)的方法來歸結為多路合併排序。歸併排序演算法採用了經典的分治(divide-and0conquer)策略,將問題先分成一些小問題,然後遞推下去,在"治"的階段將分的階段得到的各答案"修補"在一起。

 

  1. //二分法進行遞迴排序
  2. public static int[] sort(int[] nums, int low, int high) {
  3.     int mid = low + (high - low)/2; //將輸入陣列分成兩個部分
  4.     if(low < high) {
  5.         //左邊
  6.         sort(nums, low, mid);
  7.         //右邊
  8.         sort(nums, mid + 1, high);
  9.         merge(nums, low, mid, high); //遞迴終止時,陣列長度為2,因此此時的合併即包含排序過程
  10.     }
  11. return nums;
  12. }
  13. //合併兩個有序陣列方法,這裡由於是分割原陣列,因此可以直接用輔助陣列填補原陣列即可,不需要返回結果了
  14. public static void merge(int[] nums, int low, int mid, int high) {
  15.     int[] temp = new int[high - low + 1]; //輔助陣列,和原陣列大小一樣
  16.     int i = low; //左指標
  17.     int j = mid + 1; //右指標
  18.     int k = 0;
  19.     //把較小的數移到新陣列中去
  20.     while(i <= mid && j <= high) {
  21.         temp[k++] = nums[i] < nums[j] ? nums[i++] : nums[j++];
  22.     }
  23.     //如果左邊有剩餘,則把左邊剩餘的數移入陣列
  24.     while(i <= mid) {
  25.         temp[k++] = nums[i++];
  26.     }
  27.     //如果右邊剩餘,則把右邊剩餘的數移入陣列
  28.     while(j <= high) {
  29.         temp[k++] = nums[j++];
  30.     }
  31.     //新陣列中的數字覆蓋nums陣列
  32.     for(int k2 = 0; k2 < temp.length; k2++) {
  33.         nums[k2 + low] = temp[k2]; //注意新陣列是在原陣列的low位置開始的,high位置結束的(受mid影響)
  34.     }
  35. }

 

時間複雜度:O(nlogn)

空間複雜度:O(n)

快速排序

演算法原理:選擇一個關鍵值作為基準值(一般選擇序列的第一個元素作為基準值),比基準值小的都在左邊序列(一般是無序的),比基準值大的都在右邊(一般是無序的)。一次迴圈:從後往前比較,用基準值和最後一個值比較,如果比基準值小的交換位置,如果沒有繼續比較下一個,直到找到第一個比基準值小的值才交換。找到這個值之後,又從前往後開始比較,如果有比基準值大的,交換位置,如果沒有繼續比較下一個,直到找到第一個比基準值大的值才交換。直到從前往後的比較索引等於從後往前比較的索引,結束第一次迴圈,此時,對於基準值來說,左右兩邊就是有序的了(左邊均小於基準值,右邊均大於基準值,但是左右兩邊仍然可能是無序的)。接著分別比較左右兩邊的序列,重複上述的迴圈。

  1. public static void sort(int[] nums, int low, int high) {
  2.     int start = low;
  3.     int end = high;
  4.     int key = nums[low]; //基準值,一般選取左邊序列第一個元素
  5.     while(end > start) { 
  6.         //從後往前比較
  7.         //如果沒有比關鍵值小的,比較下一個,直到有比基準值小的值,記錄下位置end
  8.        while(end > start && nums[end] > key) {
  9.             end--;
  10.         }
  11.        //比基準值小的位置上的元素和左邊基準值進行交換(此時基準值換到了右邊)
  12.        if(nums[end] <= key) {
  13.            int temp = nums[end];
  14.             nums[end] = nums[start]; 
  15.             nums[start] = temp; //注意交換的是start和end索引對應的值
  16.     }
  17.     //然後從前往後比較
  18.     //如果沒有比關鍵值大的,比較下一個,直到有比關鍵值大的元素,記錄下位置start
  19.     while(end > start && nums[start] < key) {
  20.         start++;
  21.     }
  22.     //比基準值大的位置上的元素和右邊基準值進行交換(此時基準值換到了左邊)
  23.     if(nums[start] >= key) {
  24.         int temp = nums[start];
  25.         nums[start] = nums[end]; //此時nums[start]和key值相等
  26.         nums[end] = temp; //注意交換的是start和end索引對應的值
  27.     }
  28. }
  29. //第一次迴圈比較結束,關鍵值位置也已經確定(此時start=end,位於key值處)。左邊的值都比關鍵值小,右邊的值都比關鍵值大,但是兩邊的順序還有可能是不一樣的,進行下面的遞迴呼叫,直到 low == start == end == high時,也即是一個元素時,則所有系必有序(相對於分治中先小的有序然後逐層合併有序陣列,快速排序則是先大致有序,逐層縮小排序範圍,直到最後一個元素,共同點是都使用了遞迴演算法,且遞迴終止條件類似)
  30.     if(start > low) sort(nums, low, start-1); //左邊序列,第一個索引位置到關鍵值索引-1
  31.     if(end < high) sort(nums, end+1, high); //右邊序列,從關鍵值索引+1到最後一個
  32. }

時間複雜度:平均 O(nlog(n)),最壞情況下 O(n^2)

希爾排序(縮小增量排序)

演算法原理:現將待排序的陣列元素分成多個子序列,使得每個子序列的元素個數相對較少,然後對各個子序列分別進行直接插入排序,待整個待排序列“基本有序”後,最後在對所有元素進行一次直接插入排序。因此,我們要採用跳躍分割的策略:將相距某個“增量”的記錄組成一個子序列,這樣才能保證在子序列內分別進行直接插入排序後得到的結果是基本有序而不是區域性有序。希爾排序是對直接插入排序演算法的優化和升級。

  1. public static void shellSortSmallToBig(int[] nums) {
  2.     int j = 0;
  3.     int temp = 0;
  4.     //最開始的步長為原陣列長度的一半,步長依次為原來的 1/2
  5.     for(int incre = nums.length / 2; incre > 0; incre /= 2) { //控制步長,最小為1,之後有序
  6.         for(int i = incre; i < nums.length; i++) { //每一個i表示同一步長不同子序列的某一個待插入元素,i = incre 表示第一個子序列的第一個待插入元素
  7.         temp = nums[i]; //待插入元素,在下標為i處
  8.         j = i - incre; //j等於 i減去步長incre
  9.         //完成一個子序列的一個元素的插入,步長為incre,故 j = j - incre
  10.         while(j >= 0 && temp < nums[j]) {
  11.             nums[j + incre] = nums[j]; //按照一定步長進行插入排序,元素後移為插入元素做準備
  12.             j = j - incre;
  13.         }
  14.             nums[j + incre] = temp;
  15.         }
  16.     }
  17. }

希爾排序優點:希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小,插入排序對於有序的序列效率很高。所以,希爾排序的時間複雜度會比o(n^2)好一些

 

堆排序

reference:https://blog.csdn.net/qq_33535433/article/details/74968686

演算法原理:將資料看成是完全二叉樹,根據完全二叉樹的特性來進行排序的一種演算法,堆排序的過程就是在..建堆—>交換...之間重複。一次建堆完成之後,我們的最大值(或最小值)就在堆的根節點之上,隨後將堆頂最大值(或最小值)和陣列最後的元素進行替換,於是就完成一趟排序了

  1. //原地堆排序(從索引0開始)
  2. public static void heapSort(int[] nums, int n) {
  3.     //將一個完全無序的陣列構造成最大堆
  4.     for(int i = (n - 1) / 2; i >= 0; i--) {
  5.         shiftDown(nums, n, i);
  6.     }
  7.     //進行堆排序:依次將根節點(最大值)與最後一個值(最右葉子節點)進行交換
  8.     for(int i = n - 1; i > 0; i--) {
  9.         swap(nums, 0, i);
  10.         shiftDown(nums, i, 0); //將最大值放在原陣列最後一個位置(n - 1),然後待排序陣列長度正好為 n - 1
  11.     }
  12. }
  13. public static void _shiftDown(int[] nums, int n, int k) {
  14.     //陣列下標從0開始,則滿二叉樹父子節點間關係是:left = 2 * parent + 1, right = 2 * parent + 2, 終止條件為 n - 1
  15.     //陣列下標從1開始,則滿二叉樹父子節點間關係是:left = 2 * parent, right = 2 * parent + 1, 終止條件為 n (正好吻合節點總數)
  16.     while((2 * k + 1) < n){ //有左子節點(n - 1才為最後一個節點下標)
  17.         int j = 2 * k + 1; //記錄最大子節點下標(初始為左子節點)
  18.         if((j + 1) < n && (nums[j + 1] > nums[j])){ //有右子節點且右邊的更大
  19.             j += 1;
  20.         }
  21.         if(nums[k] >= nums[j]) //如果父節點大於等於子節點,則停止迴圈
  22.             break;
  23.         swap(nums, k, j);
  24.         k = j; //k被賦為當前位置,為下次迴圈做初始化
  25.     }
  26. }
  27. public static void swap(int[] arr,int a,int b){
  28.     int c=arr[a];
  29.     arr[a]=arr[b];
  30.     arr[b]=c;
  31. }

時間複雜度:O(nlog(n)),最壞情況下也和平均一樣,均如此