排序分類
按照是否在記憶體中分類
根據在排序過程中待排序的記錄是否全部被放置在記憶體中,排序分為:內排序和外排序。
對於內排序來說,排序演算法的效能主要是受3個方面影響:
時間效能,輔助空間,演算法的複雜性。 按照演算法的實現複雜度分類 簡單演算法
冒泡排序、簡單選擇排序和直接插入排序屬於簡單演算法 改進演算法
希爾排序、堆排序、歸併排序、快速排序屬於改進演算法。 各種排序演算法分析和實現
注意:所有排序關鍵字均按照習慣採取升序排列。 冒泡演算法
冒泡排序(Bubble Sort)一種交換排序,它的基本思想是:兩兩比較相鄰記錄的關鍵字,如果反序則交換,直到沒有反序的記錄為止。 基礎冒泡演算法
//冒泡排序,較小數字上浮,將較大數字往下沉 //平均時間複雜度為O(n²) 空間複雜度為O(1) 穩定 public void bubbleSort(int[] nums) { int len = nums.length; int temp = 0; //外層控制當前冒泡區間範圍 i∈[0,len-2] for(int i=0;i<=len-2;i++) //內層從後往前,j∈[i,len-2] for(int j=len-2;j>=i;j--){ if (nums[j] > nums[j + 1]) { temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; } } print("冒泡排序", nums); }
最佳化冒泡排序
例如對於 1,2,3,5,4 這樣的關鍵字,只需要一次交換 5,4即可。剩下的交換都是無意義的。所以我們可以添加一個標識位swap,只有當發生交換時swap=true ,才執行下一次冒泡過程。
代碼改動的關鍵就是在i變數的for迴圈中,增加了對swap是否為true的判斷。經過這樣的改進,冒泡排序在效能上就有了一些提升,可以避免因已經有序的情況下的無意義迴圈判斷。
//冒泡排序,較小數字上浮,將較大數字往下沉 //平均時間複雜度為O(n²) 空間複雜度為O(1) 穩定 public void bubbleSort(int[] nums) { int len = nums.length; int temp = 0; //交換標識 boolean swap = true; //最多執行 len-1次冒泡過程 for (int i = 0; i <= len - 2 && swap; i++) { //內層從後往前,j∈[i,len-2] swap = false; for (int j = len - 2; j >= i; j--) { if (nums[j] > nums[j + 1]) { temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; swap = true; } } } print("冒泡排序", nums); }
複雜度分析
當最好的情況,也就是要排序的表本身就是有序的,那麼我們比較次數,根據最後改進的代碼,可以推斷出就是n-1次的比較,沒有資料交換,時間複雜度為O(n)。當最壞的情況,即待排序表是逆序的情況,此時需要比較sigma(i=2, n, i-1)=1+2+3+…+(n-1)=n(n-1)/2次,並作等數量級的記錄移動。因此,總的時間複雜度為O(n²)。
空間複雜度為O(1). 簡單選擇排序演算法
簡單選擇排序法(Simple Selection Sort)就是通過n-i次關鍵字間的比較,從n-i+1個記錄中選出關鍵字最小的記錄,並和第i(1≤i≤n)個記錄交換之。 演算法實現
Java實現演算法如下
//簡單選擇排序,基本思路是每次從剩餘元素選出最小值,放到應放的位置,交換 //平均時間複雜度O(n²) 空間複雜度 O(1) 不穩定 public void selectSort(int[] nums) { int minIndex, minValue; int temp; //外層迴圈不用執行到最後一個 for (int i = 0; i < nums.length - 1; i++) { minValue = nums[i]; minIndex = i; //找出[i..len] 中的最小值,從i+1開始即可 for (int j = i + 1; j < nums.length; j++) { if (nums[j] < minValue) { minValue = nums[j]; minIndex = j; } } //執行交換 if (minValue != i) { //執行交換 temp = nums[i]; nums[i] = nums[minIndex]; nums[minIndex] = temp; } } print("簡單選擇排序", nums); }
演算法複雜度分析
從簡單選擇排序的過程來看,它最大的特點就是交換移動資料次數相當少,這樣也就節約了相應的時間。分析它的時間複雜度發現,無論最好最差的情況,其比較次數都是一樣的多,第i趟排序需要進行n-i次關鍵字的比較,此時需要比較sigma(i=1, n-1, n-i)=(n-1)+(n-2)+…+1=n(n-1)/2次。而對於交換次數而言,當最好的時候,交換為0次,最差的時候,也就初始降序時,交換次數為n-1次,基於最終的排序時間是比較與交換的次數總和,因此,總的時間複雜度依然為O(n2)。
應該說,儘管與冒泡排序同為O(n²),但簡單選擇排序的效能上還是要略優於冒泡排序。因為在逆序情況下,冒泡排序的交換次數要大於簡單選擇排序。
空間複雜為O(1) 直接插入排序演算法
直接插入排序(Straight Insertion Sort)的基本操作是將一個記錄插入到已經排好序的有序表中,從而得到一個新的、記錄數增1的有序表。 程式實現
//直接插入排序,[]不斷讀取元素並擴充原有集合 //平均時間複雜度O(n²) 空間複雜度為O(1) 穩定 public void insertSort(int nums[]) { //cur儲存當前元素 int cur; //index內層迴圈索引 最終指向待插入位置的前一個位置 int index; for (int i = 1; i < nums.length; i++) { cur = nums[i]; index = i - 1; //將大於cur的元素向後移動,找到插入位置 while (index >= 0 && cur < nums[index]) { nums[index + 1] = nums[index]; index--; } //注意index+1 nums[index + 1] = cur; } print("直接插入排序", nums); }
演算法複雜度分析
當最好的情況,也就是要排序的表本身就是有序的,比如關鍵字排序為{2,3,4,5,6},共比較了(n-1)sigma(i=2, n, 1)=(n-1)*(n-1)次,由於沒有移動的記錄,時間複雜度為O(n)。
當最壞的情況,即待排序表是逆序的情況,比如{6,5,4,3,2},此時需要比較sigma(i=2, n, i)=2+3+…+n=(n+2)(n-1)/2次,而記錄的移動次數也達到最大值sigma(i=2, n, i+1)=(n+4)(n-1)/2次。
如果排序記錄是隨機的,那麼根據機率相同的原則,平均比較和移動次數約為n2/4次。因此,我們得出直接插入排序法的時間複雜度為O(n²)。從這裡也看出,同樣的O(n²)時間複雜度,直接插入排序法比冒泡和簡單選擇排序的效能要好一些,因為冒泡排序在逆序時會有更多的交換操作。
空間複雜為O(1). 希爾排序演算法
希爾排序是對直接插入排序的改進。核心思想是對待排序記錄進行分割,是減少待排序記錄的個數,並使整個序列向基本有序發展。具體採取跳躍分割的策略:將相距某個“增量”d的記錄組成一個子序列,這樣才能保證在子序列內分別進行直接插入排序後得到的結果是基本有序而不是局部有序。 增量d的選取
這裡“增量”的選取就非常關鍵。可究竟應該選取什麼樣的增量才是最好,目前還是一個數學難題,迄今為止還沒有人找到一種最好的增量序列。不過大量的研究表明,當增量序列為dlta[k]=2t-k+1-1(0≤k≤t≤log2(n+1))時,可以獲得不錯的效率,其時間複雜度為O(n3/2次方),要好於直接排序的O(n²)。需要注意的是,增量序列的最後一個增量值必須等於1才行。另外由於記錄是跳躍式的移動,希爾排序並不是一種穩定的排序演算法。 演算法實現
這裡選取一次選擇增量 d=n/2 d=n/4 …. d=1
//希爾排序,選取增量區間,區間內直接插入排序,直接插入排序的改進版本 //增量的選取 例如d=len d/=2 public void shellSort(int[] nums) { int len = nums.length; int d = len; while (true) { //增量縮減為1/2 d /= 2; //cur儲存當前元素 int cur; //pos內層迴圈索引 最終指向待插入位置的前一個位置 int pos; //最外層遍曆每一個組 for (int i = 0; i < d; i++) { //初始位置 i+d 步長為d for (int j = i + d; j < len; j += d) { cur = nums[j]; pos = j - d; //將大於cur的元素向後移動,找到插入位置,注意步長為d while (pos >= i && cur < nums[pos]) { nums[pos + d] = nums[pos]; pos -= d; } //注意pos 指向最終插入位置的前一個位置 nums[pos + d] = cur; } } //這個放到最後,保證d==1的插入排序執行 if (d == 1) break; } print("希爾排序", nums); }
複雜度分析
跟增量選取有關係,當增量合適時,可以將時間複雜度提高到O(n3/2)
好於O(n²)。空間複雜度為O(1). 堆排序演算法
堆排序(Heap Sort)就是利用堆(假設利用大頂堆)進行排序的方法。它的基本思想是,將待排序的序列構造成一個大頂堆。此時,整個序列的最大值就是堆頂的根結點。將它移走(其實就是將其與堆數組的末尾元素交換,此時末尾元素就是最大值),然後將剩餘的n-1個序列重新構造成一個堆,這樣就會得到n個元素中的次大值。如此反覆執行,便能得到一個有序序列了。
其中最關鍵的操作有兩個,建堆(假設大頂堆)操作和交換操作。建堆操作是依次將[0,len-1-i] 區間中的所有元素構造成大頂堆,此時堆頂元素nums[0]則為最大值,然後交換堆頂元素nums[0] 和nums[len-1-i]。 程式實現
建堆操作
//建立大頂堆,將最大值交換到了根節點(索引為0) private void buildMaxHeap(int[] data, int lastIndex) { //從lastIndex 的父節點開始 for (int i = (lastIndex - 1) / 2; i >= 0; i--) { //儲存當前索引 int k = i; //如果當前k結點有子節點,需要找到自身和子節點中的最大值並進行交換 if ((2 * k + 1) <= lastIndex) { //左子節點索引 int leftIndex = 2 * k + 1; int rightIndex = leftIndex + 1; //左右結點都有,如果是lastIndex,那麼lastIndex 就是k的右子節點 if (rightIndex <= lastIndex) { //左子節點最大,包含兩個節點相等且都大於k的情況 if (data[k] < data[leftIndex] && data[leftIndex] >= data[rightIndex]) swap(data, k, leftIndex); //右子節點最大 else if (data[k] < data[rightIndex] && data[rightIndex] > data[leftIndex]) swap(data, k, rightIndex); } //只有左子節點 else { //只有左子節點大於k才執行交換 if (data[leftIndex] > data[k]) swap(data, k, leftIndex); } } } } //交換函數 private void swap(int[] data, int i, int j) { int temp = data[i]; data[i] = data[j]; data[j] = temp; }
主程式
//堆排序,構建完全二叉樹(根最大是大頂堆,最小則是小頂堆) //平均時間複雜度為O(n*logn)空間複雜度O(1) 不穩定 public void heapSort(int[] nums) { int len = nums.length; //迴圈建堆 for (int i = 0; i < len; i++) { buildMaxHeap(nums, len - 1 - i); //交換堆頂和最後一個元素,交換之後下次建隊得到次大元素 swap(nums, 0, len - 1 - i); } print("堆排序", nums); }
演算法複雜度分析
它的已耗用時間主要是消耗在初始構建堆和在重建堆時的反覆篩選上。
在構建堆的過程中,因為我們是完全二叉樹從最下層最右邊的非終端結點開始構建,將它與其孩子進行比較和若有必要的互換,對於每個非終端結點來說,其實最多進行兩次比較和互換操作,因此整個構建堆的時間複雜度為O(n)。
在正式排序時,第i次取堆頂記錄重建堆需要用O(logi)的時間(完全二叉樹的某個結點到根結點的距離為),並且需要取n-1次堆頂記錄,因此,重建堆的時間複雜度為O(nlogn)。
所以總體來說,堆排序的時間複雜度為O(nlogn)。由於堆排序對原始記錄的排序狀態並不敏感,因此它無論是最好、最壞和平均時間複雜度均為O(nlogn)。這在效能上顯然要遠遠好過於冒泡、簡單選擇、直接插入的O(n²)的時間複雜度了。
空間複雜度上,它只有一個用來交換的暫存單元,複雜度為O(1).由於記錄的比較與交換是跳躍式進行,因此堆排序也是一種不穩定的排序方法。
由於初始構建堆所需的比較次數較多,因此堆排序並不適合待排序序列個數較少的情況。 歸併排序演算法
歸併排序(Merging Sort)就是利用歸併的思想實現的排序方法。它的原理是假設初始序列含有n個記錄,則可以看成是n個有序的子序列,每個子序列的長度為1,然後兩兩歸併,得到|n/2|(|x|表示不小於x的最小整數)個長度為2或1的有序子序列;再兩兩歸併,……,如此重複,直至得到一個長度為n的有序序列為止,這種排序方法稱為2路歸併排序。
歸併排序是典型的分治法思想分解:將原問題分解成一系列子問題
遞迴地求解各子問題。若子問題足夠小,則直接求解,最後將子問題的結果合并成原問題的解。
核心操作有兩個:兩個有序數組的合并merge操作,原問題的分割操作。 演算法實現
merge操作
//merge操作 [low,mid] [mid+1,high] private void merge(int[] nums, int[] arr, int low, int mid, int high) { int i = low; int j = mid + 1; int k = 0; while (i <= mid && j <= high) { if (nums[i] < nums[j]) { arr[k] = nums[i]; i++; } else { arr[k] = nums[j]; j++; } k++; } //處理剩餘 while (i <= mid) { arr[k] = nums[i]; i++; k++; } while (j <= high) { arr[k] = nums[j]; j++; k++; } //複製到原有數組中 for (int index = 0; index < k; index++) { nums[low + index] = arr[index]; } }
遞迴調用的分割操作
//分治法,遞迴調用 private void mSort(int[] nums, int[] arr, int low, int high) { if (low < high) { int mid = (low + high) / 2; mSort(nums, arr, low, mid); //左邊有序 mSort(nums, arr, mid + 1, high); //右邊有序 merge(nums, arr, low, mid, high); //再將二個有序數列合并 } }
主程式
//歸併排序,分治法, //時間複雜度為O(nlogn),空間複雜度為O(n),穩定 public void mergeSort(int[] nums) { int[] arr = new int[nums.length]; mSort(nums, arr, 0, nums.length - 1); print("歸併排序", nums); }
演算法複雜度分析
我們來分析一下歸併排序的時間複雜度,一趟歸併需要將nums中相鄰的長度為h的有序序列進行兩兩歸併。並將結果放到原數組中,這需要將待排序序列中的所有記錄掃描一遍,因此耗費O(n)時間,而由完全二叉樹的深度可知,整個歸併排序需要進行log2n(2為底數)次,因此,總的時間複雜度為O(nlogn),而且這是歸併排序演算法中最好、最壞、平均的時間效能。
由于歸並排序在歸併過程中需要與原始記錄序列同樣數量的儲存空間存放歸併結果以及遞迴時深度為log2n的棧空間,因此空間複雜度為O(n+logn)。另外,Merge函數中有if(SR[i] 快速排序
快速排序(Quick Sort)的基本思想是:通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。
也是典型的分治法思想,大問題分解成子問題。關鍵操作有這麼幾個。樞軸的選取操作getPivot,選擇哪一個關鍵字來進行分割;一趟排序操作,根據選取的樞軸進行排序partition函數,遞迴調用的區間分割操作。 演算法實現
選擇樞軸
//取得樞軸,這裡選取第一個 private int getPivot(int[] nums, int low, int high) { return nums[low]; }
一趟排序操作
//partition函數將nums 以樞軸元素分界,[左邊<=]pivot[<=右邊] private int partition(int[] nums, int low, int high) { //取樞軸元素 int pivot = getPivot(nums, low, high); while (low < high) { //從後往前找第一個小於pivot的元素 while (low < high && nums[high] >= pivot) high--; //交換 swap(nums, low, high); //從前往後找到第一個大於pivot的元素 while (low < high && nums[low] <= pivot) low++; //交換 swap(nums, low, high); } //low=high 最終樞軸位置在此 return low; }
區間分割操作
//遞迴調用 private void qSort(int[] nums, int low, int high) { //此條件不可缺少,遞迴終止條件 if (low >= high) return; int pivot = partition(nums, low, high); //左區間遞迴排序 qSort(nums, low, pivot - 1); //右區間遞迴排序 qSort(nums, pivot + 1, high); }
主程式調用入口
//快速排序 //平均時間複雜度O(nlogn) 空間複雜度O(1) 不穩定 public void quickSort(int[] nums) { qSort(nums, 0, nums.length - 1); print("快速排序", nums); }
演算法複雜度分析
由數學計算可以得出,在最優的情況下,快速排序演算法的時間複雜度為O(nlogn)。
在最壞的情況下,待排序的序列為正序或者逆序,每次劃分只得到一個比上一次劃分少一個記錄的子序列,注意另一個為空白。如果遞迴樹畫出來,它就是一棵斜樹。此時需要執行n-1次遞迴調用,且第i次劃分需要經過n-i次關鍵字的比較才能找到第i個記錄,也就是樞軸的位置,因此比較次數為sigma(i=1, n-1, n-i)=(n-1)+(n-2)+…+1=n(n-1)/2,最終其時間複雜度為O(n²)。
平均的情況,由數學歸納法可證明,其數量級為O(nlogn)。
從空間複雜度來看,主要是遞迴造成的棧空間的使用,最好情況,遞迴樹的深度為log2n,其空間複雜度也就為O(logn),最壞情況,需要進行n-1遞迴調用,其空間複雜度為O(n),平均情況,空間複雜度也為O(logn)。
由於關鍵字的比較和交換是跳躍進行的,因此,快速排序是一種不穩定的排序方法。 快速排序最佳化 1. 最佳化樞軸選取
選取首個樞軸在待排序的序列是基本有序的情況下是不合理的,會造成很多無效操作。
隨機選取樞軸法
隨機獲得一個low與high之間的數rnd,讓它的關鍵字L.r[rnd]與L.r[low]交換,這被稱為隨機選取樞軸法。在某種程度上,解決了對於基本有序的序列快速排序時的效能瓶頸。
三數取中法
對隨機選取法的進一步改進,最佳化隨機選取出現極值的情況。
即取三個關鍵字先進行排序,將中間數作為樞軸,一般是取左端、右端和中間三個數,也可以隨機選取。這樣至少這個中間數一定不會是最小或者最大的數,從機率來說,取三個數均為最小或最大數的可能性是微乎其微的,因此中間數位於較為中間的值的可能性就大大提高了。 2. 最佳化不必要的交換
將樞軸元素值暫存起來,將交換操作改成直接賦值操作。然後在之前是swap時,只作替換的工作,最終當low與high會合,即找到了樞軸的位置時,再將樞紐元素歸位。因為這當中少了多次交換資料的操作,在效能上又得到了部分的提高。 3. 最佳化小數組時的排序方案
如果數組非常小,其實快速排序反而不如直接插入排序來得更好(直接插入是簡單排序中效能最好的)。其原因在於快速排序用到了遞迴操作,在大量資料排序時,這點效能影響相對於它的整體演算法優勢而言是可以忽略的,但如果數組只有幾個記錄需要排序時,這就成了一個大炮打蚊子的大問題。因此可以增加一個關鍵字數量的閾值判斷,當小於這個閾值時,採用直接插入排序,大於這個閾值才採用快速排序。閾值可以選取7 或50均可。有資料認為7比較合適,也有認為50更合理,實際應用可適當調整。
private static final int MAX_LENGTH_INSERT_SORT = 7; //改進qSort private void qSort1(int[] nums, int low, int high) { //此條件不可缺少,遞迴終止條件 if (low >= high) return; if (low - high > MAX_LENGTH_INSERT_SORT) { int pivot = partition1(nums, low, high); //左區間遞迴排序 qSort(nums, low, pivot - 1); //右區間遞迴排序 qSort(nums, pivot + 1, high); } else insertSort(nums); }
4. 最佳化遞迴操作
遞迴對效能是有一定影響的,qSort函數在其尾部有兩次遞迴操作。如果待排序的序列劃分極端不平衡,遞迴深度將趨近於n,而不是平衡時的log2n,這就不僅僅是速度快慢的問題了。棧的大小是很有限的,每次遞迴調用都會耗費一定的棧空間,函數的參數越多,每次遞迴耗費的空間也越多。因此如果能減少遞迴,將會大大提高效能。
//遞迴最佳化 private void qSort2(int[] nums, int low, int high) { //此條件不可缺少,遞迴終止條件 if (low >= high) return; if (low - high > MAX_LENGTH_INSERT_SORT) { while (low < high) { int pivot = partition1(nums, low, high); //左區間遞迴排序 qSort(nums, low, pivot - 1); //尾遞迴 low = pivot + 1; //下次迴圈相當於再迴圈後, // 來一次Partition(L,low,high), // 其效果等同於“QSort(L,pivot+1,high) } } else insertSort(nums); }
當我們將if改成while後,因為第一次遞迴以後,變數low就沒有用處了,所以可以將pivot+1賦值給low,再迴圈後,來一次partition(nums,low,high),其效果等同於“qSort(nums,pivot+1,high);”。結果相同,但因採用迭代而不是遞迴的方法可以縮減堆棧深度,從而提高了整體效能。 各種排序演算法的比較和選擇
從演算法的簡單性來看,我們將7種演算法分為兩類:
簡單演算法:冒泡、簡單選擇、直接插入。
改進演算法:希爾、堆、歸併、快速。 時間複雜度比較 平均情況比較
從平均情況來看,顯然最後3種改進演算法要勝過希爾排序,並遠遠勝過前3種簡單演算法。 最好情況比較
從最好情況看,反而冒泡和直接插入排序要更勝一籌,也就是說,如果你的待排序序列總是基本有序,反而不應該考慮4種複雜的改進演算法。 最壞情況比較
從最壞情況看,堆排序與歸併排序又強過快速排序以及其他簡單排序。 空間複雜度比較
從空間複雜度來說,歸併排序,快速排序都有相應的空間要求,反而堆排序等對空間要求是O(1)。如果記憶體有限,就別選擇歸併排序和快速排序了。 穩定性比較
從穩定性來看,歸併排序只對於非常在乎排序穩定性的應用中,歸併排序是個好演算法。 根據待排序資料本身特徵來選擇 排序記錄數
從待排序記錄的個數上來說,待排序的個數n越小,採用簡單排序方法越合適。反之,n越大,採用改進排序方法越合適。這也就是我們為什麼對快速排序最佳化時,增加了一個閥值,低於閥值時換作直接插入排序的原因。 關鍵字本身所佔儲存空間較大
似乎簡單選擇排序在3種簡單排序中效能最差,其實也不完全是,比如,如果記錄的關鍵字本身資訊量比較大(例如,關鍵字都是數十位的數字),此時表明其佔用儲存空間很大,這樣移動記錄所花費的時間也就越多,所以應該盡量減少關鍵字的交換次數。我們給出3種簡單排序演算法的移動次數比較,
此時簡單選擇排序就變得非常有優勢,原因也就在於,它是通過大量比較後選擇明確記錄進行移動,具有最少的記錄交換次數。因此對於資料量不是很大而記錄的關鍵字資訊量較大的排序要求,簡單排序演算法是佔優的。另外,記錄的關鍵字資訊量大小對那四個改進演算法影響不大。
總之,從綜合各項指標來說,經過最佳化的快速排序是效能最好的排序演算法,但是不同的場合我們也應該考慮使用不同的演算法來應對它。 其他排序 計數排序
當輸入的元素是 n 個 0 到 k 之間的整數時,它的已耗用時間是 Θ(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序演算法。
由於用來計數的數組C的長度取決於待排序數組中資料的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於資料範圍很大的數組,需要大量記憶體。計數排序是用來排序0到100之間的數位最好的演算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的演算法來排序資料範圍很大的數組。
演算法的步驟如下:
1.找出待排序的數組中最大和最小的元素
2. 統計數組中每個值為i的元素出現的次數,存入數組C的第i項
3. 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
4. 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1 演算法實現
//計數排序 public void countSort(int[] nums) { int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; //找出數組中的最大最小值 for (int i = 0; i < nums.length; i++) { max = Math.max(max, nums[i]); min = Math.min(min, nums[i]); } int[] help = new int[max - min + 1]; //找出每個數字出現的次數 for (int i = 0; i < nums.length; i++) { int mapPos = nums[i] - min; help[mapPos]++; } int index = 0; for (int i = 0; i < help.length; i++) { while (help[i] > 0) { nums[index++] = i + min; help[i]--; } } print("