八種排序演算法總結(Java實現)
排序演算法有很多,在特定情景中使用哪一種演算法很重要。本文對幾種常見排序演算法做了簡單的總結。
一、氣泡排序
氣泡排序(BubbleSort)是一種簡單的排序演算法。它重複地走訪要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。
1、原理:
- 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
- 針對所有的元素重複以上的步驟,除了最後一個。
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
2、程式碼:
從小到大排序數列
public class Sort { public static void main(String[] args){ int score[] = { 100, 99, 90, 89, 87, 75, 69, 67 }; for (int i = 0; i < score.length -1; i++){ //最多做n-1趟排序 for(int j = 0 ;j < score.length - i - 1; j++){ //對當前無序區間score[0......length-i-1]進行排序(j的範圍很關鍵,這個範圍是在逐步縮小的) if(score[j] > score[j + 1]){ //把小的值交換到前面 int temp = score[j]; score[j] = score[j + 1]; score[j + 1] = temp; } } System.out.print("第" + (i + 1) + "次排序結果:"); for(int a = 0; a < score.length; a++){ System.out.print(score[a] + "\t"); } System.out.println(""); } System.out.print("最終的排序結果:"); for(int a = 0; a < score.length; a++){ System.out.print(score[a] + "\t"); } } }
執行結果:
第1次排序結果:99 90 89 87 75 69 67 100 第2次排序結果:90 89 87 75 69 67 99 100 第3次排序結果:89 87 75 69 67 90 99 100 第4次排序結果:87 75 69 67 89 90 99 100 第5次排序結果:75 69 67 87 89 90 99 100 第6次排序結果:69 67 75 87 89 90 99 100 第7次排序結果:67 69 75 87 89 90 99 100 最終的排序結果:67 69 75 87 89 90 99 100
3、效率:
時間複雜度為O(n²),適用於排序小列表;最佳情況(序列原本就是正序)時間複雜度為O(n),此時需要對程式碼進行改進(以從大到小排列為例):
public class Sort {
public static void main(String[] args){
int score[] = { 100, 99, 90, 89, 87, 75, 69, 67 };
boolean didSwap;
for (int i = 0; i < score.length -1; i++){ //最多做n-1趟排序
didSwap=false;
for(int j = 0 ;j < score.length - i - 1; j++){ //對當前無序區間score[0......length-i-1]進行排序(j的範圍很關鍵,這個範圍是在逐步縮小的)
if(score[j] < score[j + 1]){ //把大的值交換到前面
int temp = score[j];
score[j] = score[j + 1];
score[j + 1] = temp;
didSwap=true;
}
}
if (didSwap==false) {
System.out.print("第" + (i + 1) + "次排序結果:");
for(int a = 0; a < score.length; a++){
System.out.print(score[a] + "\t");
}
System.out.println("");
System.out.print("最終的排序結果:");
for(int a = 0; a < score.length; a++){
System.out.print(score[a] + "\t");
}
return;
}
}
}
}
執行結果:
第1次排序結果:100 99 90 89 87 75 69 67
最終的排序結果:100 99 90 89 87 75 69 67
二、快速排序
1、原理
選擇一個基準元素,通常選擇第一個元素或者最後一個元素,通過一趟掃描,將待排序列分成兩部分,一部分比基準元素小,一部分大於等於基準元素,此時基準元素在其排好序後的正確位置,然後再用同樣的方法遞迴地排序劃分的兩部分。
2、程式碼:
public class Sort {
public static void main(String[] args) {
int a[] = { 37, 38, 66, 97, 76, 13, 27, 49, 78, 34, 12, 64,5, 4, 62,
99, 98, 54, 56, 18, 17, 23, 34, 15, 35, 25, 53, 51 };
quick(a);
for (int i = 0; i < a.length; i++){
System.out.print(a[i]+"");
}
}
public static int getMiddle(int[] list, int low, int high) {
int tmp = list[low]; // 陣列的第一個數作為中軸
while (low < high) {
while (low < high && list[high] >= tmp) {
high--;
}
list[low] = list[high]; // 比中軸小的記錄移到低端
while (low < high && list[low] <= tmp) {
low++;
}
list[high] = list[low]; // 比中軸大的記錄移到高階
}
list[low] = tmp; // 中軸記錄到尾
return low; // 返回中軸的位置
}
public static void _quickSort(int[] list, int low, int high) {
if (low < high) {
int middle = getMiddle(list, low, high); // 將list陣列進行一分為二
_quickSort(list, low, middle - 1); // 對低字表進行遞迴排序
_quickSort(list, middle + 1, high); // 對高字表進行遞迴排序
}
}
public static void quick(int[] a2) {
if (a2.length > 0) { // 檢視陣列是否為空
_quickSort(a2, 0, a2.length - 1);
}
}
}
執行結果:
4 5 12 13 15 17 1823 25 27 34 34 35 37 38 49 51 53 54 56 62 64 66 76 78 97 98 99
3、效率:
快速排序是通常被認為在同數量級(O(nlog2n))的排序方法中平均效能最好的。但若初始序列按關鍵碼有序或基本有序時,快排排序反而蛻化為氣泡排序。為改進之,通常以“三者取中法”來選取基準記錄,即將排序區間的兩個端點與中點三個記錄關鍵碼居中的調整為支點記錄。快速排序是一個不穩定的排序方法。
三、直接選擇排序
選擇排序是常用內部排序的一種,常見的實現演算法有直接選擇排序演算法和堆排序演算法,選擇排序的基本思想是每次從待排資料中選擇第n小的資料放到排序列表的第n個位置,假如共有N個數據待排,那麼經過N-1次排序後,待排資料就已經按照從小到大的順序排列了。
1、原理
直接選擇排序(Selectionsort)是一種簡單直觀的排序演算法。它的工作原理如下。首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小元素,然後放到排序序列末尾(目前已被排序的序列)。以此類推,直到所有元素均排序完畢。
假設資料放在一個數組a中,且陣列的長度是N,則直接選擇排序的流程為:
- 從a[0]-a[N-1]中選出最小的資料,然後與a[0]交換位置
- 從a[1]-a[N-1]中選出最小的資料,然後與a[1]交換位置(第1步結束後a[0]就是N個數的最小值)
- 從a[2]-a[N-1]中選出最小的資料,然後與a[2]交換位置(第2步結束後a[1]就是N-1個數的最小值)
- 以此類推,N-1次排序後,待排資料就已經按照從小到大的順序排列了。
2、程式碼:
public class Sort {
public static void main(String[] args){
int score[] = { 49,38,65,97,76,13,27,14,10 };
for (int i = 0; i < score.length -1; ++i){
int k=i;
for(int j = i ;j < score.length; ++j){
if(score[k] < score[j]){
k=j;
}
}
if (k!=i) {//交換元素
int temp = score[i];
score[i] = score[k];
score[k] = temp;
}
System.out.print("第" + (i + 1) + "次排序結果:");
for(int a = 0; a < score.length; a++){
System.out.print(score[a]+ "\t");
}
System.out.println("");
}
System.out.print("最終的排序結果:");
for(int a = 0; a < score.length; a++){
System.out.print(score[a]+ "\t");
}
}
}
執行結果:
第1次排序結果:97 38 65 49 76 13 27 14 10
第2次排序結果:97 76 65 49 38 13 27 14 10
第3次排序結果:97 76 65 49 38 13 27 14 10
第4次排序結果:97 76 65 49 38 13 27 14 10
第5次排序結果:97 76 65 49 38 13 27 14 10
第6次排序結果:97 76 65 49 38 27 13 14 10
第7次排序結果:97 76 65 49 38 27 14 13 10
第8次排序結果:97 76 65 49 38 27 14 13 10
最終的排序結果:97 76 65 49 38 27 14 13 10
3、效率:
時間複雜度為(O(n*n)),適用於排序小的列表。
四、堆排序
堆排序演算法和直接選擇排序演算法最大的不同在於,堆排序演算法充分利用大頂堆和完全二叉樹的性質,保留每次排序後的結構,同時由於每次比較只是比較根節點和它的子節點,因此大大降低了比較的次數和交換的次數,從而提高效率。
1、原理
假設資料放在一個數組a中,且陣列的長度是N:
- 以陣列a為資料,建立一個大頂堆(這樣對於二叉樹的每個節點,根節點總是比子節點大,其實沒必要要求二叉樹的每個子樹也是大頂堆)
- 交換大頂堆的根節點和陣列a中的最後一個節點(最後一個節點不在參與後邊的工作)
- 重複上邊的工作,經過N-1次後,陣列a已經排好序。
2、程式碼:
public class Sort {
public static void main(String[] args){
int[] score = { 49,38,65,97,76,13,27,14,10 };
for (int i = score.length -1; i>0;i--){
buildHeap(score,i);//建堆
swap(score,0,i);//交換根節點和最後一個節點
System.out.print("第" + (score.length -i) + "次排序結果:");
for(int a = 0; a < score.length; a++){
System.out.print(score[a] + "\t");
}
System.out.println("");
}
System.out.print("最終的排序結果:");
for(int a = 0; a < score.length; a++){
System.out.print(score[a] + "\t");
}
}
private static void buildHeap(int[] elements,int lastIndex){
int lastParentIndex = (lastIndex-1)/2;//獲得最後一個父節點
for(int i = lastParentIndex; i >=0; i--){
int parent = elements[i];
int leftChild = elements[i*2+1];//左節點肯定存在
int rightChild = leftChild;
if(i*2+2 <=lastIndex){
rightChild = elements[i*2+2];//右節點不一定存在
}
int maxIndex = leftChild<rightChild?i*2+2:i*2+1;
if(parent < elements[maxIndex]){
swap(elements,i,maxIndex);
}
}
}
private static void swap(int[] elements,int firstIndex,int secondIndex){
int temp = elements[firstIndex];
elements[firstIndex] =elements[secondIndex];
elements[secondIndex] = temp;
}
}
執行結果:
第1次排序結果:10 49 65 38 76 13 27 14 97
第2次排序結果:14 10 65 38 49 13 27 76 97
第3次排序結果:27 49 14 38 10 13 65 76 97
第4次排序結果:13 27 14 38 10 49 65 76 97
第5次排序結果:10 13 14 27 38 49 65 76 97
第6次排序結果:13 10 14 27 38 49 65 76 97
第7次排序結果:13 10 14 27 38 49 65 76 97
第8次排序結果:10 13 14 27 38 49 65 76 97
最終的排序結果:10 13 14 27 38 49 65 76 97
3、效率:
時間複雜度為(O(nlogn),以2為底)。
五、直接插入排序
插入排序是一種通過不斷地把新元素插入到已排好序的資料中的排序演算法,常用的插入排序演算法包括直接插入排序和希爾(Shell)排序,直接插入排序實現比較簡單,但是直接插入沒有充分的利用已插入的資料已經排序這個事實,因此有很多針對直接插入排序改進的演算法。
1、原理
在要排序的一組數中,假設前面(n-1)[n>=2] 個數已經是排好順序的,現在要把第n個數插到前面的有序數中,使得這n個數也是排好順序的。如此反覆迴圈,直到全部排好順序。
也就是說,先從無序區拿第一個記錄出來,它是有序的,然後把無序區中的記錄一個一個插入到其中,那麼插入之後是有序的,所以直到最後都是有序的。
2、程式碼:
public class Sort {
public static void main(String[] args) {
int a[] = { 37, 38, 66, 97, 76, 13, 27, 49, 78, 34, 12, 64,5, 4, 62,
99, 98, 54, 56, 18, 17, 23, 34, 15, 35, 25, 53, 51 };
int temp = 0;
for (int i = 1; i < a.length; i++) {
int j = i - 1;
temp = a[i];
for (; j >= 0 && temp < a[j]; j--) {
a[j + 1] = a[j]; // 將大於temp的值整體後移一個單位
}
a[j + 1] = temp;
}
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "");
}
}
}
執行結果:
4 5 12 13 15 17 1823 25 27 34 34 35 37 38 49 51 53 54 56 62 64 66 76 78 97 98 99
3、效率:
時間複雜度是O(n)
六、希爾(Shell)排序
希爾排序(ShellSort)是插入排序的一種。是針對直接插入排序演算法的改進。該方法又稱縮小增量排序,因DL.Shell於1959年提出而得名。
1、原理
先取定一個小於n的整數d1作為第1個增量,把檔案的全部記錄分成d1個組,所有距離為d1的倍數的記錄放在同一個組中,在各組內進行直接插入排序;然後,取第2個增量d2<d1重複上述的分組和排序,直至所取的增量d1=1(dt<dt-1<…< d2<d1),即所有記錄放在同一組中進行直接插入排序為止。
2、程式碼:
public class Sort {
public static void main(String[] args) {
int a[] = { 37, 38, 66, 97, 76, 13, 27, 49, 78, 34, 12, 64,5, 4, 62,
99, 98, 54, 56, 18, 17, 23, 34, 15, 35, 25, 53, 51 };
double d1 = a.length;
int temp = 0;
while (true) {
d1 = Math.ceil(d1 / 2);//增量d1為n/2,n為要排序數的個數
int d = (int) d1;
for (int x = 0; x < d; x++) {
for (int i = x + d; i < a.length; i += d) {
int j = i - d;
temp = a[i];
for (; j >= 0 && temp < a[j]; j -= d) {
a[j + d] = a[j];
}
a[j + d] = temp;
}
}
if (d == 1)
break;
}
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+"");
}
}
}
執行結果:
4 5 12 13 15 17 1823 25 27 34 34 35 37 38 49 51 53 54 56 62 64 66 76 78 97 98 99
3、效率:
適用於排序小列表。
效率估計O(nlog2n)~O(n1.5),取決於增量值的最初大小。建議使用質數作為增量值,因為如果增量值是2的冪,則在下一個通道中會再次比較相同的元素。
希爾(Shell)排序改進了插入排序,減少了比較的次數。是不穩定的排序,因為排序過程中元素可能會前後跳躍。
七、歸併排序
歸併排序是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divideand Conquer)的一個非常典型的應用。
1、原理
歸併(Merge)排序法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分為若干個子序列,每個子序列是有序的。然後再把有序子序列合併為整體有序序列。
2、程式碼:
public class Sort {
public static void main(String[] args) {
int a[] = { 37, 38, 66, 97, 76, 13, 27, 49, 78, 34, 12, 64,5, 4, 62 };
sort(a,0,a.length-1);
for(int i=0;i<a.length;i++)
System.out.print(a[i]+" ");
}
public static void sort(int[] data, int left, int right) {
if(left<right){
//找出中間索引
intcenter=(left+right)/2;
//對左邊陣列進行遞迴
sort(data,left,center);
//對右邊陣列進行遞迴
sort(data,center+1,right);
//合併
merge(data,left,center,right);
}
}
public static void merge(int[] data, int left, int center, int right) {
int [] tmpArr=new int[data.length];
int mid=center+1;
//third記錄中間陣列的索引
int third=left;
int tmp=left;
while(left<=center&&mid<=right){
//從兩個陣列中取出最小的放入中間陣列
if(data[left]<=data[mid]){
tmpArr[third++]=data[left++];
}else{
tmpArr[third++]=data[mid++];
}
}
//剩餘部分依次放入中間陣列
while(mid<=right){
tmpArr[third++]=data[mid++];
}
while(left<=center){
tmpArr[third++]=data[left++];
}
//將中間陣列中的內容複製回原陣列
while(tmp<=right){
data[tmp]=tmpArr[tmp++];
}
System.out.println(Arrays.toString(data));
}
}
執行結果:
[37, 38, 66, 97, 76, 13, 27,49, 78, 34, 12, 64, 5, 4, 62]
[37, 38, 66, 97, 76, 13, 27,49, 78, 34, 12, 64, 5, 4, 62]
[37, 38, 66, 97, 76, 13, 27,49, 78, 34, 12, 64, 5, 4, 62]
[37, 38, 66, 97, 13, 76, 27,49, 78, 34, 12, 64, 5, 4, 62]
[37, 38, 66, 97, 13, 76, 27,49, 78, 34, 12, 64, 5, 4, 62]
[37, 38, 66, 97, 13, 27, 49,76, 78, 34, 12, 64, 5, 4, 62]
[13, 27, 37, 38, 49, 66, 76,97, 78, 34, 12, 64, 5, 4, 62]
[13, 27, 37, 38, 49, 66, 76,97, 34, 78, 12, 64, 5, 4, 62]
[13, 27, 37, 38, 49, 66, 76,97, 34, 78, 12, 64, 5, 4, 62]
[13, 27, 37, 38, 49, 66, 76,97, 12, 34, 64, 78, 5, 4, 62]
[13, 27, 37, 38, 49, 66, 76,97, 12, 34, 64, 78, 4, 5, 62]
[13, 27, 37, 38, 49, 66, 76,97, 12, 34, 64, 78, 4, 5, 62]
[13, 27, 37, 38, 49, 66, 76,97, 4, 5, 12, 34, 62, 64, 78]
[4, 5, 12, 13, 27, 34, 37, 38,49, 62, 64, 66, 76, 78, 97]
4 5 12 13 27 34 3738 49 62 64 66 76 78 97
3、效率:
時間複雜度為O(nlogn)這是該演算法中最好、最壞和平均的時間效能。
八、基數排序
基數排序(radixsort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucketsort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些"桶"中,藉以達到排序的作用。
1、原理
將所有待比較數值(正整數)統一為同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。
2、程式碼:
public class Sort {
public static void main(String[] args) {
int a[] = { 37, 38, 66, 97, 76, 13, 27, 49, 78, 34, 12, 64,5, 4, 62 };
sort(a);
for (int i = 0; i < a.length; i++)
System.out.print(a[i] + "");
}
public static void sort(int[] array) {
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
int time = 0;
// 判斷位數;
while (max > 0) {
max /= 10;
time++;
}
// 建立10個佇列;
List<ArrayList> queue = new ArrayList<ArrayList>();
for (int i = 0; i < 10; i++) {
ArrayList<Integer> queue1 = newArrayList<Integer>();
queue.add(queue1);
}
// 進行time次分配和收集;
for (int i = 0; i < time; i++) {
// 分配陣列元素;
for (int j = 0; j < array.length; j++) {
// 得到數字的第time+1位數;
int x = array[j] % (int) Math.pow(10, i + 1)
/ (int) Math.pow(10, i);
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(array[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);
array[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
}
執行結果:
4 5 12 13 27 34 3738 49 62 64 66 76 78 97
3、效率:
基數排序法是屬於穩定性的排序,其時間複雜度為O(nlog(r)m),其中r為所採取的基數,而m為堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。
總結
1、各種排序的穩定性,時間複雜度和空間複雜度總結
見下圖:
2、時間複雜度說明
當原表有序或基本有序時,直接插入排序和氣泡排序將大大減少比較次數和移動記錄的次數,時間複雜度可降至O(n);
而快速排序則相反,當原表基本有序時,將蛻化為氣泡排序,時間複雜度提高為O(n2);
原表是否有序,對簡單選擇排序、堆排序、歸併排序和基數排序的時間複雜度影響不大。
3、選擇排序演算法的依據
影響排序的因素有很多,平均時間複雜度低的演算法並不一定就是最優的。相反,有時平均時間複雜度高的演算法可能更適合某些特殊情況。同時,選擇演算法時還得考慮它的可讀性,以利於軟體的維護。一般而言,需要考慮的因素有以下四點:
- 待排序的記錄數目n的大小;
- 記錄本身資料量的大小,也就是記錄中除關鍵字外的其他資訊量的大小;
- 關鍵字的結構及其分佈情況;
- 對排序穩定性的要求。
設待排序元素的個數為n.
1)當n較大,則應採用時間複雜度為O(nlog2n)的排序方法:快速排序、堆排序或歸併排序序。
快速排序:是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;
堆排序:如果記憶體空間允許且要求穩定性的,
歸併排序:它有一定數量的資料移動,所以我們可能過與插入排序組合,先獲得一定長度的序列,然後再合併,在效率上將有所提高。
2)當n較大,記憶體空間允許,且要求穩定 → 歸併排序
3)當n較小,可採用直接插入或直接選擇排序。
直接插入排序:當元素分佈有序,直接插入排序將大大減少比較次數和移動記錄的次數。
直接選擇排序 :元素分佈有序,如果不要求穩定性,選擇直接選擇排序
5)一般不使用或不直接使用傳統的氣泡排序。
6)基數排序
它是一種穩定的排序演算法,但有一定的侷限性:
1、關鍵字可分解。
2、記錄的關鍵字位數較少,如果密集更好
3、如果是數字時,最好是無符號的,否則將增加相應的映射覆雜度,可先將其正負分開排序。