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

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