排序演算法在面試中是必過的一道關,因此今天我們就來簡單總結一下各種排序演算法的思想及**實現(c/c++版)。為了討論各演算法的時間複雜度和穩定性,我們以同一例項進行分析:5 3 6 5 2 1 5(遞增排序)。
1. 交換排序
1.1 氣泡排序
氣泡排序的基本思想是:從第乙個元素開始與其下一元素進行比較,若是當前元素比下乙個元素大則進行交換,反之則不進行任何操作。當全部遍歷一遍整個陣列後,最後乙個位置上的數字即是該陣列的最大值。因此當我們遍歷n-1次即可完成排序。
**實現:
vectorbubblesort(vectorarray)
} }return array;
}
深入分析:對於已經有序或者部分有序的陣列,我們完全沒有必要遍歷n-1遍,有時甚至只需要遍歷一遍即可實現有序(比如原先就是有序的陣列[1,3,5,6,7,8])。那麼在遍歷的過程中我們如何判斷當前陣列是否已經完全有序呢?答案就是當遍歷完一遍陣列後,並沒有進行一次「冒泡」(交換)操作,這時陣列就已經是有序的了。在改進的**中我們只需要增加乙個變數即可。改進後的**如下:
vectorbubblesort(vectorarray)
} if(issorted==true)
break; // 當前陣列已經有序,停止遍歷
} return array;
}
1.2 快速排序
快速排序的基本思想是:從陣列中任選乙個元素,然後把陣列中所有比該元素小的、相等的都調整到該元素的前面,把比該元素大的都調到該元素後面(相等的元素放在該元素的後面也是可以的,怎麼放快排都是不穩定的)。最後遞迴對前後兩個部分進行快排。注:初次建堆的時間複雜度為o(n)。
**實現:
int partition(vector&array, int begin, int end)
array[i] = temp;
return i; // 返回之前選擇的元素最終放置的位置
}void quicksort(vector&array, int begin, int end)
簡單分析:快排最壞的情況下時間複雜度為o(n^2),平均時間複雜度為o(nlogn),最好也是o(nlogn)。從測試用例[5, 3, 6, 5, 2, 1, 5]中可以看出,在最終排好序的陣列中第二個5排在第三個5後面,破壞了原有的穩定性,因此快排是不穩定的。
深入分析:寫出快排的非遞迴**。一般而言,遞迴演算法都可以用非遞迴的形式來表達(只需要深入分析出遞迴的原因,最後用迴圈來表達即可)。快排之所以需要進一步遞迴,是因為對於比該元素小的那一部分和比該元素大的那一部分依然是無序的,需要進行排序處理。因此,只要我們把需要進一步遞迴地區間儲存起來,然後利用迴圈對這些區間進一步排序即可。這裡我們借助「棧」這一資料結構,快排非遞迴的**實現如下:
stackwaitsort;
void quicksort2(vector&array, int begin, int end)
if(index+1begin) // 若左區間需要排序,入棧
if(index+12. 選擇排序
2.1 簡單選擇排序
簡單選擇排序的基本思想:該演算法和冒泡演算法有些相似,也是需要遍歷n-1遍;不同之處在於每次遍歷的目的是找出最大的元素,然後與最後的元素交換。
**實現:
vectorselectsort(vectorarray)
} array[max_location] = array[len-1-i];
array[len-1-i] = max;
} return array;
}
2.2 堆排序
堆排序的基本思想是:以遞增排序為例,首先根據待排序陣列構建乙個大根堆,然後把堆頂元素與最後乙個資料互換,互換之後的大根堆的性質可能會被破壞,需要進一步調整,調整後的大根堆堆頂元素又是剩下元素中的最大值,周而復始,一共需要調整n-1次。而構建大根堆的過程也是調整堆的過程,所以堆排序的核心就是如何調整堆。詳細思路可參考其他資料,**中也會寫出大致思路。
**實現:
void adjustheap(vector&array, int loc, int len) // 調整大根堆,這裡採用遞迴形式
array[j+1] = temp;}}
簡單分析:直接插入排序的時間複雜度是o(n^2),並且是穩定的。
深入分析:二分(折半)插入排序。如果利用二分查詢來搜尋元素應該插入的位置,一次搜尋的時間複雜度可以由原先的o(n)降低到o(logn),有所改進。但是二分插入排序的時間複雜度依然是o(n^2)(這是因為移動元素依然需要o(n)),並且穩定。
3.2 希爾排序
希爾排序的基本思想:先將整個待排元素序列分割成若干個子串行(由相隔某個「增量」的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。因為直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率上比前兩種方法有較大提高。
**實現:
void shellsort(vector&array)
array[j+index] = temp;
}} index = index / 2; // 增量折半減小
}}
簡單分析:以上**可以詳細描述希爾排序的思想,但是三重迴圈顯得**不夠簡潔,因此可以進一步改進。改進後的**如下:
void shellsort2(vector&array) // 改進後的希爾排序
array[j+index] = temp;
}//print(array);
index = index / 2; // 增量折半減少
}}
改進後的**形式更加簡單,也很容易理解。希爾排序最壞情況下的時間複雜度為o(n^2), 其他情況取決於增量的大小,其上限不會好於o(nlogn)。另外希爾排序是不穩定的。
4. 歸併排序
4.1 二路歸併
歸併排序運用了分治的思想,二路歸併作為平時最常見的歸併演算法,其基本思想是:把陣列從中間劃分出左右兩個區間,分別對左、右區間的陣列進行二路歸併排序,最後對排好序的這兩個區間進行歸併即可。
**實現:
void merge(vector&array, int begin, int mid, int end) // 定義乙個函式用於歸併,前半段為[begin,mid], 後半段為[mid+1,end]
else
}while(i<=mid) // 未比對完則全部依次加入結果陣列即可
result.push_back(array[i++]);
while(j<=end) // 該while與上乙個while有且僅有乙個會執行
result.push_back(array[j++]);
j = 0;
for(i=begin;i<=end;i++)
array[i] = result[j++]; // 把歸併結果拷貝到原陣列中
}void mergesort(vector&array, int begin, int end) // 遞迴實現歸併排序
簡單分析:二路歸併是穩定的排序演算法,其時間複雜度為o(nlogn)。
深入分析:二路歸併的非遞迴實現。對於二路歸併,其本質是把大陣列分為若干個小陣列,然後對相鄰的小陣列進行歸併,因此我們可以用迴圈來代替遞迴,迴圈變數為分組的步長。**如下所示:
void mergesort2(vector&array) // 非遞迴實現歸併排序
{ int len = array.size(); // 計算陣列長度
for(int i=1;i4.2 多路歸併
外部排序&多路歸併排序
經典排序之多路歸併
5. 基數排序
簡單理解基數排序
6. 小結
各排序演算法的時間複雜度和穩定性比較,如下圖所示。
排序演算法小結
1 快速排序 quicksort 快速排序是乙個就地排序,分而治之,大規模遞迴的演算法。從本質上來說,它是歸併排序的就地版本。快速排序可以由下面四步組成。1 如果不多於1個資料,直接返回。2 一般選擇序列最左邊的值作為支點資料。3 將序列分成2部分,一部分都大於支點資料,另外一部分都小於支點資料。4...
排序演算法小結
1 歸併排序 3.區別與聯絡 遞迴是從未知推到已知,相當於把未知的東西壓入棧,等到可以算出結果了,就一步一步出棧。迭代是從已知到未知,從已知的東西一步一步推至目標。遞迴與迭代就好像一對逆元。遞迴的 更加清晰,但開銷更大,也更容易出錯,除錯較困難 而迭代的 編寫更困難,但速度和開銷較小。4.空間占用 ...
排序演算法小結
演算法過程 假設乙個無序的序列,該演算法將其分成兩部分,前一部分已經完成排序 有序,一開始時只有乙個元素 後一部分任然無序,將後面序列選擇第乙個插入到前面的有序序列,如此直到所有完全有序。複雜度 最簡單的即為,整個序列原來即有序,按照一種最 省事 的方式,我們僅需比較n 1次即可。最複雜的情況,應該...