面試題: java中常見的排序演算法的實現及比較
目錄
1.氣泡排序
1.1 氣泡排序普通版
每次冒泡過程都是從數列的第一個元素開始,然後依次和剩餘的元素進行比較,若小於相鄰元素,則交換兩者位置,同時將較大元素作為下一個比較的基準元素,繼續將該元素與其相鄰的元素進行比較,直到數列的最後一個元素 . 示意圖如下:
/** * 氣泡排序: * 依次比較相鄰的元素,若發現逆順序,則交換。小的向前換,大的向後換, * 本次迴圈完畢之後再次從頭開始掃描,直到某次掃描中沒有元素交換, * 說明每個元素都不比它後面的元素大,至此排序完成。 */ import java.util.Arrays; public class BubbleSort { public static void main(String[] args) { int[] arr = new int[]{9, 2, 1, 0, 5, 3, 6, 4, 8, 7}; System.out.println("排序前:" + Arrays.toString(arr)); sort(arr); System.out.println("排序後:" + Arrays.toString(arr)); } public static void sort(int[] arr) { for (int i = 1; i < arr.length; i++) { //第一層for迴圈,用來控制冒泡的次數 for (int j = 0; j < arr.length - 1; j++) { //第二層for迴圈,用來控制冒泡一層層到最後 //如果前一個數比後一個數大,兩者調換 ,意味著泡泡向上走了一層 if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } }
執行結果:
排序前:[9, 2, 1, 0, 5, 3, 6, 4, 8, 7]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1.2 冒泡排序升級版
在這個版本中,改動了兩點 . 第一點是加入了一個布林值,判斷第二層迴圈中的調換有沒有執行,如果沒有進行兩兩調換,說明後面都已經排好序了,已經不需要再迴圈了,直接跳出迴圈,排序結束 ; 第二點是第二層迴圈不再迴圈到arr.length - 1,因為外面的i迴圈遞增一次,說明陣列最後就多了一個排好序的大泡泡.第二層迴圈也就不需要到最末尾一位了,可以提前結束迴圈
/** * 升級版氣泡排序: * 加入一個布林變數,如果內迴圈沒有交換值,說明已經排序完成,提前終止 */ import java.util.Arrays; public class BubbleSort { public static void main(String[] args) { int[] arr = new int[]{9, 2, 1, 0, 5, 3, 6, 4, 8, 7}; System.out.println("排序前:" + Arrays.toString(arr)); plusSort(arr); System.out.println("排序後:" + Arrays.toString(arr)); } public static void plusSort(int[] arr){ if(arr != null && arr.length > 1){ for(int i = 0; i < arr.length - 1; i++){ // 初始化一個布林值 boolean flag = true; for(int j = 0; j < arr.length - i - 1 ; j++){ if(arr[j] > arr[j+1]){ // 調換 int temp; temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; // 改變flag flag = false; } } if(flag){ break; } } } } }
執行結果:
排序前:[9, 2, 1, 0, 5, 3, 6, 4, 8, 7]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2.選擇排序
選擇排序也是一種簡單直觀的排序演算法,實現原理比較直觀易懂:首先在未排序數列中找到最小元素,然後將其與數列的首部元素進行交換,然後,在剩餘未排序元素中繼續找出最小元素,將其與已排序數列的末尾位置元素交換。以此類推,直至所有元素圴排序完畢
/** * 選擇排序: * 每一次從待排序的資料元素中選出最小(或最大)的一個元素, * 存放在序列的起始位置,直到全部待排序的資料元素排完。 */ import java.util.Arrays; public class SelectSort { public static void main(String[] args) { int[] arr = new int[] {3,4,5,7,1,2,0,9,3,6,8}; System.out.println("排序前:"+Arrays.toString(arr)); selectSort(arr); System.out.println("排序後:"+Arrays.toString(arr)); } public static void selectSort(int[] arr) { for(int i=0;i<arr.length;i++) { int minIndex=i; for(int j=i+1;j<arr.length;j++) { if(arr[minIndex]>arr[j]) { minIndex=j; } } if(i!=minIndex) { int temp=arr[i]; arr[i]=arr[minIndex]; arr[minIndex]=temp; } } } }
執行結果:
排序前:[3, 4, 5, 7, 1, 2, 0, 9, 3, 6, 8]
排序後:[0, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9]
3.插入排序
一次插入排序的操作過程:將待插元素,依次與已排序好的子數列元素從後到前進行比較,如果當前元素值比待插元素值大,則將移位到與其相鄰的後一個位置,否則直接將待插元素插入當前元素相鄰的後一位置,因為說明已經找到插入點的最終位置
/**
* 插入排序:
* 從第一個元素開始,該元素可以認為已經被排序
* 取出下一個元素,在已經排序的元素序列中從後向前掃描
* 如果該元素(已排序)大於新元素,將該元素移到下一位置
* 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
* 將新元素插入到該位置後
* 重複上面步驟
*/
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
System.out.println("排序前:"+Arrays.toString(arr));
insertSort(arr);
System.out.println("排序後:"+Arrays.toString(arr));
}
public static void insertSort(int[] arr) {
for(int i=1;i<arr.length;i++) {
if(arr[i]<arr[i-1]) {
int temp=arr[i];
int j;
for(j=i-1;j>=0&&temp<arr[j];j--)
arr[j+1]=arr[j];
arr[j+1]=temp;
}
}
}
}
執行結果:
排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
4.快速排序
快速排序演算法利用的是一趟快速排序,基本內容是選擇一個數作為準基數,然後利用這個準基數將遺傳資料分為兩個部分,第一部分比這個準基數小,都放在準基數的左邊,第二部分都比這個準基數大,放在準基數的右邊.
import java.util.Arrays;
/**
* 快速排序:
* 快速排序演算法利用的是一趟快速排序,基本內容是選擇一個數作為準基數,
* 然後利用這個準基數將遺傳資料分為兩個部分,第一部分比這個準基數小,
* 都放在準基數的左邊,第二部分都比這個準基數大,放在準基數的右邊.
*/
public class QuickSort {
public static void main(String[] args) {
int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
System.out.println("排序前:"+ Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println("排序後:"+Arrays.toString(arr));
}
public static void quickSort(int[] arr,int begin,int end) {
//先定義兩個引數接收排序起始值和結束值
int a = begin;
int b = end;
//先判斷a是否大於b
if (a >= b) {
//沒必要排序
return;
}
//基準數,預設設定為第一個值
int x = arr[a];
//迴圈
while (a < b) {
//從後往前找,找到一個比基準數x小的值,賦給arr[a]
//如果a和b的邏輯正確--a<b ,並且最後一個值arr[b]>x,就一直往下找,直到找到後面的值大於x
while (a < b && arr[b] >= x) {
b--;
}
//跳出迴圈,兩種情況,一是a和b的邏輯不對了,a>=b,這時候排序結束.二是在後面找到了比x小的值
if (a < b) {
//將這時候找到的arr[b]放到最前面arr[a]
arr[a] = arr[b];
//排序的起始位置後移一位
a++;
}
//從前往後找,找到一個比基準數x大的值,放在最後面arr[b]
while (a < b && arr[a] <= x) {
a++;
}
if (a < b) {
arr[b] = arr[a];
//排序的終止位置前移一位
b--;
}
}
//跳出迴圈 a < b的邏輯不成立了,a==b重合了,此時將x賦值回去arr[a]
arr[a] = x;
//呼叫遞迴函式,再細分再排序
quickSort(arr,begin,a-1);
quickSort(arr,a+1,end);
}
}
執行結果:
排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
5.歸併排序
歸併排序,簡單的說把一串數,從中平等分為兩份,再把兩份再細分,直到不能細分為止,這就是分而治之的分的步驟. 再從最小的單元,兩兩合併,合併的規則是將其按從小到大的順序放到一個臨時陣列中,再把這個臨時陣列替換原陣列相應位置
import java.util.Arrays;
/**
* 歸併排序:
* 歸併操作的工作原理如下:
* 第一步:申請空間,使其大小為兩個已經 排序序列之和,該空間用來存放合併後的序列
* 第二步:設定兩個 指標,最初位置分別為兩個已經排序序列的起始位置
* 第三步:比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置重複步驟3直到某一指標超出序列尾
* 將另一序列剩下的所有元素直接複製到合併序列尾
*
*/
public class MergeSort {
public static void main(String[] args) {
int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
System.out.println("排序前:"+ Arrays.toString(arr));
mergeSort(arr, 0, arr.length-1);
System.out.println("排序後:"+ Arrays.toString(arr));
}
public static void mergeSort(int[] a,int s,int e){
int m = (s + e) / 2;
if (s < e){
mergeSort(a,s,m);
mergeSort(a,m+1,e);
//歸併
merge(a,s,m,e);
}
}
private static void merge(int[] a, int s, int m, int e) {
//初始化一個從起始s到終止e的一個數組
int[] temp = new int[(e - s) + 1];
//左起始指標
int l = s;
//右起始指標
int r = m+1;
int i = 0;
//將s-e這段資料在邏輯上一分為二,l-m為一個左邊的陣列,r-e為一個右邊的陣列,兩邊都是有序的
//從兩邊的第一個指標開始遍歷,將其中小的那個值放在temp陣列中
while (l <= m && r <= e){
if (a[l] < a[r]){
temp[i++] = a[l++];
}else{
temp[i++] = a[r++];
}
}
//將兩個陣列剩餘的數放到temp中
while (l <= m){
temp[i++] = a[l++];
}
while (r <= e){
temp[i++] = a[r++];
}
//將temp陣列覆蓋原陣列
for (int n = 0; n < temp.length; n++) {
a[s+n] = temp[n];
}
}
}
執行結果:
排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
6.希爾排序
**希爾排序(Shell’s Sort)是插入排序的一種又稱“縮小增量排序”。希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序演算法排序;隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止 . 希爾排序實質上是一種分組插入的方法 . **
/**
* 希爾排序:
* 希爾排序(Shell’s Sort)是插入排序的一種又稱“縮小增量排序”。
* 希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序演算法排序;
* 隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止
*/
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
System.out.println("排序前:"+Arrays.toString(arr));
shellSort(arr);
System.out.println("排序後:"+Arrays.toString(arr));
}
public static void shellSort(int[] arr) {
int k = 1;
for (int d = arr.length / 2; d > 0; d /= 2) {
for (int i = d; i < arr.length; i++) {
for (int j = i - d; j >= 0; j -= d) {
if (arr[j] > arr[j + d]) {
int temp = arr[j];
arr[j] = arr[j + d];
arr[j + d] = temp;
}
}
}
System.out.println( Arrays.toString(arr));
k++;
}
}
}
執行結果:
排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
[5, 1, 0, 7, 4, 9, 3, 2, 8, 6]
[0, 1, 3, 2, 4, 6, 5, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
7.基數排序
基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先順序排序。最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。
/**
* 基數排序:
* 取得陣列中的最大數,並取得位數;
* arr為原始陣列,從最低位開始取每個位組成radix陣列;
* 對radix進行計數排序(利用計數排序適用於小範圍數的特點);
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class RadixSort {
public static void main(String[] args) {
int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
System.out.println("排序前:"+ Arrays.toString(arr));
radixSort(arr);
System.out.println("排序後:"+Arrays.toString(arr));
}
/**
* 基數排序
*/
public static void radixSort(int[] a) {
int max = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
int time = 0;
while (max > 0) {
max /= 10;
time++;
}
List<ArrayList<Integer>> queue = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++) {
ArrayList<Integer> queue1 = new ArrayList<Integer>();
queue.add(queue1);
}
for (int i = 0; i < time; i++) {
for (int j = 0; j < a.length; j++) {
int x = a[j] % (int) Math.pow(10, i+1)/(int)Math.pow(10, i);
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(a[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);
a[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
}
執行結果:
排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
8.堆排序
堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點
/**
* 堆排序
* 將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆為初始的無序區;
* 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
* 由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調整為新堆,
* 然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。
* 不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成。
*/
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] arr = new int[] {5,3,2,8,4,9,1,0,7,6};
System.out.println("排序前:"+ Arrays.toString(arr));
heapSort(arr);
System.out.println("排序後:"+Arrays.toString(arr));
}
/**
* 堆排序
*/
public static void heapSort(int[] a) {
int len = a.length;
for (int i = 0; i < len - 1; i++) {
buildMaxHeap(a,len - 1 - i);
swap(a,0,len - 1 - i);
}
}
private static void buildMaxHeap(int[] data, int lastIndex) {
//從lastIndex處節點(最後一個節點)的父節點開始
for (int i = (lastIndex - 1)/2; i >= 0; i--) {
//k儲存正在判斷的節點
int k = i ;
//如果當前K節點的子節點存在
while(k * 2 + 1 <= lastIndex) {
//k節點的左子節點的索引
int biggerIndex = 2 * k +1;
//如果biggerIndex小於lastIndex,即biggerIndex +1代表的K節點的右子節點存在
if(biggerIndex < lastIndex) {
//如果右子節點的值較大
if(data[biggerIndex] < data[biggerIndex + 1]) {
biggerIndex++;
}
}
//如果K節點的值小於其較大的子節點的值
if(data[k] < data[biggerIndex]) {
//交換他們
swap(data, k, biggerIndex);
k = biggerIndex;
} else {
break;
}
}
}
}
private static void swap(int[] a, int i, int j) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
執行結果
排序前:[5, 3, 2, 8, 4, 9, 1, 0, 7, 6]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
各種演算法的比較
排序法 | 平均時間 | 最小時間 | 最大時間 | 穩定度 | 額外空間 | 備註 |
---|---|---|---|---|---|---|
氣泡排序 | O(n2) | O(n) | O(n2) | 穩定 | O(1) | n小時較好 |
選擇排序 | O(n2) | O(n2) | O(n2) | 不穩定 | O(1) | n小時較好 |
插入排序 | O(n2) | O(n) | O(n2) | 穩定 | O(1) | 大部分已排序時較好 |
基數排序 | O(logRB) | O(n) | O(logRB) | 穩定 | O(n) | B是真數(0-9),R是基數(個十百) |
Shell排序 | O(nlogn) | - | O(ns) 1<s<2 | 不穩定 | O(1) | s是所選分組 |
快速排序 | O(nlogn) | O(n2) | O(n2) | 不穩定 | O(logn) | n大時較好 |
歸併排序 | O(nlogn) | O(nlogn) | O(nlogn) | 穩定 | O(n) | 要求穩定性時較好 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | 不穩定 | O(1) | n大時較好 |
速度: 快速排序>>歸併排序>>>>>插入排序>>選擇排序>>氣泡排序
並且可以看到,選擇排序,氣泡排序在資料量越來越大的情況下,耗時已經呈指數型上漲,而不是倍數上漲(1)若n較小(如n≤50),可採用直接插入或直接選擇排序。
當記錄規模較小時,直接插入排序較好;否則因為直接選擇移動的記錄數少於直接插人,應選直接選擇排序為宜。
(2)若檔案初始狀態基本有序(指正序),則應選用直接插人、冒泡或隨機的快速排序為宜;
(3)若n較大,則應採用時間複雜度為O(nlgn)的排序方法:快速排序、堆排序或歸併排序。
快速排序是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;
堆排序所需的輔助空間少於快速排序,並且不會出現快速排序可能出現的最壞情況。這兩種排序都是不穩定的。
若要求排序穩定,則可選用歸併排序。