深入理解指针(4):qsort 函数 & 通过冒泡排序实现

加油,start anew!

mj starbucks

好好享受现在的时光,不停的朝着目标前进,keep moving,行动不停!

1. 冒泡排序(基础版 + 优化版)💧

1.1 个人理解

冒泡排序是通过相邻元素两两比较交换,把大元素逐步 "冒" 到数组末尾。

1.2 深入

基础冒泡排序逻辑

👉 核心动作:从数组第一个元素开始,依次比较相邻的两个元素(arr[j]arr[j+1]),如果前一个比后一个大就交换位置;每一轮外层循环都会把当前未排序部分的最大元素放到末尾。

⚠️ 我写的初级冒泡函数有个错误:比较的是arr[i]arr[i+1],应该是arr[j]arr[j+1]复习时要注意)。

优化版冒泡排序(提前终止)

👉 优化原因:基础版不管数组是否已排好序,都会走完所有循环,效率低;

👉 优化思路:加计数器count,每轮内层循环开始时置 0,发生交换则count++;若一轮循环后count=0,说明数组已完全有序,直接break跳出循环,减少无效比较。

数组传参本质(地址传递)

👉 我最初疑惑 "自定义函数改不了主函数内容",但数组传参的本质是传递首元素地址,非常方便,因此在冒泡函数里修改数组元素,主函数的原数组会同步变化 ------ 这是 C 语言数组传参的关键(没有传整个数组,只传地址)📌。

1.3 专业知识

冒泡排序属于交换排序,时间复杂度:最好情况(已排序)O(n),最坏情况(逆序)O(n²);空间复杂度O(1)(原地排序)。其核心特征是 "相邻比较、逆序交换",每轮确定一个最大元素的位置;优化版通过提前终止条件,降低了已排序数组的排序耗时,是基础版的效率优化手段。

1.4 代码实现

cpp 复制代码
void myprint(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

//初级冒泡函数
void bubblesort(int arr[], int sz)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < sz; i++)
	{
		for (j = 0; j < sz - i - 1; j++)
		{
			if (arr[i] > arr[i + 1])
			{
				int t = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = t;
			}
		}
	}
}

//改进的冒泡函数:
//就是,如果对于第i次外层循环,里面的for循环没有一次让两个相邻的数交换,那么就可以停下了
void bubblesort(int arr[], int sz)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < sz; i++)
	{
		int count = 0;
		for (j = 0; j < sz - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int t = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = t;
				count++;
			}
		}
		if (count == 0)
			break;
	}
}

int main()
{
	int arr[10] = { 1,3,4,2,5,6,9,8,7,10 };
	myprint(arr, 10);
	bubblesort(arr, 10);
	myprint(arr, 10);
	return 0;
}

2. qsort 函数(C 内置通用排序)🔧

2.1 个人理解

qsort 是 C 语言内置的通用排序函数,默认按升序排列,比较函数自己写

2.2 深入

qsort 四大参数解析

👉 qsort 的 4 个参数缺一不可,每个参数都有明确作用:

  1. 第一个参数:待排序数组的首元素地址(void*类型,支持任意类型数组);
  2. 第二个参数:数组元素个数(size_t类型,无符号整数,比int更适合表示 "个数");
  3. 第三个参数:单个元素的字节长度(size_t类型,比如int占 4 字节、结构体按实际大小算);
  4. 第四个参数:函数指针(比较函数),需自己编写,用来定义两个元素的比较规则。
比较函数编写规则

👉 固定格式:返回值为int,两个参数都是const void*const保证不修改元素,void*适配任意类型);

👉 编写步骤:先把const void*强制转换为对应数据类型的指针,再解引用获取值比较;

👉 示例(整型):return *(int*)a - *(int*)b; ------ 强制转int*后解引用,相减结果决定排序顺序。

qsort 排序规则(有点像减法,然后把大的放后面)

👉 返回正数:第一个元素 > 第二个元素,qsort 会把第一个元素排在后面(升序逻辑);

👉 返回 0:两个元素相等,位置不变;

👉 返回负数:第一个元素 < 第二个元素,排在前面;

👉 总结:升序返回a-b,降序返回b-a⚠️。

2.3 专业

qsort 底层实现为快速排序(Quick Sort),平均时间复杂度O(n log n),最坏O(n²)。其泛型特性通过void*指针(无类型指针)和自定义比较函数实现,支持整型、浮点型、结构体等任意类型数据排序,是 C 语言中高效、通用的排序工具。size_t是 C 标准定义的无符号整数类型,专门用于表示 "对象大小 / 元素个数",比int更符合语义。

2.4 适用场景

  1. 需对任意类型数组排序,且不想手动实现复杂排序算法时;
  2. 对排序效率有一定要求(快排比基础冒泡排序高效得多);
  3. 项目中需要统一、规范的排序接口时。

2.5 代码实现(整型排序)

cpp 复制代码
#include<stdlib.h>

void myprint(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int cmp_int(const void* a, const void* b)
{
	return *(int*)a - *(int*)b;//强制类型转换之后再解引用
}

void test2()
{
	int arr[10] = { 2,3,5,6,8,9,1,4,7,10 };
	myprint(arr, 10);

	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), cmp_int);
	myprint(arr, 10);

}

3. 结构体排序(qsort 实战)👥

3.1 前置:结构体指针箭头操作符

个人理解

结构体指针的箭头操作符->可直接通过结构体指针访问成员 ,比(*ps).name更简洁。

深入

👉 结构体成员的两种访问方式:

  1. 结构体变量:用点操作符.,比如s.names.age
  2. 结构体指针:
    • 方式 1(解引用 + 点):(*ps).name
    • 方式 2(箭头):ps->name(无需写解引用,代码更简洁);

👉 箭头操作符的优势:函数传结构体指针时,用->访问成员更直观,减少代码冗余。

专业

->是 C 语言中结构体 / 联合体指针的成员访问操作符,运算优先级高于解引用*,仅适用于指针类型。ps->name(*ps).name的语法糖(语法简化形式),目的是提升代码可读性,是结构体指针成员访问的最佳实践。

代码实现
cpp 复制代码
void test(struct Stu* ps)
{
	//我们熟知的打印方法(通过解引用):
	printf("%s\n", (*ps).name);
	printf("%d\n", (*ps).age);

	//(结构体地址)结构体成员访问操作符(箭头操作符)的使用:
	printf("%s\n", ps->name);
	printf("%d\n", ps->age);
}

3.2 按结构体年龄排序

个人理解

按结构体年龄排序的比较函数,需把void*转成结构体指针,用->访问age成员,返回两数之差实现升序。

深入

👉 强制类型转换的原因:qsort 传的比较函数参数是void*,必须转成struct Stu*才能访问****age成员;(强制类型转换

👉 两种写法对比:

  1. 普通版:(*(struct Stu*)p1).age - (*(struct Stu*)p2).age(先解引用,再用.);
  2. 箭头版:((struct Stu*)p1)->age - ((struct Stu*)p2)->age(直接用->,更简洁);

👉 排序逻辑:返回值为正 → p1年龄更大 → 排在p2后面,实现年龄升序。

专业

结构体成员比较的核心是 "类型映射":将void*指针强制转换为具体的结构体类型指针,再访问目标成员。比较函数返回值需符合 qsort 约定,通过成员值的差值控制升序 / 降序;->的使用符合 C 语言代码规范,是结构体指针访问成员的推荐写法。

代码实现
cpp 复制代码
//按照年龄
int cmp_stu_by_age(const void* p1,const void*p2)//对结构体中年龄的排序
{
	//普通版本:
	return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;

	//使用结构体访问操作符(结构体指针,箭头)
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}

3.3 按结构体姓名排序(字符串)

个人理解

结构体姓名排序要用strcmp函数比较字符串,返回其结果,因为字符串不能直接用-比较,strcmp返回值还完美适配 qsort 规则,挺好。

深入

👉 为什么不能用-比较字符串?字符串是字符数组,直接减是比较地址(不是内容),必须用strcmp

👉 strcmp的作用:按 ASCII 码值逐字节比较两个字符串(字典序 / 字母顺序);

👉 strcmp返回值(++完美适配 qsort++):

  1. 正数:第一个字符串 > 第二个;
  2. 0:两个字符串相等;
  3. 负数:第一个字符串 < 第二个;

⚠️注意:使用strcmp必须包含string.h头文件!

专业

字符串比较必须使用库函数strcmp<string.h>),其实现逻辑是逐字节比较字符的 ASCII 码值,直到遇到'\0'或不同字符。strcmp的返回值语义与 qsort 比较函数完全兼容,是字符串类型排序的标准实现方式。

代码实现
cpp 复制代码
#include<string.h>

//按照姓名
//字符串比较的时候用strcmp(字符串1,字符串2) ,需要头文件string.h,比较的是字母顺序(ASCII码值)
//这个函数的返回的和qsort是相应的
int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name,((struct Stu*)p2)->name);
}

3.4 结构体排序完整代码(test3)

cpp 复制代码
//对结构体
struct Stu
{
	char name[30];
	int age;
};

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

void test3()//结构体该复习复习了
{
	struct Stu arr[] = { {"zhangsan",20},{"lisi",38},{"wangwu",18} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);

	//对结构体的打印:
	print_stu(arr, sz);
}

4. 通用冒泡排序(模仿 qsort)🚀

4.1 个人理解

模仿 qsort ,拆分的思想

4.2 深入

核心:泛型化设计思路

👉 基础冒泡的局限性:只针对int类型,比较和交换都是硬编码;

👉 通用冒泡的关键设计:

  1. 参数模仿 qsort:void* base(首地址)、size_t sz(元素数)、size_t width(元素字节数)、比较函数指针;
  2. 地址定位:把void*转成char*(1 字节),通过j*width找到第 j 个元素地址,(j+1)*width找到下一个;
  3. 比较逻辑:调用++自定义比较函数++,判断是否需要交换;
  4. 交换逻辑:按字节交换(不管元素类型),实现通用交换。
Swap 函数(字节级交换)

👉 字节级交换的原因:不知道元素类型(int / 结构体等),但所有类型都可拆分为字节;

👉 实现逻辑:循环width次(元素字节数),每次交换char*指针指向的 1 个字节,指针 ++,直到所有字节交换完成🔄;

👉 示例:int占 4 字节 → 循环 4 次,逐字节交换后,整个int值完成交换。

bubblesort2 函数逻辑

👉 外层循环:i从 0 到sz-1(我最初写成sz是错误的❌,sz 个元素只需 sz-1 轮);

👉 内层循环:j从 0 到sz-i-1(和基础冒泡一致);

👉 比较:调用cmp函数,传入第 j 和 j+1 个元素地址,返回值 > 0 则交换;

👉 交换:调用Swap函数,传入元素地址和字节数;

👉 提前终止:加count计数器,无交换则break(和优化版冒泡一致)。

4.3 专业

通用冒泡排序的核心是泛型编程思想(C 语言通过void*和字节操作实现),char*指针是实现任意类型访问的关键(char 为最小内存单元,占 1 字节)。该实现的时间复杂度与基础冒泡一致(最好O(n),最坏O(n²)),但具备 qsort 的泛型特性,可适配任意数据类型。Swap函数的字节级交换是通用数据交换的标准方式,避免为不同类型编写多个交换函数。

4.4 适用场景

  1. 学习泛型编程思想,理解 qsort 的底层泛型实现逻辑;
  2. 数据量小、场景简单,且需自定义排序规则的任意类型数组排序;
  3. 不希望依赖标准库 qsort,需手动实现通用排序时。

4.5 代码实现

Swap 函数
cpp 复制代码
void Swap(char* buf1, char* buf2, size_t width)
{
	int i = 0;
	char tmp = 0;
	for (i = 0; i < width; i++)
	{
		tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
bubblesort2 函数
cpp 复制代码
//和qsort函数模仿(用冒泡函数的基本)
void bubblesort2(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{
	//底层还是冒泡排序的算法思想:
	int i = 0;
	int j = 0;
	for (i = 0; i < sz-1; i++)//我最开始写成了sz,错误❌
	{
		int count = 0;
		for (j = 0; j < sz - i - 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);
				count++;
			}
		}
		if (count == 0)
			break;
	}
}
整型测试(test4)
cpp 复制代码
void test4()
{
	
	int arr[10] = { 1,3,4,2,5,6,9,8,7,10 };
	myprint(arr, 10);
	bubblesort2(arr, 10,sizeof(arr[0]),cmp_int);
	myprint(arr, 10);
	
}
结构体测试(test5)
cpp 复制代码
void test5()
{
	struct Stu arr[] = { {"zhangsan",20},{"lisi",38},{"wangwu",18} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubblesort2(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	bubblesort2(arr, sz, sizeof(arr[0]), cmp_stu_by_age);

	//对结构体的打印:
	print_stu(arr, sz);
}

5. 核心要点总结 📝

  1. 冒泡排序:基础版靠相邻交换排序,优化版加count提前终止;数组传参本质是传地址,函数内修改会影响原数组。
  2. qsort 函数:通用排序工具,依赖自定义比较函数;void*适配任意类型,strcmp适配字符串比较,->简化结构体指针成员访问。
  3. 通用冒泡:模仿 qsort 的泛型设计,通过char*实现字节级地址定位,Swap函数逐字节交换实现任意类型数据交换。

6. 完整主函数代码

复制代码
int main()
{
	test2();//对qsort函数的使用(int)
	test3();

	struct Stu s = { "zhangsan",20 };
	test(&s);//对(结构体地址)结构体成员访问操作符(箭头操作符)用法的说明:

	test4();
	test5();

	return 0;
}
相关推荐
fie88899 小时前
基于MATLAB的转子动力学建模与仿真实现(含碰摩、不平衡激励)
开发语言·算法·matlab
唐梓航-求职中9 小时前
编程大师-技术-算法-leetcode-1472. 设计浏览器历史记录
算法·leetcode
_OP_CHEN9 小时前
【算法基础篇】(五十八)线性代数之高斯消元法从原理到实战:手撕模板 + 洛谷真题全解
线性代数·算法·蓝桥杯·c/c++·线性方程组·acm/icpc·高斯消元法
唐梓航-求职中9 小时前
编程大师-技术-算法-leetcode-355. 设计推特
算法·leetcode·面试
少许极端9 小时前
算法奇妙屋(二十八)-递归、回溯与剪枝的综合问题 1
java·算法·深度优先·剪枝·回溯·递归
仰泳的熊猫9 小时前
题目1453:蓝桥杯历届试题-翻硬币
数据结构·c++·算法·蓝桥杯
唐梓航-求职中9 小时前
技术-算法-leetcode-1606. 找到处理最多请求的服务器(易懂版)
服务器·算法·leetcode
啊阿狸不会拉杆9 小时前
《机器学习导论》第 10 章-线性判别式
人工智能·python·算法·机器学习·numpy·lda·线性判别式
会叫的恐龙9 小时前
C++ 核心知识点汇总(第11日)(排序算法)
c++·算法·排序算法