iOS標準庫中常用資料結構和演算法之排序

歐陽大哥2013發表於2019-04-22

上一篇:iOS系統中的常用資料結構之連結串列

?排序

排序是指將亂序陣列變為有序排列的處理。iOS提供了快速排序、堆排序、歸併排序、並行排序、基數排序一共5種排序函式。具體每種排序的概念介紹請大家參考相關的文件這裡就不再贅述了。下面的表格將會從時間複雜度、穩定性、是否需要分配額外記憶體、是否對有序陣列進行優化、 應用範圍、平臺支援6個維度來考察各種排序函式:

排序演算法 時間複雜度 是否穩定 是否需要分配額外記憶體 是否對有序陣列進行優化 應用範圍 平臺支援
快速排序 N*logN 遞迴棧記憶體 任意陣列 POSIX
堆排序 N*logN 任意陣列 BSD UNIX/iOS
歸併排序 N*logN 任意陣列 BSD UNIX/iOS
並行排序 N*logN 任意陣列 iOS
穩定基數排序 N+D 位元組串 BSD UNIX/iOS
不穩定基數排序 N+D 位元組串 BSD UNIX/iOS
一、快速、堆、歸併、並行排序

功能:這四類排序函式的引數都是一致的,所以列在一起進行介紹。

標頭檔案:#include <stdlib.h>

平臺:qsort被POSIX支援、psort為iOS獨有、其他的都被BSD Unix支援

函式簽名

//快速排序
void qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));
//快速排序block版本
void qsort_b(void *base, size_t nel, size_t width, int (^compar)(const void *, const void *));
//快速排序附加引數版本
void qsort_r(void *base, size_t nel, size_t width, void *thunk, int (*compar)(void *, const void *, const void *));

//堆排序
int heapsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));
//堆排序block版本
int heapsort_b(void *base, size_t nel, size_t width, int (^compar)(const void *, const void *));

//歸併排序
int mergesort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));
//歸併排序block版本
int mergesort_b(void *base, size_t nel, size_t width, int (^compar)(const void *, const void *));

//並行排序
void psort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));
//並行排序block版本
void psort_b(void *base, size_t nel, size_t width, int (^compar)(const void *, const void *));
//並行排序附加引數版本
void psort_r(void *base, size_t nel, size_t width, void *thunk, int (*compar)(void *, const void *, const void *));


複製程式碼

引數

base:[in/out] 參與排序的陣列的首地址。

nel:[in] 陣列的元素的個數。

width:[in] 陣列中每個元素的尺寸。

compar: [in] 函式比較器,排序時會通過對陣列中的兩個元素呼叫函式比較器來判斷排序的順序。函式比較器的格式如下:

/*
@thunk: 函式比較器的附加引數,其值就是上述的帶附加引數版本的排序函式的thunk引數。
@element1, element2: 元素在陣列中的地址,這裡需要注意的是這個引數不是元素本身,而是元素所在的陣列中的偏移地址。
@return: 如果比較結果相等則返回0, 如果element1在element2前返回小於0,如果element1在elemen2後面則返回大於0
*/
 int compar(const void *element1, const void *element2);
 //帶附加引數版本
 int compar(void *thunk, const void *element1, const void *element2);
複製程式碼

return:[out] 對於堆排序和歸併排序來說有可能會排序失敗,如果排序成功會返回0否則返回非0,其他幾個函式則一定會排序成功。

描述

  1. qsort函式是用於快速排序的函式,採用的是C.A.R. Hoare 所實現的快速排序演算法。快速排序是一種不穩定排序,排序速度最快,平均時間複雜度為O(N*logN),因為其並未對有序陣列進行優化處理,因此最差的時間可能是O(N^2)。快速排序內部採用遞迴的機制進行排序,因此沒有額外的記憶體分配,當然如果陣列元素數量眾多則過度的遞迴可能會導致棧溢位,因此其內部實現如果超過了約定的遞迴次數後就會轉化為堆排序。

  2. heapsort函式是用於堆排序的函式,採用的是J.W.J. William所實現的堆排序演算法。堆排序是一種不穩定排序,其時間複雜度比較穩定為O(N*logN)。堆排序對有序陣列進行優化處理。堆排序進行排序時幾乎沒有附加記憶體的分配和消耗處理。

  3. mergesort函式是用於歸併排序的函式,歸併排序是一種穩定的排序,平均時間複雜度為O(N*logN), 因為其對有序陣列進行了優化處理,因此最好的時間可能達到O(N)。歸併排序的缺點是有可能會在排序實現內部分配大量的額外記憶體(排序陣列的尺寸),所以不適合用在陣列元素過多的排序中。

  4. psort函式是用於並行排序的函式,這函式是iOS系統獨有的函式。並行排序也是一種不穩定的排序。當陣列的元素數量小於2000或者CPU是單核時並行排序內部使用快速排序qsort來實現,而當數量大於2000並且是多核CPU時系統內部會開闢多執行緒來執行並行的排序處理。因此當數量眾多而且又希望能並行處理時可以用這個函式來進行排序,當然缺點就是排序時有執行緒建立和排程的開銷。

  5. 上述的排序函式有_r結尾的表明是帶有附加引數的排序函式,這樣在比較器中就可以使用這個附加引數,從而實現一些擴充套件的能力,這個就和帶_b結尾的用block進行比較的元素比較能力是一樣。

示例程式碼:

int compar(const void *element1, const  void *element2)
{
   //注意這裡的element1,element2是元素在陣列中的指標而非元素本身
    const char *p1 = *(const char **)element1;
    const char *p2 = *(const char **)element2;
    return strcmp(p1, p2);
}

void main()
{
     char *arr[6] = {"Bob","Max","Alice","Jone","Lucy","Lili"};
    
      qsort(arr, sizeof(arr)/sizeof(char*), sizeof(char*), compar); 
      heapsort(arr, sizeof(arr)/sizeof(char*), sizeof(char*), compar); 
      mergesort(arr, sizeof(arr)/sizeof(char*), sizeof(char*), compar); 
      psort(arr, sizeof(arr)/sizeof(char*), sizeof(char*), compar); 
}
複製程式碼
二、基數排序

功能:基數排序是利用了排序元素取值的一些限制來進行排序的排序方式。因此基數排序並不能適用於任何的資料結構。就以系統提供的函式來說,目前只支援基於位元組串陣列(位元組串包括字串)的排序。系統為基數排序分別提供了穩定和非穩定兩種版本的排序函式。要想更加詳細的瞭解基數排序請參考相關的文件。

標頭檔案:#include <stdlib.h>, #include <limits.h>

平臺:BSD Unix

函式簽名

//基數排序非穩定版
int radixsort(const unsigned char **base, int nmemb, const unsigned char *table,unsigned endbyte);
//基數排序穩定版,穩定版排序會有double的附加記憶體分配
int sradixsort(const unsigned char **base, int nmemb, const unsigned char *table,unsigned endbyte);
複製程式碼

引數

base: [in/out]: 位元組串陣列指標。

nmemb:[in] 位元組串陣列元素個數。

table:[in] 可以傳NULL或者一個長度為256的陣列。因為一個位元組符號的編碼取值範圍是0-255,所以這個表中的每個元素的值就表明每個位元組符號的比重值,其取值也是0-255。這個表用來決定基數字節串陣列的排序是升序還是降序,如果表中的值分別是從0到255那麼位元組串就按升序排列,如果表中的值分別是從255到0則表示按降序排列。同時這個表還可以用來決定是否對字元的大小寫敏感,舉例來說對於字元A-Z以及a-z的位元組編碼值不一樣,因此如果table中對應位置的比重值不一樣那麼就表示是大小寫敏感,而如果將表中對應位置的比重值設定為一樣,那麼就可以實現大小寫不敏感的排序了。具體的對table的使用將會在下面的例子中有詳細說明。如果我們不想自定義排序規則那麼將這個引數傳遞NULL即可表明按升序進行排序。

endbyte:[in] 每個位元組串的結尾位元組值,因為基數排序不侷限於字串,也可以用在位元組串上,所以需要有一個標誌來標識每個位元組或者字串是以什麼位元組結尾的。預設情況下的字串一般都是以'\0'結尾,所以這個引數對於常規字串來說傳0即可。

return:[out] 返回排序成功與否,成功返回0,否則返回其他。

功能

  1. 基數排序只能對位元組串陣列進行排序,而不能對任意的資料結構進行排序處理,因此其排序具有一定的侷限性。基數排序的時間複雜度為O(N+D),這裡的D是指待排序位元組串中最長的位元組串的長度,因此基數排序幾乎接近於線性時間的長度了。
  2. 基數排序中的table表決定著基數排序的排序順序和結果。這個表所表達的每個位元組編碼的比重值。因為位元組的編碼是從0到255,而預設的每個位元組的比重值和編碼值相等,這樣就表明著位元組串將按照編碼的大小進行升序排列。
  3. 基數排序分為穩定版本和不穩定版本,二者的區別就是當值相同時,是否會位置保持而不被交換。穩定版基數排序的一個缺點就是會產生雙倍大小的額外記憶體分配。

示例程式碼1

void main()
{
   char *arr[6] = {"Bob","Max","Alice","ada","lucy","Lili"};
    
    //預設升序排列
    radixsort(arr, sizeof(arr)/sizeof(char*), NULL, '\0');
    
    //降序排列,這裡需要構建table表,其比重順序變為由大到小。
    unsigned char table1[UCHAR_MAX + 1] = {0};
    for (int i = 0; i < UCHAR_MAX + 1; i++)
        table1[i] = UCHAR_MAX - i;    //每個位元組編碼位置的比重值由大到小
    radixsort(arr, sizeof(arr)/sizeof(char*), table1, '\0');
    
    
    //大小寫不敏感升序排序,這裡需要構建table表,將大寫字母和小寫字母的比重值設定為一致。因為上面的排序內容只有字母符號所以只需要修改字母符號位置的比重值即可。
    unsigned char table2[UCHAR_MAX + 1] = {0};
    for (int i = 'A';i <= 'Z'; i++)
    {
        table2[i] = i;
        table2[i+32] = i;  //小寫部分的比重值也設定和大寫部分的比重值一致。
    }
    radixsort(arr, sizeof(arr)/sizeof(char*), table2, '\0');
}

複製程式碼

示例程式碼2

雖然基數排序正常情況下只能用於位元組串陣列進行排序,如果位元組串是某個結構體的成員時,我們希望整個結構體也跟著排序。這時候就需要進行結構體的特殊設計,我們需要將結構體的第一個資料成員設定為位元組串陣列即可實現將結構體來應用基數排序。具體的程式碼如下:

//對結構體的排序。要求字串作為結構體的第一個成員,而且字串成員必須是陣列,而不能是字串指標。
    typedef struct student
    {
        char name[16];    //結構體中字串必須以陣列的形式被定義並且作為第一個資料成員。
        int age;
    }student_t;
    
    student_t *a1 = malloc(sizeof(student_t));
    strcpy(a1->name, "Bob");
    a1->age = 10;
    
    student_t *a2 = malloc(sizeof(student_t));
    strcpy(a2->name, "Jone");
    a2->age = 15;
    
    student_t *a3 = malloc(sizeof(student_t));
    strcpy(a3->name, "Alice");
    a3->age = 12;
    
    student_t *a4 = malloc(sizeof(student_t));
    strcpy(a4->name, "Tom");
    a4->age = 12;
    
    student_t *a5 = malloc(sizeof(student_t));
    strcpy(a5->name, "Lucy");
    a5->age = 8;
    
    student_t *a6 = malloc(sizeof(student_t));
    strcpy(a6->name, "Lily");
    a6->age = 18;
    
    student_t *arr[6] = {a1,a2,a3,a4,a5,a6};
    radixsort(arr, 6, NULL, '\0'); 

複製程式碼

相關文章