java八大經典排序演算法
當n較大,則應採用時間複雜度為O(nlog2n)的排序方法:快速排序、堆排序或歸併排序序。快速排序:是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短。
直接插入
基本思想
直接插入排序是將未排序的資料插入至已排好序序列的合適位置。
具體流程如下:
1、首先比較陣列的前兩個資料,並排序;
2、比較第三個元素與前兩個排好序的資料,並將第三個元素放入適當的位置;
3、比較第四個元素與前三個排好序的資料,並將第四個元素放入適當的位置;
......
4、直至把最後一個元素放入適當的位置。
例子
{4,5,1,2,8,6,7,3,10,9}
取無序區間的第一個,從右向左掃描有序區間比較,方括號內可視為有序區間。
第二次:[4,5],1,2,8,6,7,3,10,9
第三次:[1,4,5],2,8,6,7,3,10,9
第四次:[1,2,4,5],8,6,7,3,10,9
第五次:[1,2,4,5,8],6,7,3,10,9
第六次:[1,2,4,5,6,8],7,3,10,9
第七次:[1,2,4,5,6,7,8],3,10,9
第八次:[1,2,3,4,5,6,7,8],10,9
第九次:[1,2,3,4,5,6,7,8,10],9
第十次:[1,2,3,4,5,6,7,8,9,10]
演算法分析
直接插入排序演算法的空間複雜度為O(1)。
最好的情況,要比較的無序序列原本就是順序有序的,那麼要比較的次數是n-1,移動了0次,時間複雜度O(n)。
最壞的情況,要比較的無序序列原本就是逆序有序的,那麼要比較的次數是(n+2)(n-1)/2,移動的次數(n+4)(n-1)/2,時間複雜度O(n²)。
直接插入排序的平均複雜度為O(n²)。
直接插入排序是穩定的。
Java實現
/**
* 直接插入排序
* 時間複雜度:O(n^2)
*@param data
*/
public static void insertSort(int[] data){
int temp;
for (int i = 1; i < data.length; i++) {
temp = data[i];//待插入資料
int j;
for(j = i - 1; j >= 0; j--) {
//判斷是否大於temp,大於則後移一位
if(data[j] > temp) {
data[j+1] = data[j];
}else{
break;
}
}
data[j + 1] = temp;
}
}
希爾排序
基本思想
希爾排序嚴格來說是基於插入排序的思想,又被稱為縮小增量排序。
具體流程如下:
1、將包含n個元素的陣列,分成n/2個數組序列,第一個資料和第n/2+1個數據為一對...
2、對每對資料進行比較和交換,排好順序;
3、然後分成n/4個數組序列,再次排序;
4、不斷重複以上過程,隨著序列減少並直至為1,排序完成。
假如有初始資料:25 11 45 26 12 78。
1、第一輪排序,將該陣列分成 6/2=3 個數組序列,第1個數據和第4個數據為一對,第2個數據和第5個數據為一對,第3個數據和第6個數據為一對,每對資料進行比較排序,排序後順序為:[25, 11, 45, 26,12, 78]。
2、第二輪排序,將上輪排序後的陣列分成6/4=1個數組序列,此時逐個對資料比較,按照插入排序對該陣列進行排序,排序後的順序為:[11, 12, 25, 26, 45, 78]。
演算法分析
對於插入排序而言,如果原陣列是基本有序的,那排序效率就可大大提高。另外,對於數量較小的序列使用直接插入排序,會因需要移動的資料量少,其效率也會提高。因此,希爾排序具有較高的執行效率。
希爾排序並不穩定,O(1)的額外空間,時間複雜度為O(n²)。
Java實現
/**
* 希爾(shell)演算法
* 時間複雜度:O(n^2)
*@param data
*/
public static voidshellSortSmallToBig(int[] data) {
int j = 0;
int temp = 0;
for (int increment = data.length / 2; increment > 0; increment /= 2){
for (int i = increment; i < data.length; i++) {
temp = data[i];
for (j = i - increment; j >= 0; j -= increment) {
if (temp < data[j]) {
data[j + increment] =data[j];
} else {
break;
}
}
data[j + increment] = temp;
}
}
}
簡單選擇
基本思想
選擇排序是一種簡單直觀的排序演算法,其基本原理如下:對於給定的一組記錄,經過第一輪比較後得到最小的記錄,然後將該記錄的位置與第一個記錄的位置交換;接著對不包括第一個記錄以外的其他記錄進行第二次比較,得到最小記錄並與第二個位置記錄交換;重複該過程,知道進行比較的記錄只剩下一個為止。
演算法分析
時間複雜度:假設有n個數據,資料交換的次數最多為n-1次,但程式的總體的比較次數較多。所以綜合考慮有直接選擇排序的時間複雜度為O(n2)(n的平方)。所以當記錄佔用位元組數較多時,通常比直接插入排序的執行速度快些。
空間複雜度:直接選擇排序的空間複雜度很好,它只需要一個附加單元用於資料交換,所以其空間複雜度為O(1)。
穩定性:由於在直接選擇排序中存在著不相鄰元素之間的互換,因此,直接選擇排序是一種不穩定的排序方法。以陣列{49,38,65,97,76,13,27,49}為例,
Java實現
/**
* 簡單選擇排序
* 時間複雜度:O(n^2)
*@param data
*/
public static void selectSort(int[] data) {
for (int i = 0; i < data.length; i++) {
int temp = data[i];
int flag = i; //將當前下標定義為最小值下標
for (int j = i + 1; j < data.length; j++) {
if (data[j] < temp) { //a[j] < temp 從小到大排序;a[j] >temp 從大到小排序
temp = data[j];
flag = j; //如果有小於當前最小值的關鍵字將此關鍵字的下標賦值給flag
}
}
if (flag != i) {
data[flag] = data[i];
data[i] = temp;
}
}
}
堆排序
基本思想
堆是一種特殊的樹形資料結構,其每個節點都有一個值,通常提到的堆都是指一顆完全二叉樹,根結點的值小於(或大於)兩個子節點的值,同時,根節點的兩個子樹也分別是一個堆。
堆排序就是利用堆(假設利用大頂堆)進行排序的方法。它的基本思想是,將待排序的序列構造成一個大頂堆。此時,整個序列的最大值就是堆頂的根節點。將它移走(其實就是將其與堆陣列的末尾元素交換,此時末尾元素就是最大值),然後將剩餘的 n-1 個序列重新構造成一個堆,這樣就會得到 n 個元素中次大的值。如此反覆執行,便能得到一個有序序列了。
堆排序的實現需要解決的兩個關鍵問題:
(1)將一個無序序列構成一個堆。
(2)輸出堆頂元素後,調整剩餘元素成為一個新堆。
大根堆排序演算法的基本操作
① 初始化操作:將R[1..n]構造為初始堆;
②每一趟排序的基本操作:將當前無序區的堆頂記錄R[1]和該區間的最後一個記錄交換,然後將新的無序區調整為堆(亦稱重建堆)。
注意:
①只需做n-1趟排序,選出較大的n-1個關鍵字即可以使得檔案遞增有序。
②用小根堆排序與利用大根堆類似,只不過其排序結果是遞減有序的。堆排序和直接選擇排序相反:在任何時刻堆排序中無序區總是在有序區之前,且有序區是在原向量的尾部由後往前逐步擴大至整個向量為止。
演算法分析
堆排序的執行時間主要耗費在初始構建堆和在重建堆時反覆篩選上。在構建對的過程中,因為我們是完全二叉樹從最下層最右邊的非終端節點開始構建,將它與其孩子進行比較和若有必要的互換,對每個非終端節點來說,其實最多進行兩次比較和互換操作,因此整個構建堆的時間複雜度為O(n)。
在正式排序時,第i次取堆頂記錄重建堆需要用O(logi)的時間(完全二叉樹的某個節點到根節點的距離為這裡寫圖片描述),並且需要取n-1次堆頂記錄,因此,重建堆的時間複雜度為O(nlogn)。
所以總體來說,堆排序的時間複雜度為O(nlogn),由於堆排序對原始記錄的狀態並不敏感,因此它無論是最好、最壞和平均時間複雜度均為O(nlogn)。這在效能上顯然要遠遠好過於冒泡、簡單選擇、直接插入的時間複雜度了。
空間複雜度上,它只有一個用來交換的暫存單元,也非常的不錯。不過由於記錄的比較與交換是跳躍式進行的,因此堆排序也是一種不穩定的排序方法。
另外,由於出事構建堆所需要的比較次數比較多,因此,他並不適合待排序序列個數較少的情況。
Java實現
/**
* 構建大頂堆
*/
public static void adjustHeap(int[] data,int i, int len) {
inttemp, j;
temp = data[i];
for (j = 2 * i; j < len; j *= 2) { //沿關鍵字較大的孩子結點向下篩選
if (j < len && data[j] < data[j + 1]) {
++j; //j為關鍵字中較大記錄的下標
}
if (temp >= data[j]) {
break;
}
data[i] = data[j];
i = j;
}
data[i] = temp;
}
/**
* 堆排序
* 時間複雜度:O(n^2)
*@param data
*/
public static void heapSort(int[] data) {
int i;
for (i = data.length / 2 - 1; i >= 0; i--) { //構建一個大頂堆
adjustHeap(data, i, data.length - 1);
}
for (i = data.length - 1; i >= 0; i--) { //將堆頂記錄和當前未經排序子序列的最後一個記錄交換
int temp = data[0];
data[0] = data[i];
data[i] = temp;
adjustHeap(data, 0, i - 1); //將a中前i-1個記錄重新調整為大頂堆
}
}
氣泡排序
基本思想
設排序表長為n,從後向前或者從前向後兩兩比較相鄰元素的值,如果兩者的相對次序不對(A[i-1]> A[i]),則交換它們,其結果是將最小的元素交換到待排序序列的第一個位置,我們稱它為一趟冒泡。下一趟冒泡時,前一趟確定的最小元素不再參與比較,待排序序列減少一個元素,每趟冒泡的結果把序列中最小的元素放到了序列的”最前面”。
演算法分析
氣泡排序的時間複雜度為O(n^2),空間複雜度為O(1),它是一種穩定的排序演算法。
1..如果我們的資料正序,只需要走一趟即可完成排序。所需的比較次數C和記錄移動次數M均達到最小值,即:Cmin=n-1;Mmin=0;所以,氣泡排序最好的時間複雜度為O(n)。
2.如果很不幸我們的資料是反序的,則需要進行n-1趟排序。每趟排序要進行n-i次比較(1≤i≤n-1),且每次比較都必須移動記錄三次來達到交換記錄位置。在這種情況下,比較和移動次數均達到最大值:
氣泡排序的最壞時間複雜度為:O(n2) 。
綜上所述:氣泡排序總的平均時間複雜度為:O(n2) 。
Java實現
/**
* 氣泡排序
* 時間複雜度:O(n^2)
*@param data
*/
public static void bubbleSort(int[] data){
int j , k;
int flag = data.length ;//flag來記錄最後交換的位置,也就是排序的尾邊界
while (flag > 0){//排序未結束標誌
k = flag; //k 來記錄遍歷的尾邊界
flag = 0;
for(j=1; j<k; j++){
if(data[j-1] > data[j]){//前面的數字大於後面的數字就交換
//交換a[j-1]和a[j]
int temp;
temp = data[j-1];
data[j-1] = data[j];
data[j]=temp;
//表示交換過資料
flag = j;//記錄最新的尾邊界
}
}
}
}
快速排序
基本思想
快速排序是對氣泡排序的一種改進。首先在陣列中選擇一個基準點,然後分別從陣列的兩端掃描陣列,設兩個指示標誌(lo指向起始位置,hi指向末尾),首先從後半部分開始,如果發現有元素比該基準點的值小,就交換lo和hi位置的值,然後從前半部分開始掃秒,發現有元素大於基準點的值,就交換lo和hi位置的值,如此往復迴圈,直到lo>=hi,然後把基準點的值放到hi這個位置。一次排序就完成了。以後採用遞迴的方式分別對前半部分和後半部分排序,當前半部分和後半部分均有序時該陣列就自然有序了。
假設要排序的陣列是A[1]……A[N],首先任意選取一個數據(通常選用第一個資料)作為關鍵資料,然後將所有比它的數都放到它前面,所有比它大的數都放到它後面,這個過程稱為一躺快速排序。一趟快速排序的演算法是:
1)、設定兩個變數I、J,排序開始的時候I:=1,J:=N;
2)以第一個陣列元素作為關鍵資料,賦值給X,即X:=A[1];
3)、從J開始向前搜尋,即由後開始向前搜尋(J:=J-1),找到第一個小於X的值,兩者交換;
4)、從I開始向後搜尋,即由前開始向後搜尋(I:=I+1),找到第一個大於X的值,兩者交換;
5)、重複第3、4步,直到I=J。
演算法分析
快速排序最“快”的地方在於左右兩邊能夠快速同時遞迴排序下去,所以最優的情況是基準值剛好取在無序區的中間,這樣能夠最大效率地讓兩邊排序,同時最大地減少遞迴劃分的次數。此時的時間複雜度僅為O(NlogN)。
快速排序也有存在不足的情況,當每次劃分基準值時,得到的基準值總是當前無序區域裡最大或最小的那個元素,這種情況下基準值的一邊為空,另一邊則依然存在著很多元素(僅僅比排序前少了一個),此時時間複雜度為:O(n2) 。
Java實現
/**
* 將陣列的某一段元素進行劃分,小的在左邊,大的在右邊
* @param data
* @param start
* @param end
* @return
*/
public static intdivide(int[] data, int start, int end){
//每次都以最右邊的元素作為基準值
int base = data[end];
//start一旦等於end,就說明左右兩個指標合併到了同一位置,可以結束此輪迴圈。
while(start < end){
while(start < end &&data[start] <= base) {
//從左邊開始遍歷,如果比基準值小,就繼續向右走
start++;
}
//上面的while迴圈結束時,就說明當前的a[start]的值比基準值大,應與基準值進行交換
if(start < end){
//交換
int temp = data[start];
data[start] = data[end];
data[end] = temp;
//交換後,此時的那個被調換的值也同時調到了正確的位置(基準值右邊),因此右邊也要同時向前移動一位
end--;
}
while(start < end &&data[end] >= base) {
//從右邊開始遍歷,如果比基準值大,就繼續向左走
end--;
}
//上面的while迴圈結束時,就說明當前的a[end]的值比基準值小,應與基準值進行交換
if(start < end){
//交換
int temp = data[start];
data[start] = data[end];
data[end] = temp;
//交換後,此時的那個被調換的值也同時調到了正確的位置(基準值左邊),因此左邊也要同時向後移動一位
start++;
}
}
//這裡返回start或者end皆可,此時的start和end都為基準值所在的位置
return end;
}
/**
* 快速排序
* 時間複雜度:O(n^2)
* @param data
* @param start
* @param end
*/
public static voidquickSort(int[] data, int start, int end){
if(start > end){
//如果只有一個元素,就不用再排下去了
return;
}else{
//如果不止一個元素,繼續劃分兩邊遞迴排序下去
int partition = divide(data, start,end);
quickSort(data, start, partition-1);
quickSort(data, partition+1, end);
}
}
歸併排序
基本思想
歸併排序就是利用歸併的思想實現的排序方法。而且充分利用了完全二叉樹的深度是的特性,因此效率比較高。其基本原理如下:對於給定的一組記錄,利用遞迴與分治技術將資料序列劃分成為越來越小的半子表,在對半子表排序,最後再用遞迴方法將排好序的半子表合併成為越來越大的有序序列。
經過第一輪比較後得到最小的記錄,然後將該記錄的位置與第一個記錄的位置交換;接著對不包括第一個記錄以外的其他記錄進行第二次比較,得到最小記錄並與第二個位置記錄交換;重複該過程,知道進行比較的記錄只剩下一個為止。
工作原理:
(1)申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
(2)設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
(3)比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
(4)重複步驟3直到某一指標達到序列尾
(5)將另一序列剩下的所有元素直接複製到合併序列尾
演算法分析
一趟歸併需要將陣列 a[]中相鄰的長度為h的有序序列進行兩兩歸併.並將結果放到temp[]中,這需要將待排序列中的所有記錄掃描一遍,因此耗費O(n),而又完全二叉樹的深度可知,整個歸併排序需要進行()次,因此總的時間複雜度為O(nlogn),而且這是歸併排序演算法中最好、最壞、平均的時間效能。
由於歸併排序在歸併過程中需要與原始序列同樣數量的儲存空間存放歸併結果以及遞迴時深度為的棧空間,因此空間複雜度為O(n+logn).
另外,對程式碼進行仔細研究,發現merge函式中有if (a[i] < a[j]) 的語句,說明它需要兩兩比較,不存在跳躍,因此歸併排序是一種穩定的排序演算法。
也就是說,歸併排序是一種比較佔記憶體,但卻效率高且穩定的演算法。
Java實現
/**
* 歸併排序
* @param data
* @param low
* @param mid
* @param high
*/
public static voidmerge(int[] data, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i= low;// 左指標
int j = mid + 1;// 右指標
int k = 0;
// 把較小的數先移到新陣列中
while (i <= mid && j <= high){
if (data[i] < data[j]) {
temp[k++] = data[i++];
} else {
temp[k++] = data[j++];
}
}
// 把左邊剩餘的數移入陣列
while (i <= mid) {
temp[k++] = data[i++];
}
// 把右邊邊剩餘的數移入陣列
while (j <= high) {
temp[k++] = data[j++];
}
// 把新陣列中的數覆蓋nums陣列
for (int k2 = 0; k2 < temp.length; k2++){
data[k2 + low] = temp[k2];
}
}
/**
* 歸併排序
* 時間複雜度:O(nlog2n)
* @param data
* @param low
* @param high
*/
public static voidmergeSort(int[] data, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
mergeSort(data, low, mid);//左邊
mergeSort(data, mid + 1, high);//右邊
merge(data, low, mid, high);//左右歸併
}
}
基數排序
基本思想
像選擇排序、插入排序、快速排序等都是基於兩個元素的比較進行排序的。而基數排序無需進行元素比較,基於佇列處理就能夠達到排序的目的。
基數排序不是基於排序關鍵字來比較排序項,而是基於排序關鍵字的結構。對於排序關鍵字中的每一個數字或字元的每一種可能取值,都會建立一個單獨的佇列。佇列的數目就稱為基數。
例如:要排序全部由小寫字母組成的字串,則基數就是26,就會用到26個單獨的佇列。如果對十進位制數進行排序,則基數應該是10。
為什麼不是所有的排序都使用基數排序演算法呢?
1.基數排序演算法要根據給定問題特別設計;
2.如果排序關鍵字中的數字數目與列表中元素的數目接近,那麼演算法的時間複雜度接近O(n平方);
3.基數影響空間複雜度。
演算法分析
在基數排序中,沒有任何元素的比較和交換,元素只是在每一輪中從一個佇列移動到另一個佇列。對於給定的基數,遍歷資料的輪次是一個常數,它與排序關鍵字的數目無關,於是,基數排序演算法的時間複雜度為O(n)。
Java實現
/**
* 基數排序
* 時間複雜度:O(nlog2n)
* @param data
*/
public static voidradixSort(int[] data){
String temp;
int numObj;
int digit,num;
Queue<Integer>[] digitQueue =(LinkedList<Integer>[])(new LinkedList[10]);
for(int digitVal = 0; digitVal <= 9;digitVal++){
digitQueue[digitVal] =(Queue<Integer>)(new LinkedList<Integer>());
}
//sort
for(int pos = 0; pos <= 3; pos++){
for(int scan = 0; scan <data.length; scan++){
temp = String.valueOf(data[scan]);
digit =Character.digit(temp.charAt((3 - pos)), 10);
digitQueue[digit].add(newInteger(data[scan]));
}
num = 0;
for(int digitVal = 0; digitVal <= 9;digitVal++){
while(!(digitQueue[digitVal]).isEmpty()){
numObj =digitQueue[digitVal].remove();
data[num] = numObj;
num++;
}
}
}
}