目录
1 . 基础版的冒泡排序
我们先来看一下代码
void bubble_sort(int* arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)
{
for (j = 0; j < sz - 1 - i; j++)
{
if(arr[j] > arr[j + 1])
{
int tmp = 0;
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
我们如果要排序的话,就需要给出一组数吧,那么我们先 int arr[ ] = { } 定义出一组数,我们定义出来之后,是不是就该就去实现冒泡函数啦。
那么我们先将该函数命名为bubble_sort吧,既然要调用函数,那我们是不是应该将函数的地址(也就是函数名)传过去,再在bubble_sort函数中用一个指针来接收啊,同时我们再将数组的元素个数传过去(此处用sz来命名元素个数),用int sz接收
而在使用完bubble_sort函数之后我们就用一个for循环将排序后的arr数组打印一下
如下:
void bubble_sort( int* arr ,int sz)
{
}
int main()
{
int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
接下来我们来重点看看bubble_sort函数的实现
首先,冒泡排序是怎么个冒泡法呢?这样来看:假设我现在要让一组数从小到大进行排列,而我给出的是
9,8,7,6,5,4,3,2,1
而冒泡完一趟之后,就变成了:
8,7,6,5,4,3,2,1,9
接着进行第二趟冒泡:
7,6,5,4,3,2,1,8,9
......
最后就变成了
1,2,3,4,5,6,7,8,9
他的机制是这样的: 先让第一个数和第二个数进行比较,如果第一个数字比第二个数字大,就将其进行交换。交换完之后再让第二个数字与第三个数字进行比较,以此类推......
每一趟大概是这样的:
9,8,7,6,5,4,3,2,1 ->
8,9,7,6,5,4,3,2,1 ->
8,7,9,6,5,4,3,2,1 ->
......
8,7,6,5,4,3,2,1,9
所以当我们知道冒泡是这么一个东西之后,我们就想:一趟冒泡是这样的,那如果我要将这一整组数字排完序,那不就需要9-1趟吗?(注:9是数组的元素个数)那我们能否用先用一个for循环表示一共有多少趟呢?然后再在这个for循环里面加一个for循环表示每一趟for循环?答案是肯定的。
但是问题来了:我们第一层for循环知道限制条件是 "个数 - 1",也就是sz - 1,那我们的第二层呢?每一趟冒泡排序之后就有一个数不用再被排序,那我们就将这个条件设置为 (sz - 1) - i,你想啊,i 代表的是每一趟,到了第几趟 i 就等于几,也就相当于有多少个数不用再被排序
代码如下:
void bubble_sort(int* arr, int sz)
{
int i = 0;
int j = 0;
//第一层for循环表示有多少趟冒泡
for (i = 0; i < sz - 1; i++)
{
//第二层for循环表示每一趟冒泡
for (j = 0; j < sz - 1 - i; j++)
{
//语句
}
}
}
如果没法理解for循环嵌套的话,那我们这么看:我现在打印一个3*5的数字矩阵,每一行都是1,2,3,4,5。如下:
int main()
{
int i = 0;
int j = 0;;
//第一层每一次循环代表一行
//当i=1时代表第一行,i=2时代表第二行...
for (i = 1; i <= 3; i++)
{
//第二层代表每一行的每个数字
//i=1时,j=1代表第一行第一个数,j=2代表第一行第二个数...
for (j = 1; j <= 5; j++)
{
printf("%d ", j);//打印结果
}
printf("\n");//每打印一行之后就用\n换行
}
return 0;
}
结果如下图所示
那我们讲完for循环的嵌套之后就继续来看一看bubble_sort的实现
这时候我们已经用for循环的嵌套表示出了每一趟冒泡,那我们想一想:我每一趟冒泡是不是都需要将两个数字进行对比啊,而现在要对比的两个数就是 arr[ i ] 和 arr[ i + 1 ]。那我们就先定义出一个tmp变量,将arr[ i ] 放进去,再将 arr[ i + 1 ] 放进 arr[ i ] 里,最后把 tmp 放进 arr[ i + 1 ] 里就实现了交换这个过程啦!!代码如下:
void bubble_sort(int* arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)
{
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = 0;
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
2 . 用了qsort的冒泡排序(使用qsort函数)
在学完了基础版的冒泡排序之后,相信你对冒泡排序也算是有了一个基本的概念了。但是仅仅是用分支与循环去实现冒泡的话,我们会发现这个冒泡有很大的局限性:如果我此时定义的不是一个整型数组,而是一个浮点型的数组呢?如果是一串结构体呢?你会发现这排不了,仅用分支与循环的内容的话只能给整形排序。
那我们这里介绍一个库函数叫 qsort ,需要引的头文件是#include<stdlib.h>,q 代表的是quick,sort是排序,所以qsort代表的就是快速排序。使用qsort就能实现其他的排列方式
qsort(因为代码比较长,所以换了一行)
void qsort (void* base, size_t num, size_t size,
int (*compar)(const void*,const void*));
注:如图内容可cplusplus.com - The C++ Resources Network 上找到
我们简单解释一下这里面的内容:qsort函数在调用时需要传四个参数
假设我们定义的数组是float arr[ ] = { 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0 }
int cmp_float(const void* e1, const void* e2)
{
return ((int)(*(float*)e2 - *(float*)e1));
}
1 . base Pointer to the first object of the array to be sorted, converted to a void*(指向要排序的数组中第一个对象的指针,转换为 void*),因为我们假定的数组是浮点型的,那么我们的第一个参数就是 arr
2 . num Number of elements in the array pointed to by base.size_t is an unsigned integral type . 简单点理解:第二个参数就是数组的元素个数。根据上文,我们可以用 int sz = sizeof (arr) / sizeof(arr[ 0 ]) 来进行实现
3 . size Size in bytes of each element in the array.size_t is an unsigned integral type. 简单点理解:第三个参数就是该数组每个元素所占的字节的大小,这里我们传 sizeof( arr[ 0 ] ) 就行了
4 . compar Pointer to a function that compares two elements.This function is called repeatedly by qsort to compare two elements. It shall follow the following prototype: ( 指向比较两个元素的函数的指针。qsort 反复调用此函数以比较两个元素 )简单点理解:既然qsort函数可以将不同类型的数组进行排序,而不同类型之间的比较方法又不一样,那就需要我们将比较的方法写成一个函数,再将这个函数的地址传进来就可以啦。这里我们传如上所示代码里的函数的地址:cmp_float
注:(*cmp_float)和cmp_float是一个东西
那我们在了解了之后,我们就能得到如下代码:
int cmp_float(const void* e1, const void* e2)
{
return ((int)(*(float*)e2 - *(float*)e1));
}
void test()
{
float f[] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 };
int sz = sizeof(f) / sizeof(f[0]);
qsort(f, sz, sizeof(f[0]), cmp_float);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%.1f ", f[i]);
}
}
int main()
{
test();
return 0;
}
我们来理一理逻辑:在test函数内部先是创建了一组待排序的浮点数,计算出了数组的元素个数sz,然后通过qsort函数进行排序,最后用for循环将该数组内的数全部打印出来。
接下来我们来重点说说这第四个参数:
int cmp_float(const void* e1, const void* e2)
{
//语句
}
//假设我们定义的是这么一个数组
float f[] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 };
想象一下,我们进入了qsort函数内部,我们是不是应该将两个元素进行比较啊,比如我们要比较1.0和2.0,那我们就是将这两个元素的地址传到cmp_float函数内部去,再通过浮点型的比较方式进行比较
但有人会问:我见过int*,char*等等,这个void*是什么呀?你可以这么去理解,
int main()
{
int a = 1;
int* p = &a;
char* pc = &a;
char* ph = (char*)&a;
void* ps = &a;
return 0;
}
你看,当我们这么写的时候,a 是整形,如果要将地址放到char* 里的话就需要强制类型转换,但放在void* 里就不需要。你可以这么说,void* 就是一个能放所有地址的东西。
但注意,void* 不能+-,也不能解引用
接下来有人就看到说,为什么这个函数的返回类型是int 呀?是因为qsort函数规定说如果a<b,就返回一个小于0的整数,如果a=b,就返回0,a>b,就返回一个大于0的整数,如下图
那好,接下来我们就来实现一下cmp_float函数,
cmp_float 将两个元素的地址用void* 接收,并分别命名为a b,既然是void*,我们就将其强制类型转换成float* ,又因为是地址,所以我们需要将其解引用一下,就得到了 * ( flaot* ) a,同理,我们也可以得到 * ( flaot* ) b,又因为要返回一个整数,那我们就将两个相减之后的值强制类型转换为int再 return 回去不就可以啦!代码如下:
int cmp_float(const void* e1, const void* e2)
{
return ((int)(*(float*)e1 - *(float*)e2));
}
同理,如果是结构体变量的话,我们也可以将 a 转换成相应的类型,这里假设是stu*,那么我们将其强制类型转换了之后就可以用 -> 来直接选择结构体内想比较的来进行比较。注意:我们将其强制转换成这个类型的指针之后,这个指针它就指向的是这个结构体的类型,它是可以直接访问结构体成员的,所以我们这里不需要解引用。例子如下:
int cmp_stu_by_age(const void*a, const void*b)
{
return (int)(((stu*)e1)->age - ((stu*)e2)->age);
}
讲到这里,相信大家已经清楚qsort函数的使用方法了,那我们接下来来进行下一步:qsort函数的实现!!
3 . 冒泡排序之 qsort 函数的实现
先上代码!!
int compare(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void* base, int sz, int width, int(*cmp)(void*, void*))
{
int i = 0, j = 0;
for (i = 0; i < sz - 1; i++)
{
for (j = 0; j < sz - 1 - 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);
}
}
}
}
void test()
{
int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), compare);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
test();
return 0;
}
我们这里先换成整型数组,而我们主要是实现一个qsort函数,所以大多数是不需要变的。那我们现在将我们要实现的qsort函数取名为bubble_sort。因为我们只需要让他帮我们排个序,不需要返回什么,所以返回类型就是void。那既然是一个函数,要调用这个函数就需要传递参数吧。
先来看第一个参数,当我们在test函数内部调用这个函数的时候,bubble_sort函数知道我们传的是什么类型的数组吗?不知道!那如果我们拿诸如 int*,float* 之类的指针接收该地址的话,那不就没法实现传什么类型都能排序的初衷了吗?但换一个角度来看,如果我们拿void*来接收会不会更好一点呢?当我们用void*来接收之后,虽然其并不能解引用,但我们可以在回调函数这步时将其强制类型转换,不就可以进行解引用了吗。 第一个参数如下:
void bubble_sort(void* base, , , )
{
//语句
}
而第二个与第三个函数我们可以模仿一下 qsort 函数,第二个传的是数组的元素个数,sz,第三个传的是每个元素的大小,我们也可以将其称之为 width ( 宽度 )。而我们传过去的话,我们可以用int接收。sz可以用在for循环嵌套中,而width可以用于比较所需排序的数组中的每一个元素,这个在后面会讲到
而至于第四个参数,因为他是传一个函数进来,所以我们就需要拿一个函数指针来接收。但我们不知道他传过来的是一个什么类型的数,有可能是int,float或其他。所以这个参数我们得写 void*,然后在回调函数的时候只需要强制类型转换一下就能得到返回值啦,之后再根据返回值来判断应不应该交换。由此,我们就能得到如下代码:
void bubble_sort(void* base, int sz, int width, int(*cmp)(void*, void*))
{
//实现过程
}
接下来我们来看,既然是冒泡,那我们依然可以用for循环嵌套来表示有多少趟冒泡和每一趟冒泡。如下:
void bubble_sort(void* base, int sz, int width, int(*cmp)(void*, void*))
{
int i = 0, j = 0;
//表示有多少趟冒泡
for (i = 0; i < sz - 1; i++)
{
//表示每一趟冒泡
for (j = 0; j < sz - 1 - i; j++)
{
//判断与交换
}
}
}
那么既然已经把冒泡给表示出来了,那么接下来我们就进入判断环节
可是问题来了,我们该怎么判断呢?我们接收的起点被命名为base,也就是那个数组的首元素地址。可是我们是拿void*来接收的,void*又不能进行加减运算,不加减运算我们也没办法找到下一个要比较的数,这可怎么办呢?
不知各位看到这里的时候有没有发现我们的width还没有被用到啊?width就是这个需要排序的数组里的每一个元素占了多少个字节。试想一下,我们先将base强制类型转换成(char*),char*每次加一的时候只会跳过一个字节,我们需要跳过多少个字节我们才能找到下一个字节我们知道吗?知道!width个字节。我们如果要表示每趟冒泡里要交换的数,我们只需要让其加上 j*width就行了(第二层for循环里的是 j ),前一个是(char*)base+j*width,后一个是(char*)base+(j+1)*width
那如果我们要判断的话,我们就需要再次调用我们接收的第四个参数, 让其返回一个小于或大于0的整数,或者是两数相等的话就返回0。接下来就让其判断:如果返回值大于0就进行下一步。毕竟我们无法判断其是要从小到大排序还是其他顺序,其所需要的排序方式应该在传过来的那个函数里被写好,由此,我们就能得到如下代码:
void bubble_sort(void* base, int sz, int width, int(*cmp)(void*, void*))
{
int i = 0, j = 0;
for (i = 0; i < sz - 1; i++)
{
for (j = 0; j < sz - 1 - i; j++)
{
//( cmp ( 参数一 , 参数二 ) >0)
//需要比较的两个数作为参数
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
//交换
}
}
}
}
接下来我们就进入了交换这个环节,这里我们可以写一个函数来分装,姑且叫他swap函数好了。
但问题来了,既然要调用这个函数我们总得传参吧,那我们传什么过去好呢?有人可能会说传(char*)base + j * width 和 (char*)base + ( j + 1) * width就行了,真是如此吗?
我们且看,我们传过去的是一个char类型的地址,那swap函数就应该拿一个char*来接收吧,那接收完之后,就该开始交换了。可是,我们要交换的那个元素是几个字节的我们知道吗?我们不知道,而且我们交换的话,我们要一个字节一个字节地交换,我们要交换几个字节我们知道吗?我们发现我们又不确定了。
但,如果我们多传一个width会怎么样?我们就会知道我们需要传width个字节,而一个一个地传的话,我们可以用一个for循环,令其判断条件为从0开始并小于width,这样我们就实现了交换这个过程。由此,我们得出如下代码:
void swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
//交换的过程
}
}
那我们接下来我们来实现一下交换的过程。
既然要交换的元素是用char类型接收的,那我们就用char类型定义一个变量->char tmp。而初始化的值我们可以用*buf1来赋值给tmp,这里解引用是因为传的是地址,所以要解引用之后才能使用。交换完之后我们再各自让buf1和buf2 ++一下就可以啦。注:这里的buf1和buf2++并不需要解引用,因为地址++了之后指向的就是下一个字节(char类型)。由此我们可以得出以下代码:
void swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
至此,我们今天的内容也就结束了!!
如果觉得分享的内容对各位有帮助的话,就请多多关注咯!