總結一下常見的排序算法。
算法
排序份內排序和外排序。
內排序:指在排序期間數據對象所有存放在內存的排序。
外排序:指在排序期間所有對象個數太多,不能同時存放在內存,必須根據排序過程的要求,不斷在內、外存之間移動的排序。
內排序的方法有許多種,按所用策略不一樣,可概括爲五類:插入排序、選擇排序、交換排序、歸併排序、分配排序和計數排序。
插入排序主要包括直接插入排序,折半插入排序和希爾排序兩種;
選擇排序主要包括直接選擇排序和堆排序;
交換排序主要包括冒泡排序和快速排序;
歸併排序主要包括二路歸併(經常使用的歸併排序)和天然歸併。
分配排序主要包括箱排序和基數排序。
計數排序就一種。shell
穩定排序:假設在待排序的文件中,存在兩個或兩個以上的記錄具備相同的關鍵字,在用某種排序法排序後,若這些相同關鍵字的元素的相對次序仍然不變,則這種排序方法是穩定的。
其中冒泡,插入,基數,歸併屬於穩定排序;選擇,快速,希爾,堆屬於不穩定排序。
下面是對這些常見排序算法的介紹。將來測試代碼的正確性,咱們採起了隨機生成10個序列,而後先使用C++STL中給出的排序算法sort來獲得一個正確的排序,而後再使用咱們的方法進行排序獲得結果,經過對比這二者的結果來驗證咱們代碼的正確性。數組
測試代碼以下:函數
void sort_test(void (*_sort)(int*,int)){
const int N=10;
int orig[N];
int standard[N];
int arr[N];
srand(time(0));
for(int j=0;j<15;j++){
for(int i=0;i<N;i++)
orig[i]=rand()%100;//隨機生成序列
cout<<"bef:";
print(orig,N);
copy(orig,orig+N,standard);
sort(standard,standard+N);//利用sort函數進行排序
cout<<"std:";
print(standard,N);
copy(orig,orig+N,arr);
_sort(arr,N);//採用咱們的方法進行排序
cout<<"aft:";
print(arr,N);
if(equal(standard,standard+N,arr))//測試咱們的方法是否正確
printf("%sOK%s\n",green,normal);
else
printf("%sNO%s\n",red,normal);
}
}測試
其中參數是要測試的方法,void (*_sort)(int*,int)是排序方法的指針,咱們全部的排序方法都寫成這種形式。ui
1. 直接插入排序
spa
直接插入排序(straight insertion sort)的做法是:每次從無序表中取出第一個元素,把它插入到有序表的合適位置,使有序表仍然有序。
指針
第一趟比較前兩個數,而後把第二個數按大小插入到有序表中; 第二趟把第三個數據與前兩個數從後向前掃描,把第三個數按大小插入到有序表中;依次進行下去,進行了(n-1)趟掃描之後就完成了整個排序過程。
orm
直接插入排序屬於穩定的排序,時間複雜性爲o(n^2),空間複雜度爲O(1)。
對象
直接插入排序是由兩層嵌套循環組成的。外層循環標識並決定待比較的數值。 內層循環爲待比較數值肯定其最終位置。直接插入排序是將待比較的數值與它的前一個數值進行比較,因此外層循環是從第二個數值開始的。當前一數值比待比較數 值大的狀況下繼續循環比較,直到找到比待比較數值小的並將待比較數值置入其後一位置,結束該次循環。(從小到大)
值得注意的是,咱們必需用一個存儲空間來保存當前待比較的數值,由於當一趟比較完成時,咱們要將待比較數值置入比它小的數值的後一位。插入排序相似玩牌時整理手中紙牌的過程。
代碼以下:
void insert_sort(int a[],int n)
{
_FUNC;
for(int i=1;i<n;i++) {
int t=a[i];
int j;
for(j=i-1;j>=0&&a[j]>t;j--) {
a[j+1]=a[j];
}
a[j+1]=t;
print(a,n);
}
}
測試結果以下:
2. 折半插入排序
折半插入排序(binary insertion sort)是對插入排序算法的一種改進,因爲排序算法過程當中,就是不斷的依次將元素插入前面已排好序的序列中。因爲前半部分爲已排好序的數列,這樣咱們不用按順序依次尋找插入點,能夠採用折半查找的方法來加快尋找插入點的速度。
折半插入排序算法的具體操做爲:在將一個新元素插入已排好序的數組的過程當中,尋找插入點時,將待插入區域的首元素設置爲a[low],末元素設置爲 a[high],則輪比較時將待插入元素與a[m],其中m=(low+high)/2相比較,若是比參考元素小,則選擇a[low]到a[m-1]爲新 的插入區域(即high=m-1),不然選擇a[m+1]到a[high]爲新的插入區域(即low=m+1),如此直至low<=high不成 立,即將此位置以後全部元素後移一位,並將新元素插入a[high+1]。
折半插入排序算法是一種穩定的排序算法,比直接插入算法明顯減小了關鍵字之間比較的次數,所以速度比直接插入排序算法快,但記錄移動的次數沒有變,因此折半插入排序算法的時間複雜度仍然爲O(n^2),與直接插入排序算法相同。
代碼以下:
void binary_insert_sort(int a[],int n){
for(int i=1;i<n;i++){
int low=0;
int high=i-1;
int t=a[i];
int mid;
while(low<=high){
mid=(low+high)/2;
if(t<a[mid])
high=mid-1;
else
low=mid+1;
}
for(int j=i;j>mid;j--)
a[j]=a[j-1];
a[low]=t;
}
}
測試結果以下:
3. 希爾排序
希爾排序(Shell Sort)又叫作縮小增量排序(diminishing increment sort),是一種很優秀的排序法,算法自己不難理解,也很容易實現,並且它的速度很快。
基本思想:
先取一個小於n的整數d1做爲第一個增量,把文件的所有記錄分紅d1個組。全部距離爲dl的倍數的記錄放在同一個組中。先在各組內進行直接插入 排序;而後,取第二個增量d2<d1重複上述的分組和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1), 即全部記錄放在同一組中進行直接插入排序爲止。
該方法實質上是一種分組插入方法。插入排序(Insertion Sort)的一個重要的特色是,若是原始數據的大部分元素已經排序,那麼插入排序的速度很快(由於須要移動的元素不多)。從這個事實咱們能夠想到,若是原 始數據只有不多元素,那麼排序的速度也很快。--希爾排序就是基於這兩點對插入排序做出了改進。
下圖是希爾排序的一種實現方式:
該圖對應的實現方式以下:
void shell_sort(int a[],int n)
{
_FUNC;
int gap=n/2;
bool flag=true;
while(gap>1||flag)
{
flag=false;
for(int i=0;i+gap<n;i++)
if(a[i]>a[i+gap])
{
swap(a[i],a[i+gap]);
flag=true;
}
print(a,n);
if(gap>1)
gap/=2;
}
}
測試結果以下:
另外一種實現方式:
void shell_sort2(int a[],int n){
// _FUNC;
int gap=n/2;
while(gap>0){
for(int i=gap;i<n;i++){
int t=a[i];
int j;
for(j=i-gap;j>=0&&a[j]>t;j-=gap)
a[j+gap]=a[j];
a[j+gap]=t;
}
gap/=2;
}
}
4. 直接選擇排序
排序是給每一個位置選擇當前元素最小的,好比給第一個位置選擇最小的,在剩餘元素裏面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個 元素不用選擇了,由於只剩下它一個最大的元素了。那麼,在一趟選擇,若是當前元素比一個元素小,而該小的元素又出如今一個和當前元素相等的元素後面,那麼 交換後穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9,咱們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對先後順序就被破壞了,因此選擇排序不是一個穩定的排序算法。時間複雜度是O(n^2)
代碼以下:
void select_sort(int a[],int n)
{
for(int i=0;i<n-1;i++)
{
int min=a[i];
int index=i;
for(int j=i+1;j<n;j++)
if(a[j]<min)
{
min=a[j];
index=j;
}
swap(a[i],a[index]);
}
}
這個是最基本的:從中找出最小的而後和第一個數交換,再從第2到n-1中找出最小的和第二個數交換
方法二:
void select_sort2(int a[],int n)
{
_FUNC;
for(int i=n-1;i>0;i--){
for(int j=0;j<i;j++)
if(a[j]>a[i])
swap(a[j],a[i]);
}
}
這兒感受形式上有點相似下面的冒泡排序。
方法三:
這是對方法二的改進,判斷過程當中是否有交換髮生,若是沒有交換,說明已經完成排序了。
void select_sort3(int a[],int n)
{
_FUNC;
bool flag=true;
for(int i=n-1;i>0&&flag;i--){
flag=false;
for(int j=0;j<i;j++)
if(a[j]>a[i])
swap(a[j],a[i]),flag=true;
print(a,n);
}
}
5. 堆排序
咱們知道堆的結構是節點i的孩子爲2*i和2*i+1節點,大頂堆要求父節點大於等於其2個子節點,小頂堆要求父節點小於等於其2個子節點。在一個長爲n 的序列,堆排序的過程是從第n/2開始和其子節點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇固然不會破壞穩定性。但當爲n /2-1, n/2-2, ...1這些個父節點選擇元素時,就會破壞穩定性。有可能第n/2個父節點交換把後面一個元素交換過去了,而第n/2-1個父節點把後面一個相同的元素沒 有交換,那麼這2個相同的元素之間的穩定性就被破壞了。因此,堆排序不是穩定的排序算法。
堆排序的代碼以下:
void adjust(int b[],int m,int n){
// int *b=a-1;
int j=m;
int k=2*m;
while(k<=n){
if(k<n&&b[k]<b[k+1])
k++;
if(b[j]<b[k])
swap(b[j],b[k]);
j=k;
k*=2;
}
}
void heap_sort(int a[],int n){
_FUNC;
int *b=a-1;
for(int i=n/2;i>=1;i--)
adjust(b,i,n);
for(int i=n-1;i>=1;i--){
swap(b[1],b[i+1]);
adjust(b,1,i);
}
}
須要注意的是,若是使用數組表示堆的話,要從下標1開始,而不是從0開始。因此,這兒採用了一個技巧,讓int*b=a-1;這樣的話b[1]就至關於對原數組從0開始似的,即a[0]。
6. 冒泡排序
冒泡排序就是把小的元素往前調或者把大的元素日後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。因此,若是兩個元素相等,是不用交換的;若是兩個相等的元素沒有相鄰,那麼即便經過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,因此相同元素的先後順序並無改 變,因此冒泡排序是一種穩定排序算法。
代碼以下:
void bubble_sort(int a[],int n)
{
_FUNC;
for(int i=n-1;i>0;i--)
for(int j=0;j<i;j++)
if(a[j]>a[j+1])
swap(a[j],a[j+1]);
}
下面的方法是加入了是否已經排好序的判斷。
void bubble_sort2(int a[],int n)
{
bool flag=true;
for(int i=n-1;i>0&&flag;i--){
flag=false;
for(int j=0;j<i;j++)
if(a[j]>a[j+1])
swap(a[j],a[j+1]),flag=true;
}
}
7. 快速排序
快速排序(Quicksort)是對冒泡排序的一種改進。由C. A. R. Hoare在1962年提出。它的基本思想是:經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比另一部分的全部數據都要小,然 後再按此方法對這兩部分數據分別進行快速排序,整個排序過程能夠遞歸進行,以此達到整個數據變成有序序列。
快速排序有兩個方向,左邊的i下標一直往右走,當a[i] <= a[center_index],其中center_index是中樞元素的數組下標,通常取爲數組第0個元素。而右邊的j下標一直往左走,當a[j] > a[center_index]。若是i和j都走不動了,i <= j, 交換a[i]和a[j],重複上面的過程,直到i>j。交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交 換的時候,頗有可能把前面的元素的穩定性打亂,好比序列爲 5 3 3 4 3 8 9 10 11,如今中樞元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂,因此快速排序是一個不穩定的排序算法,不穩定發生在中樞元素和 a[j] 交換的時刻。
下面的代碼中中樞元素採用的中間的元素:
void qsort(int a[],int l,int r){
int pvt=a[(l+r)/2];
int i=l,j=r;
while(i<=j){
while(a[i]<pvt)
i++;
while(a[j]>pvt)
j--;
if(i<=j){
if(i!=j)
swap(a[i],a[j]);
i++;
j--;
}
}
if(j>l)
qsort(a,l,j);
if(i<r)
qsort(a,i,r);
}
void quick_sort(int a[],int n){
qsort(a,0,n-1);
}
8. 二路歸併排序
歸併排序是把序列遞歸地分紅短序列,遞歸出口是短序列只有1個元素(認爲直接有序)或者2個序列(1次比較和交換),而後把各個有序的段序列合併成一個有 序的長序列,不斷合併直到原序列所有排好序。能夠發現,在1個或2個元素時,1個元素不會交換,2個元素若是大小相等也沒有人故意交換,這不會破壞穩定 性。那麼,在短的有序序列合併的過程當中,穩定是是否受到破壞?沒有,合併過程當中咱們能夠保證若是兩個當前元素相等時,咱們把處在前面的序列的元素保存在結 果序列的前面,這樣就保證了穩定性。因此,歸併排序也是穩定的排序算法。
方法一:遞歸形式的歸併排序
void merge(int a[],int b[],int l,int m,int r){
// int *b=new int[r-l+1];
int i,j,k;
i=l;
j=m+1;
k=l;
while(i<=m&&j<=r){
if(a[i]<a[j])
b[k++]=a[i++];
else
b[k++]=a[j++];
}
while(i<=m)
b[k++]=a[i++];
while(j<=r)
b[k++]=a[j++];
for(int s=l;s<=r;s++)
a[s]=b[s];
// delete[] b;
}
void msort(int a[],int b[],int l,int r){
if(l<r){
int m=(l+r)/2;
msort(a,b,l,m);
msort(a,b,m+1,r);
merge(a,b,l,m,r);
}
}
void merge_sort(int a[],int n){
_FUNC;
int *b=new int[n];
msort(a,b,0,n-1);
delete[] b;
}
方法二:去除遞歸的方法
void merge_pass(int x[],int y[],int s,int n){
int i=0;
while(i+2*s-1<n){
merge(x,y,i,i+s-1,i+2*s-1);
i+=2*s;
}
if(i+s<n)
merge(x,y,i,i+s-1,n-1);
else
for(int j=i;j<=n-1;j++)
y[j]=x[j];
}
void merge_sort2(int a[],int n){
_FUNC;
int *b=new int [n];
int s=1;
while(s<n){
merge_pass(a,b,s,n);
s+=s;
merge_pass(b,a,s,n);
s+=s;
}
delete[] b;
}
9. 天然歸併排序
下面的兩種形式是同樣的,開始我先採用的vector來記錄子序列的位置,後來發現其實採用一個數組就能夠了。兩種代碼都放在這兒吧。
形式1:
void merge_sort3(int a[],int n){
vector<int> st;
for(int i=0;i<n-1;i++){
if(a[i]>a[i+1])
st.push_back(i);
}
st.push_back(n-1);
// copy(st.begin(),st.end(),ostream_iterator<int>(cout," "));
// cout<<endl;
int *b=new int [n];
int l,m,r;
l=0;
if(!st.empty())
{
m=st.front();
st.erase(st.begin());
}
while(!st.empty()){
r=st.front();
st.erase(st.begin());
merge(a,b,l,m,r);
// print(a,n);
// copy(st.begin(),st.end(),ostream_iterator<int>(cout," "));
// cout<<endl;
m=r;
}
// print(a,n);
delete [] b;
}
形式2:
void merge_sort4(int a[],int n){
_FUNC;
int *pos=new int[n];
int k=0;
for(int i=0;i<n-1;i++){
if(a[i]>a[i+1])
pos[k++]=i;
}
pos[k++]=n-1;
int *b=new int [n];
int l,m,r;
l=0;
int p=0;
if(p<k)
m=pos[p++];
while(p<k){
r=pos[p++];
merge(a,b,l,m,r);
m=r;
}
delete [] b;
}
10. 箱排序
一個簡單的測試例子以下:
View Code
上面的程序採用一個簡單的Node類來描述學生的姓名和成績,採用STL中的list來實現箱子排序。一樣進行隨機生成了多個實例來測試程序的正確性。而所採用的標準是STL中的multimap容器。由於這個容器能夠自動根據關鍵字進行排序。原本想使用map容器,可是map容器不容許重複,而咱們的測試實例中有不少的重複元素。測試的部分結果以下:
11. 基數排序
基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優 先級排序,最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,因此其是穩定的排序算法。
下面的一種方法是採用STL的鏈表容器list來實現的,這種實現比較直觀:
void radix_sort2(int a[],int n){
int bits=maxbits(a,n);
list<int> x(a,a+n);
int range=10;
vector<list<int> > bin(range);
list<int> y;
list<int>::iterator ite;
int adix=1;
for(int i=0;i<bits;i++){
for(ite=x.begin();ite!=x.end();ite++){
int d=(*ite/adix)%10;
bin[d].push_back(*ite);
}
vector<list<int> >::iterator ite2;
y.clear();
for(ite2=bin.begin();ite2!=bin.end();++ite2){
for(ite=ite2->begin();ite!=ite2->end();++ite)
y.push_back(*ite);
ite2->clear();
}
x=y;
adix*=10;
}
int i=0;
for(ite=x.begin();ite!=x.end();ite++)
a[i++]=*ite;
}
另外一種方法是採用多個數組來實現,不是很容易理解,這是參考的網上的代碼。具體代碼以下:
int maxbits(int a[],int n){
int d=0;
for(int i=0;i<n;i++){
int b=1;
int r=a[i];
while(r/10>0){
b++;
r/=10;
}
if(d<b)
d=b;
}
return d;
}
void radix_sort(int a[],int n){
_FUNC;
int d=maxbits(a,n);
int *temp=new int[n];
int *count=new int[10];
int adix=1;
for(int b=1;b<=d;b++){
for(int i=0;i<10;i++)
count[i]=0;
for(int i=0;i<n;i++){
int k=(a[i]/adix)%10;
count[k]++;
}
for(int i=1;i<10;i++)
count[i]+=count[i-1];
for(int i=n-1;i>=0;i--){
int k=(a[i]/adix)%10;
count[k]--;
temp[count[k]]=a[i];
}
for(int i=0;i<n;i++)
a[i]=temp[i];
adix*=10;
}
delete[] temp;
delete[] count;
}
12.計數排序
代碼以下:
void rank(int arr[],int n,int r[])
{
for(int i=0;i<n;i++)
r[i]=0;
for(int i=1;i<n;i++){
for(int j=0;j<i;j++)
{
if(arr[j]<=arr[i])
r[i]++;
else
r[j]++;
}
}
}
代碼以下:
void rank_sort2(int a[],int n){
int *r=new int[n];
rank(a,n,r);
int *u=new int[n];
for(int i=0;i<n;i++)
u[r[i]]=a[i];
for(int i=0;i<n;i++)
a[i]=u[i];
delete[] r;
delete[] u;
}
代碼以下:
void rank_sort(int arr[],int n)
{
int *r=new int[n];
rank(arr,n,r);
for(int i=0;i<n;i++)
{
while(r[i]!=i)
{
int t=r[i];
swap(arr[i],arr[t]);
swap(r[i],r[t]);
}
}
delete[] r;
}
排序算法複雜度:
按平均時間將排序分爲四類:
(1)平方階(O(n2))排序
通常稱爲簡單排序,例如直接插入、直接選擇和冒泡排序;
(2)線性對數階(O(nlgn))排序
如快速、堆和歸併排序;
(3)O(n1+£)階排序
£是介於0和1之間的常數,即0<£<1,如希爾排序;
(4)線性階(O(n))排序
如桶、箱和基數排序。
不一樣條件下,排序方法的選擇
(1)若n較小(如n≤50),可採用直接插入或直接選擇排序。
當記錄規模較小時,直接插入排序較好;不然由於直接選擇移動的記錄數少於直接插人,應選直接選擇排序爲宜。
(2)若文件初始狀態基本有序(指正序),則應選用直接插人、冒泡或隨機的快速排序爲宜;
(3)若n較大,則應採用時間複雜度爲O(nlgn)的排序方法:快速排序、堆排序或歸併排序。
快速排序是目前基於比較的內部排序中被認爲是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;
堆排序所需的輔助空間少於快速排序,而且不會出現快速排序可能出現的最壞狀況。這兩種排序都是不穩定的。
若要求排序穩定,則可選用歸併排序。但本章介紹的從單個記錄起進行兩兩歸併的 排序算法並不值得提倡,一般能夠將它和直接插入排序結合在一塊兒使用。先利用直接插入排序求得較長的有序子文件,而後再兩兩歸併之。由於直接插入排序是穩定 的,因此改進後的歸併排序還是穩定的。
4)在基於比較的排序方法中,每次比較兩個關鍵字的大小以後,僅僅出現兩種可能的轉移,所以能夠用一棵二叉樹來描述比較斷定過程。
當文件的n個關鍵字隨機分佈時,任何藉助於"比較"的排序算法,至少須要O(nlgn)的時間。
箱排序和基數排序只需一步就會引發m種可能的轉移,即把一個記錄裝入m個箱子之一,所以在通常狀況下,箱排序和基數排序可能在O(n)時間內完成對n個 記錄的排序。可是,箱排序和基數排序只適用於像字符串和整數這類有明顯結構特徵的關鍵字,而當關鍵字的取值範圍屬於某個無窮集合(例如實數型關鍵字)時, 沒法使用箱排序和基數排序,這時只有藉助於"比較"的方法來排序。
若n很大,記錄的關鍵字位數較少且能夠分解時,採用基數排序較好。雖然桶排序對關鍵字的結構無要求,但它也只有在關鍵字是隨機分佈時才能使平均時間達到 線性階,不然爲平方階。同時要注意,箱、桶、基數這三種分配排序均假定了關鍵字若爲數字時,則其值均是非負的,不然將其映射到箱(桶)號時,又要增長相應 的時間。
(5)有的語言(如Fortran,Cobol或Basic等)沒有提供指針及遞歸,致使實現歸併、快速(它們用遞歸實現較簡單)和基數(使用了指針)等排序算法變得複雜。此時可考慮用其它排序。
(6)本章給出的排序算法,輸人數據均是存儲在一個向量中。當記錄的規模較大時,爲避免耗費大量的時間去移動記錄,能夠用鏈表做爲存儲結構。譬如插入排 序、歸併排序、基數排序都易於在鏈表上實現,使之減小記錄的移動次數。但有的排序方法,如快速排序和堆排序,在鏈表上卻難於實現,在這種狀況下,能夠提取 關鍵字創建索引表,而後對索引表進行排序。然而更爲簡單的方法是:引人一個整型向量t做爲輔助表,排序前令t[i]=i(0≤i<n),若排序算法 中要求交換R[i]和R[j],則只需交換t[i]和t[j]便可;排序結束後,向量t就指示了記錄之間的順序關係:
R[t[0]].key≤R[t[1]].key≤…≤R[t[n-1]].key
若要求最終結果是:
R[0].key≤R[1].key≤…≤R[n-1].key
則能夠在排序結束後,再按輔助表所規定的次序重排各記錄,完成這種重排的時間是O(n)。