前言:
说到排序,初学者脑海中冒出的第一个算法大多是冒泡排序,而此时我们大多只学过用它实现整形数据的排序,如下:
void bubble_sort(int arr[], int sz)
{
int i = 0;
int j = 0;
int flag = 1;//一个优化,当数组已经有序后,就不在继续比较
for (i = 1; i < sz; i++)//趟数=元素个数-1
{
for (j = 0; j < sz - i; j++)//一趟冒泡排序的过程=当前排序的元素个数-1
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j ] = arr[j+1];
arr[j + 1] = temp;
flag = 0;//如果发生了交换,则证明当前数组不是有序的
}
}
if (flag == 1)//说明没有发生交换,当前数组有序,直接跳出循环
{
break;
}
}
for (j = 0; j < sz; j++)
{
printf("%d ", arr[j]);
}
}
int main()
{
int arr[] = { 12,5,2,8,56,47,33,19 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
return 0;
}
但当我们要排序一组浮点数或者字符串甚至是结构体时,只使用冒泡排序好像是很难实现的,但我们其实依旧可以使用它的思想,一起来看看如何实现吧!
首先我们要学习一种用于排序的库函数qsort()
C语言的库函数中其实给我们提供了一个库函数叫qsort(),这是一个可以排序各种类型数据的函数,通过查阅资料,其函数声明如下:
void qsort(void* base,size_t num,size_t width,int(__cdecl*compare)(const void*,const void*))
我们分别来看看它的参数:
void* base:你要排序的数据起始位置
size_t num:待排序的数据元素个数
size_t width:待排序的数据元素的大小,单位字节
int(__cdecl*compare)(const void*e1,const void*e2):是一个函数指针(__cdecl不用管,是函数调用约定的),实质上是一个比较函数,为什么要有一个比较函数呢?实际上qsort这个函数可以排序任意数据类型的元素,而每一种数据类型的两两比较的方法肯定是不同的,那我们就要分别定义这些函数,在使用qsort()函数时,只需要把对应的比较函数的地址传过去即可,e1,e2分别表示你要比较的两个数据的地址
这里其实涉及到的就是回调函数,在C语言进阶中的指针部分也简单提到过
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
知道这些我们无论要排序什么类型的数据,只需要把对应的参数传过去,qsort就会根据你提供的比较函数帮你快速排序,我们先以整型为例:
//比较两个整型元素,e1指向一个整型,e2指向另一个整型
int cmp_int(const void* e1, const void* e2)
{
if (*(int*)e1 > *(int*)e2)
return 1;
else if (*(int*)e1 == *(int*)e2)
return 0;
else
return -1;
}
//简化
//int cmp_int(const void* e1, const void* e2)
//{
// return (*(int*)e1-*(int*)e2);
//}
int main()
{
int arr[] = { 12,5,2,8,56,47,33,19 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
代码解析:我们从主函数进入开始分析,首先给了我们一个整型数组,我们现在要利用qsor函数实现该组数据的排序,首先分别传入参数,数组名即该数组首元素地址(待排序数据的起始位置地址),sz(数组元素个数),sizeof(arr[0])(每个元素的大小),cmp_int(比较函数),其它的都不难理解,主要来理解这个比较函数。
int cmp_int(const void* e1, const void* e2) { if (*(int*)e1 > *(int*)e2) return 1; else if (*(int*)e1 == *(int*)e2) return 0; else return -1; }
首先,该函数定义时的返回值和参数须得满足qsort的规定
1、关于参数:既然e1和e2是两个数值的地址,那使用它是为何不是直接解引用,而要先强制转换其指针类型后再解引用?
原因:这个地方就涉及到了我们初阶指针中探讨指针类型意义的问题,因为qsort能排序不同数据类型,那比较函数函数定义为void*就能在接收数据地址时接收任意类型的地址,但void*指针在使用时不能直接解引用和+-整数(因为不知道类型所以不知道应该访问或者跳过多少字节的内存),因此在使用时应该先将其强制转换为相应数据的指针类型再解引用。
2、关于返回值:qsort对于函数的返回值有些规定,当e1>e2时应该返回正数,e1=e2时应该返回0,e1<e2时应该返回负数,(这里的><=表示的是我们想要的逻辑上各种数据的一个比较,不单纯表示数据大小),这我们可以利用选择语句来轻松实现。
优化:
int cmp_int(const void* e1, const void* e2)
{
return (*(int*)e1-*(int*)e2);
}
我们再来练习使用一下qsort排序结构体数据:
//完整代码
#include<stdlib.h>
#include<string.h>//strcmp的头文件
struct stu
{
char name[20];
int age;
};
int cmp_stu_by_name(const void* e1, const void* e2)//按姓名来排序
{
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);//strcmp用于比较两字符串大小,恰好其返回值就是>0,==0和<0
}
int cmp_stu_by_age(const void* e1, const void* e2)//按年龄来排序
{
return ((struct stu*)e1)->age - ((struct stu*)e2)->age;//年龄是整数,直接按照整数的方式返回
}
int main()
{
struct stu s[] = {{"zhangsan",12},{"lisi",9},{"wangwu",18}};//这是一个结构体数组
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s,%d\n", s[i].name, s[i].age);
}
printf("---------------------\n");
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
int j = 0;
for (j = 0; j < sz;j++)
{
printf("%s,%d\n", s[j].name, s[j].age);
}
return 0;
}
运行结果:
由此我们就大致了解了qsort()的使用方法,当然具体的排序思想我们目前并不知道,是库函数提供的,但我们可不可以利用冒泡排序的思想对照qsort()函数来实现自己的排序函数呢?这就回到了我们最初讨论的问题,用冒泡排序思想排序除整型数据以外其它类型的数据。
我们创建自己的排序函数bubble_qsort(),对照qsort的参数和返回值,对其声明如下:
void bubble_qsort(void* base,int sz,int width,int (*cmp_)(const void*e1,const void*e2))
根据冒泡排序的思想,设计函数如下:
void swap(char* buf1, char* buf2, int width) { int i = 0; for (i = 0; i < width; i++) { char temp = *buf1; *buf1 = *buf2; *buf2 = temp; buf1++; buf2++; } } void bubble_qsort(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2)) { int i = 0; int j = 0; int flag = 1; for (i = 1; i < sz; i++) { for (j = 0; j < sz - i; j++) { if (cmp((char*)base+j*width, (char*)base +( j+1)* width) > 0) swap((char*)base + j * width, (char*)base + (j + 1) * width, width); flag = 0; } if (flag == 1) break; } }
代码分析:首先整体的冒泡排序逻辑要有,不再赘述,我们直接来看两部分较难理解的
1、判断是否交换的条件:因为我们不知道传过来的数据的大小,因此我们直接将其强制转化为最小单位的字节,通过首元素地址加(下标*宽度width)来实现访问不同下标的元素
2、交换函数的设计:交换的时候,为了方便,我们采用将数据所占空间整体进行交换,因此我们同样把两个首元素所占空间的首字节地址传给交换函数,再传一个元素所占空间的大小width,从首字节开始交换width次即可实现预期功能。下面我们来分别测试用自己写的排序函数来排序上面提到过得整形数据和结构体数据。
完整代码如下,附有测试结果:
void swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char temp = *buf1;
*buf1 = *buf2;
*buf2 = temp;
buf1++;
buf2++;
}
}
void bubble_qsort(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2))
{
int i = 0;
int j = 0;
int flag = 1;
for (i = 1; i < sz; i++)
{
for (j = 0; j < sz - i; j++)
{
if (cmp((char*)base+j*width, (char*)base +( j+1)* width) > 0)
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0;
}
if (flag == 1)
break;
}
}
int cmp_int(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2);
}
void test1()
{
int arr[] = { 12,5,2,8,56,47,33,19 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_qsort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
#include<string.h>//strcmp的头文件
struct stu
{
char name[20];
int age;
};
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);//strcmp用于比较两字符串大小,恰好其返回值就是>0,==0和<0
}
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct stu*)e1)->age - ((struct stu*)e2)->age;//年龄是整数,直接按照整数的方式返回
}
void test2()
{
struct stu s[] = { {"zhangsan",12},{"lisi",9},{"wangwu",18} };//这是一个结构体数组
int sz = sizeof(s) / sizeof(s[0]);
bubble_qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s,%d\n", s[i].name, s[i].age);
}
printf("---------------------\n");
bubble_qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
int j = 0;
for (j = 0; j < sz; j++)
{
printf("%s,%d\n", s[j].name, s[j].age);
}
}
int main()
{
test1();//比较整型
test2();//比较结构体
return 0;
}
test1()测试结果:
test2()测试结果:
- 我们可以发现无论是qsort还是根据qsort自己设计的bubble_qsort都是默认按照逻辑升序排序的,那么可以降序吗?当然可以,只需要把比较函数里的逻辑简单调换一下即可,这里我们只用整形数据简单演示一下。
多看几遍就能更好的理解这种思想啦~~欢迎评论私信批评指正!