认识到深入理解qsort,并用冒泡排序模拟实现

目录

认识到理解使用qsort函数

第一步:认识qsort函数

第二步:了解特点

第三步:理解qsort函数

冒泡排序对qsort进行模拟实现

第一步:写主函数

第二步:编写test函数

第三步:定义结构体

第四步:编写冒泡排序函数

第五步:编写compare函数

第六步:编写交换swap函数

第七步:编写打印(printarr)函数

全代码展示


 

 

相信大家一看到标题就知道这篇博客要写什么,没错,就是C语言中的函数---qsort

我们学习一个函数,首先我们得先认识它,知道它的作用是什么,不然我们的学习兴趣就会失去一半甚至更多

认识到理解使用qsort函数

第一步:认识qsort函数

qsort全称quick sort,意为快速排序,所以我们很容易联想到这是一个关于排序的函数,好接下来我们来观察一段代码 

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

// 比较函数,用于比较两个整数的大小
int compare(const void *a, const void *b) 
{
    return (*(int*)a - *(int*)b);
}

int main() {
    int arr[] = {5, 3, 8, 1, 2};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    //调用qsort函数
    qsort(arr, n, sizeof(arr[0]), compare);

    //打印数组
    for (int i = 0; i < n; i++) 
    {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

通过运行结果我们就能发现这段代码确确实实的实现了排序的功能,接下来让我们一起分析这段代码

代码分析:

 从上面这段代码中我们能看到,代码开头定义了一个整形数组并进行赋值{5,3,8,1,2},并且运用sizeof计算出了数组长度存放在了n中,紧接着下一行代码就调用了qsort函数,观察它我们就会发现,我们给它传递了四个参数:arr,n,sizeof(int),compare(这里我们暂时不解释这些参数有什么作用)。然后进入compare函数内部,相信大家在一开头就能发现compare是一个我们自定义的函数,但是我们目前是不知道它有什么作用的,先跳过,回到主函数就会看到一个for循环,是用来打印排完序的数组,通过运行结果发现,代码确实实现了排序的功能。

第二步:了解特点

每个东西都有它自己的特点,函数当然也不例外,但是我们凭空想象或者观察上面的那段代码是不能得到qsort这个函数的特点的,那我们就可以使用比较法来观察,所以我们这里写出另一个排序函数用来比较

#include <stdio.h>

// 冒泡排序函数
void bubbleSort(int arr[], int n) 
{
    int i, j;
    for (i = 0; i < n - 1; i++)
    {
        for (j = 0; j < n - i - 1; j++) 
        {
            if (arr[j] > arr[j + 1]) 
            {
                // 交换元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() 
{
    int arr[] = {5, 3, 8, 1, 2};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 调用冒泡排序函数
    bubbleSort(arr, n);

    // 输出排序后的数组
    for (int i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

这是一段通过冒泡排序来实现排序的代码,通过观察运行结果可以发现,这两段代码都得到了相同的结果,也就是说均实现了排序的目的,所以我们来比较这两段代码。最容易发现的就是代码长短,冒泡排序比上我们的qsort函数多了十几行代码,所以

特点一:代码行长度短

继续观察冒泡排序,可以发现它之所以可以实现排序的功能,是我们自己通过循环、交换等一系列的方法来实现的,而我们的qsort函数,仅仅是进行了传参和编写一行函数compare函数就实现了排序功能,所以

特点二:现成的,使用方便

我们学习过冒泡排序就会发现,它通过内外循环来实现数据的比较和交换,其中外循环用来进行一趟内部的比较,内循环用来比较并且交换,然而关于for循环的特点,我们知道,在没有外界干扰的情况下,循环都需要进行完整,而qsort函数是基于一种快速的算法,运行速度快,所以

特点三:效率高

对于冒泡排序,一般只适用于一些简单的数据类型,如整数、浮点数等,而qsort函数则具有很高的通用性,就连比较复杂的结构体排序也能实现,所以

特点四:所有类型的排序都能实现

看到这里,相信大家已经充分的被qsort函数所吸引,并且迫不及待的想要理解它的原理并且运用它,所以接下来就让我们对它进行理解

第三步:理解qsort函数

在第一步认识函数中我们以及观察到,在使用qsort函数时需要给它传递四个参数,查阅资料我们发现使用qsort函数的基本形式为:qsort (void* base,size_t num,size_t size,int(*compar)(const void*,const void*))

我们对基本形式进行逐字理解

void* base-->是指针,指向被排序数组的第一个元素

size_t num-->被排序数组的元素个数

size_t size-->被排序数组的元素的大小(字节)

int(*compar)(const void*,const void*)-->compar函数指针,指针指向的函数用来比较被排序数组中的两个元素的比较

!!!这里的所有void*均是为了能接收所有类型的指针

有了这个认识,我们来解释第一步中调用qsort函数时四个参数的意义是什么

arr-->数组名,代表数组的第一个元素arr[0],作为起始地址

n-->元素个数5个

sizeof(arr)-->计算每个数组元素的大小(字节)

compare-->指向要比较的两个元素(如比较5,3),并有返回值。我们来重点理解compare函数

观察并分析函数:定义该函数时定义了两个由const修饰的指针变量a,b ,在函数内部,通过return直接返回了*(int*)a - *(int*)b

如果返回值<0,则表示第一个元素应排在第二个元素之前

如果返回值=0,则表示两个元素相等

如果返回值>0,则表示第一个元素应排在第二个元素之后

写compare函数

//我们知道compare函数是用来比较两个整型的大小的
//所以a指向了一个整型数据
//所以b指向了一个整型数据

int compare(const void* a,const void* b)
{
	//我们在接收实参时使用了void*,使得a,b都是五类型的数据,所以我们需要将它们强制转换为整型
	//所以(int*)的作用就是将a强制转换为整型,对b同理
    //在前方加上*进行解引用操作(解引用的目的是为了获取指针所指向内存位置存储的实际整型数值,进而才能进行比较)
	return (*(int*)a-*(int*)b);
	//直接返回>0/<0/=0,简直明了(推荐) 
} 

通过以上三个步骤,我们已经初步认识并理解了这个函数,接下来我们将进行最令人激动的一步:实操!我们将用qsort函数对结构体排序来为我们进行更深入的理解

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

//创建结构体变量 

struct Student
{
    char name[30];
    int age;
};

//写比较函数
 
int compare(const void* a,const void* b)
{
return (*(struct Student*)a).age-(*(struct Student*)b).age;
}

void printarr(struct Student arr[],int sz)
{
	int i=0;
	for(i=0;i<sz;i++)
	{
	printf("%s: %d\n",arr[i].name,arr[i].age);
	}
}

void test()
{
    struct Student arr[]= {{"zhangsan",20},{"lisi",18},{"wangwu",31}};
    //计算数组长度 
    int sz=sizeof(arr)/sizeof(arr[0]);
    //调用函数 
    qsort(arr,sz,sizeof(arr[0]),compare);
    //打印数组 
    printarr(arr,sz);

}
//主函数 
int main()
{
	//排序函数 
    test();
    return 0;
}

终于终于,我们到这里已经学习完了qsort函数,并且会使用它了,但是大家到这里,我相信大多数人心中一定有一个疑惑,那就是如果没有这个函数时,我们是怎么对结构体进行排序的,难道说通过我们的冒泡排序的方法真的不能进行结构体的排序吗?答案是显而易见的,我们是可以通过冒泡排序进行结构体排序的,并且不只是结构体,从而达到qsort函数的作用。

冒泡排序对qsort进行模拟实现

第一步:写主函数

int main()
{
	test();
	return 0;
}

主函数内通过定义test函数进行我们的代码测试

第二步:编写test函数

//这个函数不需要有返回值
void test()
{
	struct student arr[]={{"zhangsan",20},{"lisi",18},{"wangwu",31}};
	int sz=sizeof(arr)/sizeof(arr[0]);//计算数组长度
	bubble_sort(arr,sz,sizeof(arr[0]),compare);//冒泡排序函数
	printarr(arr,sz);//打印结构体
}

第三步:定义结构体

struct student
{
    char name[30];//声明一个长度为30的字符数组name,用来存储姓名
    int age;//用来存储年龄
};

第四步:编写冒泡排序函数

void bubble_sort(void* base,size_t sz,size_t width,int(*compare)(const void* a,const void* b))
{
	int i=0;
	for(i=0;i<sz-1;i++)//外部循环
	{
		int j=0;
		for(j=0;j<sz-i-1;j++)//内部循环
		{
			if(compare((char*)base+j*width,(char*)base+(j+1)*width)>0)//判断是否交换
			{
				swap((char*)base+j*width,(char*)base+(j+1)*width,width);//实现交换
			}
		}
	}
}

相信大家看到这儿就会感到迷惑了,为什么要这样定义函数,而这个函数的定义形式又和调用qsort函数时及其相似,这是为什么呢?首先我们再次明确我们在做什么,我们是对qsort函数进行模拟实现,达到同样的对所有类型的数据进行排序,而其他类型的数据并不能像整型数据一样直接交换,所以我们打起了地址的想法,我们都知道每一个数据都有相应的内存空间,也就是地址,所以我们就想如果把地址交换以后会不会实现数据的交换功能呢?为了得到这个答案,我们来观察下面这幅图片

在交换前如果我们打印ptr1和ptr2我们将会得到“hello world”,但是在交换后我们打印ptr1和ptr2我们将得到“world hello”。所以,通过这个,我们知道交换地址确实可以达到交换数据的目的

所以,回到这个函数的定义,由于我们已经明确了需要通过地址来实现对数据的交换,所以我们需要给函数传递地址(base就是首元素的地址),然后传递数组大小(sz),再传递元素大小(width),最后传递compare实现获得返回的正负,用来判断是否进行交换。到这里我们就知道了为什么这个冒泡排序需要这样进行定义。

第五步:编写compare函数

int compare(const void* a,const void* b)
{
	return (((struct student*)a)->age-((struct student*)b)->age);
}

通过上文对qsort函数的理解,相信大家一定知道为什么这样写这个函数,所以我们这里重点不在这个函数上,而是放在传参上,为什么要为这个函数传递这样复杂的参数 

看见这张图片,我们就来到了最难的部分,但是大家不用担心,我将用最容易理解的方式进行讲解

大家观察理解这张图片就应该知道为什么我们传参时传的是(char*)base+ j*width了,这样我们就得到了元素j的地址了。

第六步:编写交换swap函数

void swap(char* buf1,char* buf2,int width)
{
	int i=0;
	char t=0;//暂时存储第一个字节
	for(i=0;i<width;i++)
	{
		t=*buf1;
		*buf1=*buf2;
		*buf2=t;
		buf1++;
		buf2++;//buf1和buf2同时++保证同时访问到j和j+1元素的下一个字节
	}
}

在第六步中,我们已经得到了j和j+1的地址,但我们还不能直接进行交换,我们还要进行分割为一个一个字节,这也是为什么我们在前面为什么要用(char*)强制转换base的原因,因为char类型在C语言中具有特殊性,char*类型的指针在进行算术运算时,是以字节为单位进行移动

swap函数交换图文演示

相信聪明的各位看到这幅图一定可以明白字节的交换和移动

第七步:编写打印(printarr)函数

void printarr(struct student arr[],int sz)
{
	int i=0;
	for(i=0;i<sz;i++)
	{
		printf("%s: %d\n",arr[i].name,arr[i].age);
	}
}

全代码展示

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

struct student
{
    char name[30];
    int age;
};

void swap(char* buf1,char* buf2,int width)
{
	int i=0;
	char t=0;
	for(i=0;i<width;i++)
	{
		t=*buf1;
		*buf1=*buf2;
		*buf2=t;
		buf1++;
		buf2++;
	}
}

int compare(const void* a,const void* b)
{
	return (((struct student*)a)->age-((struct student*)b)->age);
}

void bubble_sort(void* base,size_t sz,size_t width,int(*compare)(const void* a,const void* b))
{
	int i=0;
	for(i=0;i<sz-1;i++)
	{
		int j=0;
		for(j=0;j<sz-i-1;j++)
		{
			if(compare((char*)base+j*width,(char*)base+(j+1)*width)>0)
			{
				swap((char*)base+j*width,(char*)base+(j+1)*width,width);
			}
		}
	}
}

void printarr(struct student arr[],int sz)
{
	int i=0;
	for(i=0;i<sz;i++)
	{
		printf("%s: %d\n",arr[i].name,arr[i].age);
	}
}

void test()
{
	struct student arr[]={{"zhangsan",20},{"lisi",18},{"wangwu",31}};
	int sz=sizeof(arr)/sizeof(arr[0]);
	bubble_sort(arr,sz,sizeof(arr[0]),compare);
	printarr(arr,sz);
}

int main()
{
	test();
	return 0;
}

好了,到这里我们已经学完了qsort函数的所有知识,并用冒泡排序模拟实现了qsort函数,希望这些内容能给大家带来一些帮助。

最后,我想用一段话来结束:编程之道不在追求晦涩的奇技淫巧,而在于用最朴素的逻辑结构本质,正如达芬奇所言:“简单是终极的复杂。”愿你我都能成为代码世界的吟游诗人。

 

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值