總述:排序是指將元素集合按規定的順序排列。一般有兩種排序方法:升序排列和降序排列。例如,如整數集{6,8,9,5}進行升序排列,結果爲{5,6,8,9},對其進行降序排列結果爲{9,8,6,5}。雖然排序的顯著目的是排列數據以顯示它,但它每每能夠用來解決其餘的問題,特別是做爲某些成型算法的一部分。算法
總的來講,排序算法分爲兩大類:比較排序 和 線性時間排序。windows
某些算法只使用數據自己的存儲空間來處理和輸出數據(這些稱爲就地排序或內部排序),數組
而有一些則須要額外的空間來處理和輸出數據(雖然可能最終結果仍是會拷貝到原始內存空間中)(這些稱之爲外部排序)。數據結構
插入排序是最簡單的排序算法。正式表述爲:插入排序每次從無序數據集中取出一個元素,掃描已排好序的數據集,並將它插入有序集合的合適位置上(像咱們打撲克牌摸牌時的操做)。雖然乍一看插入排序須要獨立爲有序和無序的元素預留足夠的存儲空間,但實際上它是不須要額外的存儲空間的。函數
插入排序是一種較爲簡單的算法,但它在處理大型數據集時並不高效。由於在決定將元素插入哪一個位置以前,須要將被插入元素和有序數據集中的其餘元素進行比較,這會隨着的數據集的增大而增長額外的開銷。插入排序的優勢是當將元素插入一個有序數據集中時,只需對有序數據集最多進行一次遍歷,而不須要完整的運行算法,這個特性使得插入排序在增量排序中很是高效。性能
issortspa
int issort(void *data, int size, int esize, int (*compare)(const void *key1, const void *key2));命令行
返回值:若是排序成功返回0,不然返回-1。3d
描述:利用插入排序將數組data中的元素進行排序。data中的元素個數由size決定。而每一個元素的大小由esize決定。unix
函數指針compare會指向一個用戶定義的函數來比較元素的大小。在遞增排序中,若是key1>key2,函數返回1;若是key1=key2,函數返回0;若是key1<key2,函數返回-1。在背叛排序中,返回值相反。當issort返回時,data包含已排好序的元素。
複雜度:O(n2),n爲要排序的元素的個數。
從根本上講,插入排序就是每次從未排序的數據集中取出一個元素,插入已經排好序的數據集中。在下面的實現中,兩個數據集都存放在data中,data是一塊連續的存儲區域。
最初,data包含size個無序元素,隨着issort的運行,data逐漸被有序數據集所取代,直到issort返回,此時data已是一個有序數據集。雖然插入排序使用的是連續的存儲空間,但它仍能用鏈表來實現,而且效率也不差。
插入排序使用一個嵌套循環,外部循環使用標號j來控制元素,使元素從無序數據集中插入有序數據集中。因爲待插入的元素老是在有序數據集的右邊,所以也能夠認爲j是data中分隔有序元素集和無序元素集的界線。對於每一個處理位置j的元素,都會使用變量i來在有序數據集中向後查找元素將要放置的位置。當向後查找數據時,每一個處於位置i的元素都要向右移動一位,以保證留出足夠的空間來插入新元素。一旦j到達無序數據集的尾部,data就是一個有序數據集了。
插入排序的時間複雜度關鍵在於它的嵌套循環部分。外部循環運行時間T(n)=n-1,乘以一段固定的時間,其中n爲要排序元素的個數。考慮內部循環運行在最壞的狀況,假設在插入元素以前必須從右到左遍歷完全部的元素。這樣的話,內部循環對於第一個元素迭代一次,對於第二個元素迭代兩次,以此類推。直到外部循環終止。嵌套循環的運行時間表示爲1到n-1數據的和,即運行時間T(n)=n(n+1)/2 - n,乘以一段固定時間(這是由1到n的求和公式推導出的)。爲O表示法能夠簡化爲O(n2)。當在遞增排序中使用插入排序時,其時間複雜度爲O(n)。插入排序不須要額外的空間,所以它只使用無序數據集自己的空間便可。
示例:插入排序的實現
/*issort.c*/ #include <stdlib.h> #include <string.h> #include "sort.h" /*issort 插入排序*/ int issort(void *data, int size, int esize, int (*compare)(const void *key1, const void *key2)) { char *a = data; void *key; int i,j; /*爲key元素分配一塊空間*/ if((key =(char *)malloc(esize)) == NULL) return -1; /*將元素循環插入到已排序的數據集中*/ for(j=1; j < size; j++) {
/*取無序數據集中的第j個元素,複製到key中*/ memcpy(key, &a[j*esize], esize);
/*設i爲j緊鄰的前一個元素*/ i = j - 1; /*從i開始循環查找能夠插入key的正確位置*/
/*key和第i個元素對比,若是小於第i個元素就複製i元素到i+1的位置;i遞減循環對比*/ while(i >= 0 && compare(&a[i*esize],key)>0) { memcpy(&a[(i+1)*esize],&a[i*esize],esize); i--; }
/*將key元素的值(也就是要插入的值)複製到while循環後i+1的位置,也就是要插入的位置*/ memcpy(&a[(i+1)*esize],key,esize); } /*釋放key的空間*/ free(key); return 0; }
快速排序是一種分治算法。
普遍地認爲它是解決通常問題的最佳算法。同插入排序同樣,快速排序也屬於比較排序的一種,並且不須要額外的存儲空間。在處理中到大型數據集時,快速排序是一個比較好的選擇。
咱們來看一我的工對一堆做廢的支票進行排序的例子,能夠將未排序的支票分爲兩堆。其中一堆專門用來放小於或等於某個編號的支票,而另外一堆用來放大於這個編號的支票(假設這個支票大概是全部支票編號的中間值)。當以這種方式獲得兩堆支票後,又能夠以一樣的方式將它們分爲四堆,不斷的重複這個過程直到每一個堆中只放有一張支票。這時,全部的支票就已經排好序了。
因爲快速排序屬於分治算法的一種,咱們用分治的思想將排序分爲三個步驟:
一、分:設定一個分割值,將數據分爲兩部分;
二、治:分別在兩個部分用遞歸的方式繼續使用快速排序法;
三、合:對分割部分排序直至完成。
快速排序最壞狀況下的性能不會比插入排序的最壞狀況好。經過一點點修改能夠大大改善快速排序最懷狀況的效率,使其表現得與其平均狀況至關。如何作到這一點,關鍵在於如何選擇分割值。
所選的分割值須要儘量的將元素平均分開。若是分割值會將大部分的元素放到其中一堆中,那麼此時快速排序的性能會很是差。例如:若是用10做爲數據值{15,20,18,51,36,10,77,43}的分割值,其結果爲{10}和{15,20,18,51,36,77,43},明顯不平衡。若是將分割值選爲36,其結果爲{36,51,77,43}和{15,20,18,10},就比較平衡。
選擇分割值的一種有效的方法是經過 隨機選擇法 來選取。隨機選擇法可以有效的防止被分割的數據極度不平衡。同時,還能夠改進這種隨機選擇法,方法是:首先隨機選擇三個元素,而後選擇三個元素中的中間值。這就是所謂的中位數方法,能夠保證平均狀況下的性能。因爲這種分割方法依賴隨機數的統計特性,從而保證快速排序的總體性能,所以快速排序也是隨機算法的一個好例子。
qksort
int qksort(void *data, int size, int esize, int i, int k, int (*compare)(const void *key1, const void *key2);
返回值:若是排序成功,返回0;不然返回-1。
描述: 利用快速排序將數組data中的元素進行排序。數組中的元素個數由size決定。而每一個元素的大小由esize決定。參數i和k定義當前進行排序的兩個部分,其值分別初始爲0和size-1。函數指針compare會指向一個用戶定義的函數來比較元素大小,其函數功能與issort中描述的同樣。當qksort返回時,data包含已經排好序的元素。
複雜度: O(n lg n),n爲要被排序的元素的個數。
快速排序本質上就是不斷地將無序元素集遞歸分割,直到全部的分區都只包含單個元素。
在如下的實現方法中,data包含size個無序元素,並存放在單塊連續的存儲空間中,快速排序不須要額外的存儲空間,因此全部分割過程都在data中完成。當qksort返回時,data就是一個有序的數據集了。
快速排序的關鍵部分是如何分割數據。這部分工做由函數partition完成。函數分割data中處於i和k之間的元素(i小於k)。
首先,用前面提到的中位數法選取和個分割值。一旦選定分割值,就將k往data的左邊移動,直到找到一個小於或等於分割值的元素。這個元素屬於左邊分區。接下來,將i往右邊移動,直到找到一個大於或等於分割值的元素。這個元素屬於右邊分區。一旦找到的兩個元素處於錯誤的位置,就交換它們的位置。重複這個過程,直到i和k重合。一旦i和k重合,那麼全部處於左邊的元素將小於等於它,全部處於右邊的元素將大於等於它。
qksort中處理遞歸的過程:在初次調用qksort時,i設置爲0,k設置爲size-1。首先調用partition將data中處於i和k之間的元素分區。當partition返回時,把j賦於分割點的元素。接下來,遞歸調用qksort來處理左邊的分區(從i到j)。左邊的分區繼續遞歸,直到傳入qksort的一個分區只包含單個元素。此時i不會比k小,因此遞歸調用終止。一樣,分區的右邊也在進行遞歸處理,處理的區間是從j+1至k。總的來講,以這種遞歸的方式繼續運行,直到首次達到qksort終止的條件,此時,數據就徹底排好了。
圍繞其平均狀況下的性能分析是快速排序的重點,由於一致認爲平均狀況是它複雜度的度量。雖然在最壞狀況下,其運行時間O(n2)並不比插入排序好,但快速排序的性能通常能比較有保障地接近其平均性能O(nlgn),其中n爲要排序的元素個數。
快速排序在平均狀況下的時間複雜度取決於均勻分佈的狀況,即數據是否分割爲平衡或不平衡的分區。若是使用中位數法,那麼此平衡分區將有保障。在這種狀況下,當不斷分割數組,在圖3中用樹(高度爲(lgn)+1)的方式直觀地表示出來。因爲頂部爲lgn層的樹,所以必須遍歷全部n個元素,以造成新的分區,這樣快速排序的運行時間爲O(nlgn)。快速排序不須要額外的存儲空間,所以它只使用無序數據自己的存儲空間便可。
/*qksort.c*/ #include <stdlib.h> #include <string.h> #include "sort.h" /*compare_int 比較函數*/ static int compare_int(const void *int1, const void *int2) { /*對比兩個整數的大小(用於中位數分區)*/ if(*(const int *)int1 > *(const int *)int2) return 1; else if(*(const int *)int1 < *(const int *)int2) return -1; else return 0; } /*partition 分割函數*/ static int partition(void *data, int esize, int i, int k, int (*compare)(const void *key1, const void *key2)) { char *a=data; void *pval, *temp; int r[3]; /*爲分割值和交換值變量分配空間*/ if((pval = malloc(esize)) == NULL) return -1; if((temp = malloc(esize)) == NULL) { /*若是爲交換變量分配空間失敗,則將分割變量的空間一塊兒釋放掉*/ free(pval); return -1; } /*用中位數法找到分割值*/ r[0] = (rand()%(k-i+1))+i; r[1] = (rand()%(k-i+1))+i; r[2] = (rand()%(k-i+1))+i; /*調用插入排序函數對三個隨機數排序*/ issort(r,3,sizeof(int),compare_int); /*把排好序的三個數的中間值複製給分割值*/ memcpy(pval,&a[r[1]*esize,esize); /*圍繞分割值把數據分割成兩個分區*/ /*準備變量範圍,使i和k分割超出數組邊界*/ i--; k++; while(1) { /*k向左移動,直到找到一個小於或等於分割值的元素,這個元素處於錯誤的位置*/ do { k--; } while(compare(&a[k*esize],pval)>0); /*i向右移動,直到找到一個大於或等於分割值的元素,這個元素處於錯誤的位置*/ do { i++; } while(compare(&a[i*esize],pval)<0); /*直到i和k重合,跳出分區,不然交換處於錯誤位置的元素*/ if(i >= k) { break; } else { memcpy(temp, &a[i*esize], esize); memcpy(&a[i*esize], &a[k*esize], esize); memcpy(&a[k*esize], temp, esize); } } /*釋放動態分配的空間*/ free(pval); free(temp); /*返回兩個分區中間的分割值*/ return k; } /*qksort 快速排序函數*/ int qksort(void *data, int size, int esize, int i, int k, int(*compare)(const void *key1, const void *key2)) { int j; /*遞歸地繼續分區,直到不能進一步分區*/ while(i < k) { /*決定從何處開始分區*/ if((j = partition(data,esize,i,k,compare))<0) return -1; /*遞歸排序左半部分*/ if(qksort(data,size,esize,i,j,compare) < 0) return -1; /*遞歸排序右半部分*/ i=j+1; } return 0; }
在一個層次結構的文件系統中,文件一般分目錄進行組織。在任何一個目錄中,咱們會看到此目錄包含的文件列表和子目錄。例如,在UNIX系統中,能夠經過命令ls來顯示目錄。在windows的命令行中,經過dir來顯示目錄。
本節展現一個函數directls,它能實現與ls一樣的功能。它調用系統函數readdir來建立path路徑中指定的目錄列表。directls默認將文件按照名字排序,這一點與ls同樣。因爲在創建列表時調用了realloc來分配空間,所以一旦再也不使用列表時,也須要用free來釋放空間。
directls的時間複雜度爲O(nlgn),其中n爲目錄中要列舉的條目數。這是由於調用qksort來對條目進行排序是一個O(nlgn)級的操做,因此總的來講,遍歷n個目錄條目是一個O(n)級別的操做。
示例:獲取目錄列表的頭文件
/*directls.h*/ #ifndef DIRECTLS_H #define DIRECTLS_H #include <dirent.h> /*爲目錄列表建立一個數據結構*/ typedef struct Directory_ { char name[MAXNAMLEN+1]; }Directory; /*函數接口定義*/ int directls(const char *path,Directory **dir); #endif // DIRECTLS_H
示例:獲取目錄列表的實現
/*directls.c*/ #include <dirent.h> /*是POSIX.1標準定義的unix類目錄操做的頭文件,包含了許多UNIX系統服務的函數原型,例如opendir函數、readdir函數. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "directls.h" #include "sort.h" /*compare_dir 目錄比較*/ static int compare_dir(const void *key1,const void key2) { int retval; if((retval = strcmp(((const Directort *)key1)->name,((const Directory *)key2)->name))>0) return 1; else if (retval < 0) return -1; else return 0; } /*directls*/ int directls(const char *path,Directory **dir) { DIR *dirptr; Directory *temp; struct dirent *curdir; int count,i; /*打開目錄*/ if((dirptr = opendir(path)) == NULL ) return -1; /*獲取目錄列表*/ *dir = NULL; count =0; while((curdir = readdir(dirptr)) != NULL /*readdir()返回參數dir目錄流的下個目錄進入點*/ ) { count ++; if((temp = (Directory*)realloc(*dir,count*sizeof(Directory))) == NULL) { free(*dir); return -1; } else { *dir = temp; } strcpy(((*dir)[count - 1]).name, curdir->d_name); } closedir(dirptr); /*將目錄列表按名稱排序*/ if(qksort(*dir,count,sizeof(Directory),0,count-1,compare_dir) != 0) return -1; /*返回目錄列表的數目*/ return count; }
歸併排序也是一種運用分治法排序的算法。與快速排序同樣,它依賴於元素之間的比較來排序。可是歸併排序須要額外的存儲空間來完成排序過程。
咱們仍是以支票排序的例子說明。首先,將一堆未排序的支票對半分爲兩堆。接着,分別又將兩堆支票對半分爲兩堆,以此類推,重複此過程,直到每一堆支票只包含一張支票。而後,開始將堆兩兩合併,這樣每一個合併出來的堆就是兩個有序的合集,也是有序的。這個合併過程一直持續下去,直到一堆新的支票生成。此時這堆支票就是有序的。
因爲歸併排序也是一種分治算法,所以能夠使用分治的思想把排序分爲三個步驟:
一、分:將數據集等分爲兩半;
二、治:分別在兩個部分用遞歸的方式繼續使用歸併排序法;
三、合:將分開的兩個部分合併成一個有序的數據集。
歸併排序與其餘排序最大的不一樣在於它的歸併過程。這個過程就是將兩個有序的數據集合併成一個有序的數據集。合併兩個有序數據的過程是高效的,由於咱們只須要遍歷一次便可。根據以上事實,再加上該算法是按照可預期的方式來劃分數據的,這使得歸併排序在全部的狀況下都能達到快速排序的平均性能。
歸併排序的缺點是它須要額外的存儲空間來運行。由於合併過程不能在無序數據集自己中進行,因此必需要有兩倍於無序數據集的空間來運行算法。這點不足極大的下降了歸併排序在實際中的使用頻率,由於一般可使用不須要額外存儲空間的快速排序來代替它。
然而,歸併排序對於處理海量數據處理仍是很是有價值的,由於它可以按預期將數據集分開。這使得咱們可以將數據集分割爲更加可管理的數據,接着用歸併排序法處理數據,而後不斷的合併數據,在這個過程當中並不須要一次存儲全部的數據。
mgsort
int mgsort(void *data, int size, int esize, int i, int k, int (*compare)(const void *key1,const void key2));
返回值:若是排序成功,返回0;不然,返回-1。
描述: 利用歸併排序將數組data中的元素進行排序。數據中的元素個數由size決定。每一個元素的大小由esize決定。i和k定義當前排序的兩個部分,其值分別初始化爲0和size-1。函數指針compare指向一個用戶定義的函數來比較元素的大小。其函數功能同issort中描述的同樣。當mgsort返回時,data中包含已經排好序的元素。
複雜度:O(n lg n),n爲要排序的元素個數。
歸併排序本質上是將一個無序數據集分割成許多個只包含一個元素的集,而後不斷地將這些小集合並,直到一個新的大有序集生成。在如下介紹的實現方法中,data最初包含size個無序元素,並放在單塊連續的存儲空間中。由於歸併過程須要額外的存儲空間,因此函數要爲合併過程分配足夠的內存。在函數返回後,最終經過合併獲得的有序數據集將會拷貝回data。
歸併排序最關鍵的部分是如何將兩個有序集合併成一個有序集。這部分工做交由函數merge完成。它將data中i到j之間的數據集與j+1到k之間的數據集合併成一個i到k的有序數據集。
最初,ipos和jpos指向每一個有序集的頭部。只要數據集中還有元素存在,合併過程就將持續下去。若是數據集中沒有元素,進行以下操做:若是一個集合沒有要合併的元素,那麼將另一個集合中要合併的元素所有放到合併集合中。不然,首先比較兩個集合中的首元素,判斷哪一個元素要放到合併集合中,而後將它放進去,接着根據元素來自的集合移動ipos或jpos的位置(如圖4),依此類推。
如今咱們來看看mgsort中如何來處理遞歸。在初次調用mgsort時,i設置爲0,k設置爲size-1。首先,分割data,此時j處於數據中間元素的位置。而後,調用mgsort來處理左邊分區(從i到j)。左邊的分區繼續遞歸分割,直到傳入mgsort的一個分區只包含單個元素。在此過程當中,i再也不小於k,所以調用過程終止。在前一個mgsort的過程當中,在分區的右邊也在調用mgsort,處理的分區從j+1到k。一旦調用過程終止,就開始歸併兩個數據集。總的來講,以這種遞歸方式繼續,直到最後一次歸併過程完成,此時數據就徹底排好序了。
將數據集不斷地對半分割,在分到每一個集合只有一個元素前,須要lgn級分割(n爲要排序的元素個數)。對於兩個分別包含q和p個元素的有序集來講,歸併耗費的時長爲O(p+q),由於產生了一個合併的集,必須遍歷兩個集的每一個元素。因爲對應每一個lgn級的分割,都須要遍歷n個元素合併該集,所以歸併排序的時間複雜度爲O(nlgn)。又由於歸併排序須要額外的存儲空間,因此必需要有兩倍於要排序數據的空間來處理此算法。
示例:歸併排序的實現
/*mgsort.c*/ #include <stdlib.h> #include <string.h> #include "sort.h" /*merge 合併兩個有序數據集*/ static int merge(void *data, int esize, int i, int j, int k, int (*compare)(const void *key1,const void *key2)) { char *a = data, *m; int ipos ,jpos,mpos; /*初始化用於合併過程當中的計數器*/ ipos = i; jpos = j+1; mpos = 0; /*首先,爲要合併的元素集分配空間*/ if((m = (char *)malloc(esize * ((k-i)+1))) == NULL) return -1; /*接着,只要任一有序集有元素須要合併,就執行合併操做*/ while(ipos <= j || jpos <=k) { if(ipos > j) { /*左集中沒有元素要合併,就將右集中的元素放入目標集(合併集)*/ while(jpos <= k) { memcpy(&m[mpos * esize],&a[jpos * esize],esize); jpos++; mpos++; } continue; } else if(jpos > k) { /*右集沒有要合併的元素,就將左集中的元素放入目標集(合併集)*/ while(ipos <= j) { memcpy(&m[mpos * esize],&a[ipos *esize],esize); ipos++; mpos++; } continue; } /*追加下一個有序元素到合併集中*/ if(compare(&a[ipos * esize],*a[jpos *esize])<0) { memcpy(&m[mpos * esize],&a[ipos * esize],esize); ipos++; mpos++; } else { memccpy(&m[mpos * esize],&a[jpos * esize],esize); jpos++; mpos++; } } /*將已經排序的數據集拷貝到原數組中*/ memcpy(&a[i * esize],m,esize * ((k-i)+1)); /*釋放爲排序分配的存儲空間*/ free(m); return 0; }/*mgsort 歸併排序(遞歸調用)*/int mgsort(void *data, int size, int esize, int i, int k, int(*compare)(const void *key1,const void *key2)){ int j; /*遞歸調用mgsort持續分割,直到沒有能夠再分割的數據集*/ if(i < k) { /*計算對半分割的位置下標*/ j = (int)(((i+k-1)) / 2); /*遞歸排序兩邊的集合*/ if(mgsort(data, size, esize, i, j, compare) < 0) return -1; if(mgsort(data, size, esize, j+1, k, compare) <0) return -1; /*將兩個有序數據集合併成一個有序數據集*/ if(meger(data, esize, i, j, k compare) < 0) return -1; }return 0;}