FreeBSD中快速排序的實現qsort

FreeBSD中快速排序的實現qsort

首先,我們來看下FreeBSD中快速排序函數qsort怎麼使用。

官方給了個例子: https://www.freebsd.org/cgi/man.cgi?query=qsort&sektion=3&manpath=freebsd-release-ports

#include <stdio.h>
     #include <stdlib.h>

     /*
      * Custom comparison function that compares 'int' values through pointers
      * passed by qsort(3).
      */
     static int
     int_compare(const void *p1, const void *p2)
     {
     int left = *(const int *)p1;
     int right = *(const int *)p2;

     return ((left > right) - (left < right));
     }

     /*
      * Sort an array of 'int' values and print it to standard output.
      */
     int
     main(void)
     {
     int int_array[] = { 4, 5, 9, 3, 0, 1, 7, 2, 8, 6 };
     size_t array_size = sizeof(int_array) / sizeof(int_array[0]);
     size_t k;

     qsort(&int_array, array_size, sizeof(int_array[0]), int_compare);
     for (k = 0; k < array_size; k++)
     printf(" %d", int_array[k]);
     puts("");
     return (EXIT_SUCCESS);
     }

qsort函數的聲明:
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
有4個參數,第一個參數base是一個指向待排序數組的指針變量,第二個參數nmemb是數組的元素數量,第三個參數size是數組中一個元素的內存空間大小,
第四個參數是一個執行帶兩個指針參數的比較函數的指針,該調用的使用在上面的例子已列出。

接着,我們來看qsort函數的實現。
源代碼:https://github.com/freebsd/freebsd/blob/master/lib/libc/stdlib/qsort.c#L92

先看下函數的4個參數,a是指針,n是數組大小,es是數組元素空間大小,cmp是比較函數指針。

void
qsort(void *a, size_t n, size_t es, cmp_t *cmp)

接着研究下下面這些變量是什麼意思。

char *pa, *pb, *pc, *pd, *pl, *pm, *pn;
size_t d1, d2;
int cmp_result;
int swap_cnt;

後面兩個變量字面意思比較容易理解,cmp_result是比較結果的變量,swap_cnt是交換次數的變量。
學過快排的同學應該知道pivot樞紐元和partition分區的概念,從變量名上看,pa,pb,pc,pd應該是在分區過程中會用到的指針,
而pl,pm,pn可能是其他用處的指針,這個我們後續在看代碼中他們的使用,d1,d2後續到中可以看出是用來判斷先排序左分區,還是先排序右分區。

loop:
swap_cnt = 0;
if (n < 7) {
for (pm = (char *)a + es; pm < (char *)a + n * es; pm += es)
for (pl = pm; 
     pl > (char *)a && CMP(thunk, pl - es, pl) > 0;
     pl -= es)
swapfunc(pl, pl - es, es);
return;
}

loop這個標籤和goto配合用來循環,swap_cnt每次循環都初始化爲0代表交互次數從0開始計數。
n < 7代表數組數量小於7,那麼做插入排序,不過這個插入排序和平常用到的插入排序略有區別,
同時這裏大家看到pl,pm,pn第一使用在這個插入排序算法裏面。

通常大家用插入排序是把當前的元素作爲要插入的元素同時記錄下這個值,然後和前一個元素比較,
如果前一個元素大,那麼把前一個元素插入當前位置,指針繼續向前移動,找前一個元素和要插入的元素繼續比較,
如此循環,直到碰到前一個元素比要插入的元素小;最後如果當前位置和要插入的位置不一樣,那麼把要插入的元素的值存入指針當前的位置。
代碼可以參考: https://www.tutorialspoint.com/data_structures_algorithms/insertion_sort_program_in_c.htm
而FreeBSD中的這個插入排序是用交換元素值的方式,將插入操作都變成交換操作。

pl = pm//把當前的元素作爲要交換的元素同時記錄下這個值,
CMP(thunk, pl - es, pl) > 0//然後和前一個元素比較,
swapfunc(pl, pl - es, es);//如果前一個元素大,那麼把兩個元素交互,
pl -= es//指針繼續向前移動,
CMP(thunk, pl - es, pl) > 0//找前一個元素和要交換的元素繼續比較,
如此循環,直到碰到前一個元素比要交換的元素小或者指針已經到數組起始位置pl > (char *)a

這個算法相當於結合了插入排序的已排序機制和冒泡排序的交換機制。

接着,我們繼續看當n>7的情況。

pm = (char *)a + (n / 2) * es;
if (n > 7) {
pl = a;
pn = (char *)a + (n - 1) * es;
if (n > 40) {
size_t d = (n / 8) * es;

pl = med3(pl, pl + d, pl + 2 * d, cmp, thunk);
pm = med3(pm - d, pm, pm + d, cmp, thunk);
pn = med3(pn - 2 * d, pn - d, pn, cmp, thunk);
}
pm = med3(pl, pm, pn, cmp, thunk);
}

pm = (char *)a + (n / 2) * es;//相當於取數組中間位置的元素
n<40時會進行9個數據中值採樣,如下:
pl = med3(pl, pl + d, pl + 2 * d, cmp, thunk);//這個是取3個位置的中間數值(三位中值法)
pm = med3(pm - d, pm, pm + d, cmp, thunk);//繼續三位中值法採樣
pn = med3(pn - 2 * d, pn - d, pn, cmp, thunk);//繼續三位中值法採樣
最終再對pl,pm,pn進行三位中值法採樣pm = med3(pl, pm, pn, cmp, thunk);

得到的最終樞紐元pm會和數組第一個元素交換:swapfunc(a, pm, es);
然後指針pa,pb指向數組a的開頭,pc,pd指向數組a的結尾。

pa = pb = (char *)a + es;
pc = pd = (char *)a + (n - 1) * es;

開始快速排序算法的取樞紐元部分:

for (;;) {
while (pb <= pc && (cmp_result = CMP(thunk, pb, a)) <= 0) {
if (cmp_result == 0) {
swap_cnt = 1;
swapfunc(pa, pb, es);
pa += es;
}
pb += es;
}
while (pb <= pc && (cmp_result = CMP(thunk, pc, a)) >= 0) {
if (cmp_result == 0) {
swap_cnt = 1;
swapfunc(pc, pd, es);
pd -= es;
}
pc -= es;
}
if (pb > pc)
break;
swapfunc(pb, pc, es);
swap_cnt = 1;
pb += es;
pc -= es;
}

這部分算法和我們平常用的快排略有區別,我們平常用的只有兩個指針,而這裏用來4個指針來提高效率。
兩個指針的例子:https://www.tutorialspoint.com/data_structures_algorithms/quick_sort_program_in_c.htm

cmp_result = CMP(thunk, pb, a)//數組a第一元素作爲樞紐元,如果指針pb在向右移動過程中有值和樞紐元相等,那麼
交換pb和pa的值,同時交換計數器swap_cnt設置爲1,並且pa向右移動一個元素:

if (cmp_result == 0) {
swap_cnt = 1;
swapfunc(pa, pb, es);
pa += es;
}

最後交換pb和pc的元素值,交換計數器swap_cnt設置爲1,並且pb向右移動一個元素,pc向左移動一個元素;
直到pb > pc,循環結束。

如果循環結束時,沒有任何交換,那麼切換到插入排序:

if (swap_cnt == 0) {  /* Switch to insertion sort */
for (pm = (char *)a + es; pm < (char *)a + n * es; pm += es)
for (pl = pm; 
     pl > (char *)a && CMP(thunk, pl - es, pl) > 0;
     pl -= es)
swapfunc(pl, pl - es, es);
return;
}

pn指針指向最後一個元素,d1是取“pa指針之前元素個數”與“pb,pa之間間隔元素個數”中最小值,
如果d1>0,交換a的第一個元素和“pb - d1”位置的元素,
d1是取“pd,pc之間間隔元素個數”與“pn,pd之間間隔元素個數減一個元素”中最小值,
如果d1>0,交換pb位置的元素和“pn - d1”位置的元素,

pn = (char *)a + n * es;
d1 = MIN(pa - (char *)a, pb - pa);
vecswap(a, pb - d1, d1);
d1 = MIN(pd - pc, pn - pd - es);
vecswap(pb, pn - d1, d1);

根據本輪快速排序完,如果左邊兩個指針pb,pa間距元素個數小於等於左邊兩個指針pd,pc間距元素個數
並且左邊兩個指針pb,pa間距元素個數大於一個元素,則遞歸左分區,然後迭代右分區。

d1 = pb - pa;
d2 = pd - pc;
if (d1 <= d2) {
/* Recurse on left partition, then iterate on right partition */
if (d1 > es) {
#ifdef I_AM_QSORT_R
qsort_r(a, d1 / es, es, thunk, cmp);
#else
qsort(a, d1 / es, es, cmp);
#endif
}
if (d2 > es) {
/* Iterate rather than recurse to save stack space */
/* qsort(pn - d2, d2 / es, es, cmp); */
a = pn - d2;
n = d2 / es;
goto loop;
}
}

至此整個快排的實現已分析完。

但這種實現並非沒有任何問題,如果有針對的生成序列,使其滿足swap_cnt == 0,那麼快排將切換到插入排序,這樣時間複雜度從O(nlog n)變成O(n^2)。
關於這個問題,可以參考這篇針對快排攻擊的文章:http://calmerthanyouare.org/2014/06/11/algorithmic-complexity-attacks-and-libc-qsort.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章