目录
2. 模拟实现qsort
使用qsort实现快速排序
C语言中比较常见的排序算法有两种:一是冒泡排序法,这种方法在之前的文章中有详细介绍过,这里就不做过多赘述。但是因为冒泡排序法只能对整型进行排序,显得比较局限,所以就引出了第二种方法——qsort快速排序法,接下来会对qsort做一个基本的了解。
qsort用前须知:qsort是用快速排序的方法实现,其是C语言中的个库函数,所以使用之前要引头文件<stdlib.h>。qsort相较于冒泡排序的算法就比较灵活,它不仅可以对整型数组进行排序,还可以对字符数组、浮点型数组、结构体数组等进行排序。
首先,简单介绍一个qsort的基本用法以及传入参数等,具体的实现通过下面的例子渗透了解。
注:使用的参考资料来自cplusplus网站,以下为该网站网址(里面会对C/C++中库函数做出详细介绍)cplusplus.com - The C++ Resources Networkhttp://www.cplusplus.com/
接下来,先以qsort排序整型数组作为引入。
#include <stdio.h>
#include <stdlib.h>
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
void test1()
{
int arr[] = { 3,4,6,1,9,8,0,2,5,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
}
int main()
{
test1();
return 0;
}
上面使用qsort库函数,短短的几行代码就可以实现整型数组排序,确实比冒泡排序简单了不少。这里再次强调,qsort函数的4个参数分别为数组首元素地址、数组元素个数、每个元素大小、指向两个比较元素的函数指针(这几个参数位置不能互换)。其中,上面代码中比较灵活的是cmp_int函数,因为在函数指针那里就已经规定了指向函数的返回值应该是大于等于或者小于0了,cmp_int函数中巧妙地将两个地址进行解引用操作再相减并返回,其返回值正好与规定的返回值相符。
因为上面频繁出现void*。这里,对void*做一个整理,了解void*是什么、用法以及一些注意事项等。
1. void* 是一种无具体类型的指针。
2. void* 的指针变量可以存放任意类型的地址。
3. 但是,void* 的指针不能直接进行解引用操作,也不能直接进行+-整型。
4. 所以,如果要对void* 进行上面的两种操作,需要先强制类型转换变成自己想要的类型再进行解引用操作。
通过上面qsort排序整型数组的基础,接着,就是qsort排序结构体数据了(先上代码再解释):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
float score;
};
int cmp_stu_by_score(const void* e1, const void* e2)
{
if (((struct Stu*)e1)->score > ((struct Stu*)e2)->score)
{
return 1;
}
else if (((struct Stu*)e1)->score < ((struct Stu*)e2)->score)
{
return -1;
}
else
{
return 0;
}
}
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void print_stu(struct Stu arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s %d %.2f\n", arr[i].name, arr[i].age, arr[i].score);
}
printf("\n");
}
void test2()
{
struct Stu arr[] = { {"张三",10,85.5f},{"李四",22,72.5f},{"王五",14,90.0f} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_score);
printf("按成绩排序\n");
print_stu(arr, sz);
}
void test3()
{
struct Stu arr[] = { {"张三",10,85.5f},{"李四",22,72.5f},{"王五",14,90.0f} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
printf("按年龄排序\n");
print_stu(arr, sz);
}
void test4()
{
struct Stu arr[] = { {"张三",10,85.5f},{"李四",22,72.5f},{"王五",14,90.0f} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
printf("按名字排序\n");
print_stu(arr, sz);
}
int main()
{
test2(); //按成绩排序
test3(); //按年龄排序
test4(); //按名字排序
return 0;
}
由上面代码不难看出,表面是对结构体数据进行排序,实际上是分别对浮点型、整型、字符串类型进行排序。所以也就印证了前面的说法:qsort是实现快速排序的方法,可排序整型数组、字符数组、浮点型数组、结构体数组等。
要理解并写出这样的代码需要对函数指针、数组、结构体、结构体指针等掌握地比较扎实。其中,此段代码值得讲的是:
1. 因为指向的这个函数的返回类型的int类型,所以在比较浮点数的时候,不能像整型那样直接返回两个整数相减的值,对此,只能老老实实地使用判断语句分成3部分进行返回。
2. 对于比较字符类型,也可以像浮点数那样分情况返回,但是,有一种更加简洁的做法,就是调用strcmp函数,因为strcmp是专门用来比较字符串的。在cplusplus中搜索这个函数也不难发现strcmp的返回值的形式与qsort返回值的形式如出一辙地相似,所以排序字符类型,就可以在指向的函数返回内容中直接使用strcmp。
模拟实现qsort
在宏观了解完qsort的用法等内容后,第二部分就是要深入理解qsort,也就是要来模拟实现qsort。
模拟实现qsort就需要用到上篇文章所讲到的回调函数,具体的代码如下(代码实现解释在后面):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
float score;
};
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_stu_by_score(const void* e1, const void* e2)
{
if (((struct Stu*)e1)->score > ((struct Stu*)e2)->score)
{
return 1;
}
else if (((struct Stu*)e1)->score < ((struct Stu*)e2)->score)
{
return -1;
}
else
{
return 0;
}
}
void Swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char ret = *buf1;
*buf1 = *buf2;
*buf2 = ret;
buf1++;
buf2++;
}
}
int My_qsort(void*base,int sz,int width,int (*cmp)(const void*e1,const void*e2))
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - 1; j++)
{
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width); //交换两元素
}
}
}
}
void print_stu(struct Stu arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s %d %.2f\n", arr[i].name, arr[i].age, arr[i].score);
}
printf("\n");
}
void test5()
{
struct Stu arr[] = { {"张三",10,85.5f},{"李四",22,72.5f},{"王五",14,90.0f} };
int sz = sizeof(arr) / sizeof(arr[0]);
My_qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name); //按名字排序
printf("按名字排序\n");
print_stu(arr, sz);
My_qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age); //按年龄排序
printf("按年龄排序\n");
print_stu(arr, sz);
My_qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_score); //按成绩排序
printf("按成绩排序\n");
print_stu(arr, sz);
}
int main()
{
test5();
return 0;
}
下面详细解释一下本代码是如何模拟实现qsort的:
先分别按名字、年龄、成绩传入自定义函数My_qsort中,由这个封装的函数来分别调用不同的函数,其中,My_qsort中的最后一个参数为函数指针,它不由本函数实现,而是先传到自定义函数My_qsort中再在此函数中调用,这就是上篇代码中说到的回调函数,这样写可以大量地减少代码的行数。接下来,就可以把封装出的My_qsort函数通过最后那个参数分别指向按不同类型排序的函数(这些函数与之前的函数一样)。其实,模拟实现qsort最大的难点就是如何理解这些传进去的参数,从而进一步理解My_qsort的原理,传入的参数中最难理解的又是width(也就是数组中一个元素的大小),那么,为什么要传入这个参数呢?
因为前面传入的是数组首元素地址,但是又不知道其是什么类型(int、char、float等),不同类型之间元素的大小是不一样的,所以,这里就会想到将元素的大小也传过去,这样就可以对不同类型内存中的虚拟地址进行判断,同时也便于后面对两个元素进行交换排序等。所以,函数指针中的参数就应该先强制类型转换为char*类型,再根据该类型每个元素的宽度进行对应的操作(这里其实到最后传的也是首元素地址,只是不知道类型,只能通过一个一个来找到第二个数),然后判断返回值是否大于0,如果大于0则说明前一个元素比后一个元素大,需要进行元素交换。交换过程也是需要传元素宽度过去,因为交换的元素也是不知道是什么类型,传首元素地址过去也是跟前面一样需要强制类型转换成char*类型,在传入的函数中以字节为单位进行交换,使用循环,直到全部交换完为止。
以上就是模拟实现qsort的全部过程,通过模拟实现qsort的方法也是跟冒泡排序的思想十分类似的。其实,C语言在创作这个库函数时候的思想就是运用冒泡排序的思想的,所以,就算是qsort实现快速排序的方法,其底层原理也是冒泡排序,也是对冒泡排序进行的一个推广,从只能排序整型到可以排序任何类型,变得更加实用。