八種經典排序演算法和java實現
文章目錄
演算法概述
演算法分類
十種常見排序演算法可以分為兩大類:
非線性時間比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此稱為非線性時間比較類排序。
線性時間非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間執行,因此稱為線性時間非比較類排序。
演算法複雜度
相關概念
穩定: 如果a原本在b前面,而a=b,排序之後a仍然在b的前面。
不穩定: 如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面。
時間複雜度: 對排序資料的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
空間複雜度: 是指演算法在計算機內執行時所需儲存空間的度量,它也是資料規模n的函式。
這裡我們介紹
1.氣泡排序(Bubble Sort)
2.選擇排序(Selection Sort)
3.插入排序(Insertion Sort)
4.希爾排序(Shell Sort)
5.歸併排序(Merge Sort)
6.快速排序(Quick Sort)
7.堆排序(Heap Sort)
8.桶排序(Bucket Sort)
9.基數排序(Radix Sort)
1.氣泡排序(Bubble Sort)
氣泡排序是一種簡單的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。
演算法描述
- 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
- 針對所有的元素重複以上的步驟,除了最後一個;
- 重複步驟1~3,直到排序完成。
動圖演示
程式碼實現
void bubbleSort(int arr[]) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length -1-i; j++) {
if (arr[j] > arr[j + 1]) { // 相鄰元素兩兩對比
int temp; // 元素交換
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
2.選擇排序(Selection Sort)
選擇排序(Selection-sort)是一種簡單直觀的排序演算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
演算法描述
n個記錄的直接選擇排序可經過n-1趟直接選擇排序得到有序結果。具體演算法描述如下:
- 初始狀態:無序區為R[1…n],有序區為空;
- 第i趟排序(i=1,2,3…n-1)開始時,當前有序區和無序區分別為R[1…i-1]和R(i…n)。- 該趟排序從當前無序區中-選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1…i]和R[i+1…n)分別變為記錄個數增加1個的新有序區和記錄個數減少1個的新無序區;
- n-1趟結束,陣列有序化了。
動圖演示
程式碼實現
void selectionSort(int arr[]) {
int len = arr.length;
int minIndex, temp;
for (int i = 0; i < len - 1; i++) {
minIndex = i;
for (int j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { // 尋找最小的數
minIndex = j; // 將最小數的索引儲存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
演算法分析
表現最穩定的排序演算法之一,因為無論什麼資料進去都是O(n2)的時間複雜度,所以用到它的時候,資料規模越小越好。唯一的好處可能就是不佔用額外的記憶體空間了吧。理論上講,選擇排序可能也是平時排序一般人想到的最多的排序方法了吧。
3.插入排序(Insertion Sort)
插入排序(Insertion-Sort)的演算法描述是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。
演算法描述
一般來說,插入排序都採用in-place在陣列上實現。具體演算法描述如下:
- 從第一個元素開始,該元素可以認為已經被排序;
- 取出下一個元素,在已經排序的元素序列中從後向前掃描;
- 如果該元素(已排序)大於新元素,將該元素移到下一位置;
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
- 將新元素插入到該位置後;
- 重複步驟2~5。
動圖演示
程式碼實現
void insertionSort(int[] arr) {
int len = arr.length;
int preIndex, current;
for (int i = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
}
演算法分析
插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。
4.希爾排序(Shell Sort)
希爾排序,也稱遞減增量排序演算法,是插入排序的一種更高效的改進版本。希爾排序是非穩定排序演算法。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的資料操作時,效率高,即可以達到線性排序的效率
- 但插入排序一般來說是低效的,因為插入排序每次只能將資料移動一位
希爾排序通過將比較的全部元素分為幾個區域來提升插入排序的效能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步。然後演算法再取越來越小的步長進行排序,演算法的最後一步就是普通的插入排序,但是到了這步,需排序的資料幾乎是已排好的了(此時插入排序較快)。
演算法描述
先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,具體演算法描述:
- 先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,
- 然後依次縮減增量再進行排序,
- 待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。
步長的選擇是希爾排序的重要部分,這裡不做過多說明。發個圖理解理解
原理演示
例如,假設有這樣一組數[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我們以步長為5開始進行排序,我們可以通過將這列表放在有5列的表中來更好地描述演算法,這樣他們就應該看起來是這樣:
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然後我們對每列進行排序:
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
將上述四行數字,依序接在一起時我們得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].這時10已經移至正確位置了,然後再以3為步長進行排序:
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之後變為:
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最後以1步長進行排序(此時就是簡單的插入排序了)。
程式碼實現
void shellSort(int[] array) {
int number = array.length / 2;
int preIndex;
int current;
while (number >= 1) {
for (int i = number; i < array.length; i++) {
current = array[i];
preIndex = i - number;
while (preIndex >= 0 && array[preIndex] > current) {
array[preIndex + number] = array[preIndex];
preIndex = preIndex - number;
}
array[preIndex + number] = current;
}
number = number / 2;
}
}
演算法分析
希爾排序的核心在於間隔序列的設定。既可以提前設定好間隔序列,也可以動態的定義間隔序列。動態定義間隔序列的演算法是《演算法(第4版)》的合著者Robert Sedgewick提出的。
5.歸併排序(Merge Sort)
歸併排序是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為2-路歸併。
演算法描述
- 把長度為n的輸入序列分成兩個長度為n/2的子序列;
- 對這兩個子序列分別採用歸併排序;
- 將兩個排序好的子序列合併成一個最終的排序序列。
動圖演示
程式碼實現
//將有二個有序數列a[first...mid]和a[mid...last]合併。
void merge(int a[], int low, int mid, int high)
{
int i = low, j = mid + 1;
int m = mid, n = high;
int k = 0;
int[] temp=new int[high-low+1];
while (i <= m && j <= n)
{
if (a[i] <= a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <= m)
temp[k++] = a[i++];
while (j <= n)
temp[k++] = a[j++];
for (i = 0; i < k; i++)
a[low + i] = temp[i];
}
void mergeSort(int a[], int low, int high)
{
int mid = (low + high) / 2;
if (low < high) {
// 左邊
mergeSort(a, low, mid);
// 右邊
mergeSort(a, mid + 1, high);
// 左右歸併
merge(a, low, mid, high);
}
}
演算法分析
歸併排序是一種穩定的排序方法。和選擇排序一樣,歸併排序的效能不受輸入資料的影響,但表現比選擇排序好的多,因為始終都是O(nlogn)的時間複雜度。代價是需要額外的記憶體空間。
6.快速排序(Quick Sort)
快速排序是由東尼·霍爾所發展的一種排序演算法。在平均狀況下,排序 n 個專案要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(n log n) 演算法更快,因為它的內部迴圈(inner loop)可以在大部分的架構上很有效率地被實現出來。
演算法描述
快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分為兩個子序列(sub-lists)。
- 從數列中挑出一個元素,稱為 “基準”(pivot);
- 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作;
- 遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
動圖演示
程式碼實現
void quickSort(int[] arr, int head, int tail) {
if (head >= tail || arr == null || arr.length <= 1) {
return;
}
int i = head, j = tail, pivot = arr[(head + tail) / 2];
while (i <= j) {
while (arr[i] < pivot) {
++i;
}
while (arr[j] > pivot) {
--j;
}
if (i < j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
++i;
--j;
} else if (i == j) {
++i;
}
}
quickSort(arr, head, j);
quickSort(arr, i, tail);
}
7.堆排序(Heap Sort)
堆排序(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,則整個排序過程完成。
動圖演示
程式碼實現
/**
* 調整索引為 index 處的資料,使其符合堆的特性。
*
* @param index 需要堆化處理的資料的索引
* @param len 未排序的堆(陣列)的長度
*/
void maxHeapify(int[] arr, int index,int len){
int li = (index << 1) + 1; // 左子節點索引
int ri = li + 1; // 右子節點索引
int cMax = li; // 子節點值最大索引,預設左子節點。
if(li > len) return; // 左子節點索引超出計算範圍,直接返回。
if(ri <= len && arr[ri] > arr[li]) // 先判斷左右子節點,哪個較大。
cMax = ri;
if(arr[cMax] > arr[index]){
swap(arr,cMax, index); // 如果父節點被子節點調換,
maxHeapify(arr,cMax, len); // 則需要繼續判斷換下後的父節點是否符合堆的特性。
}
}
void swap(int[] arr,int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
void heapSort(int[] arr) {
/*
* 第一步:將陣列堆化
* beginIndex = 第一個非葉子節點。
* 從第一個非葉子節點開始即可。無需從最後一個葉子節點開始。
* 葉子節點可以看作已符合堆要求的節點,根節點就是它自己且自己以下值為最大。
*/
int len = arr.length - 1;
int beginIndex = (len - 1) >> 1;
for(int i = beginIndex; i >= 0; i--){
maxHeapify(arr, i,len);
}
/*
* 第二步:對堆化資料排序
* 每次都是移出最頂層的根節點A[0],與最尾部節點位置調換,同時遍歷長度 - 1。
* 然後從新整理被換到根節點的末尾元素,使其符合堆的特性。
* 直至未排序的堆長度為 0。
*/
for(int i = len; i > 0; i--){
swap(arr, 0, i);
len--;
maxHeapify(arr, 0,len);
}
}
8.桶排序(Bucket Sort)
有限個數字m,每個數字的大小都在1與n之間,則我們可以假設有n個桶,遍歷m個數字,將其存入對應的桶中(如數字的值為3,就存入3號桶,桶的值對應存入數字的個數)
演算法描述
桶排序以下列程式進行:
1.設定一個定量的陣列當作空桶子。
2.尋訪序列,並且把專案一個一個放到對應的桶子去。
3.對每個不是空的桶子進行排序。
4.從不是空的桶子裡把專案再放回原來的序列中。
演示
程式碼實現
void bucketSort(int[] arr){
if (arr==null||arr.length<2){
return;
}
//常用寫法
int max = Integer.MIN_VALUE;
for (int i =0;i<arr.length;i++){
max = Math.max(max,arr[i]);
}
int[] bucket = new int[max+1];
for (int i =0;i<arr.length;i++){
//桶陣列此下標有資料,數值就加一
bucket[arr[i]]++;
}
int i = 0;
for (int j = 0;j<bucket.length;j++){
while (bucket[j]-->0){
arr[i++]=j;
}
}
}
演算法分析
如果我們的數字波動範圍非常大,比如1到10000,那麼我們需要一個10000元素陣列的空間開銷,而且在倒出數字的時候需要遍歷10000個桶,這樣效率是非常低的,於是我們有了基於桶式排序的基數排序
9.基數排序(Radix Sort)
將所有待比較數值(正整數)統一為同樣的數字長度,數字較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。
演算法描述
- 取得陣列中的最大數,並取得位數;
- arr為原始陣列,從最低位開始取每個位組成radix陣列;
- 對radix進行計數排序(利用計數排序適用於小範圍數的特點);
動圖演示
程式碼實現
void radixSort(int[] arr, int d) //d表示最大的數有多少位
{
int k = 0;
int n = 1;
int m = 1; //控制鍵值排序依據在哪一位
int[][]temp = new int[10][arr.length]; //陣列的第一維表示可能的餘數0-9
int[]order = new int[10]; //陣列order[i]用來表示該位是i的數的個數
while(m <= d)
{
for(int i = 0; i < arr.length; i++) {
int lsd = ((arr[i] / n) % 10);
temp[lsd][order[lsd]] = arr[i];
order[lsd]++;
}
for(int i = 0; i < 10; i++) {
if(order[i] != 0)