引子:
昨天做这个题目的时候,突然发现一个问题:
int*给我转换的过程中,

代码:
// /**
// * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
// *
// * 计算成功举办活动需要多少名主持人
// * @param n int整型 有n个活动
// * @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
// * @param startEndRowLen int startEnd数组行数
// * @param startEndColLen int* startEnd数组列数
// * @return int整型
// */
// #include <stdio.h>
// #include <stdlib.h>
// int cmp(const void *a, const void *b)
// {
// // return *(int *)a - *(int *)b;
// int x = *(int *)a;
// int y = *(int *)b;
// if (x < y)
// {
// return -1;
// }
// else if (x > y)
// {
// return 1
// }
// else
// {
// return 0;
// }
// }
// int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen)
// {
// // write code here
// int *start = (int *)malloc(n * sizeof(int));
// int *end = (int *)malloc(n * sizeof(int));
// for (int i = 0; i < n; i++)
// {
// start[i] = startEnd[i][0];
// end[i] = startEnd[i][1];
// }
// qsort(start, n, sizeof(int), cmp);
// qsort(end, n, sizeof(int), cmp);
// int i = 0, j = 0, count = 0, maxLen = 0;
// while (i < n)
// {
// if (start[i] < end[j])
// {
// i++;
// count++;
// if (count > maxLen)
// {
// maxLen = count;
// }
// }
// else
// {
// count--;
// j++;
// }
// }
// return maxLen;
// }
// 2刷 25.10.19
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算成功举办活动需要多少名主持人
* @param n int整型 有n个活动
* @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
* @param startEndRowLen int startEnd数组行数
* @param startEndColLen int* startEnd数组列数
* @return int整型
*/
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
int funCmp(const void *a, const void *b)
{
//!!!vip #self 为什么?
// return *(int *)a - *(int *)b;
// double x = *((*double)a);
// double y = *((*double)b);
// return (int)(*x - *y);
int x = *(int *)a;
int y = *(int *)b;
if (x > y)
{
return 1;
}
else if (x < y)
{
return -1;
}
else
{
return 0;
}
}
int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen)
{
// write code here
// qsort(startEnd, n, sizeof(startEnd[0]), cmp);
int *start = (int *)malloc(n * sizeof(int));
int *end = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
{
start[i] = startEnd[i][0];
end[i] = startEnd[i][1];
}
qsort(start, n, sizeof(start[0]), funCmp);
qsort(end, n, sizeof(end[0]), funCmp);
int i = 0, j = 0, count = 0, maxNum = 0;
while (i < n)
{
if (start[i] < end[j])
{
i++;
count++;
if (count > maxNum)
{
maxNum = count;
}
}
else
{
j++;
count--;
}
}
return maxNum;
}
这里的代码,为什么会这样?
--------------------------------------------------------------------------------------------------------编程思考:
qsort排序坑+双指针精髓,嵌入式工程师必看的C语言实战解析
昨天二刷「计算最少主持人」这道题时,盯着排序函数里的int* 转换发呆了半天——第一次刷题时只想着“能跑就行”,二刷才发现当初踩的坑全是C语言的核心考点。比如为什么qsort的比较函数参数必须是void*?直接用*(int*)a - *(int*)b排序有什么隐患?双指针遍历的时候为什么要先动i再更改进度?
这道题看似是算法题,实则把C语言动态内存、库函数使用、指针操作全揉进去了,尤其适合刷过百题但对底层原理模糊的同学,以及正在学嵌入式的工程师——毕竟嵌入式开发里,内存管理和指针操作可是保命技能。今天咱们就从这道题出发,扒透所有考点,不仅教你做对题,更让你搞懂“为什么要这么做”。
本文适合人群:中等以下C语言水平、刷过牛客/力扣100热题、正在学习嵌入式的开发者。全文用纯C语言实战,无花里胡哨的框架,所有代码可直接复制到Keil或GCC编译运行。
一、先把问题聊透:这道题到底在考什么?
很多人刷题时只看“输入输出”,却忽略了题目背后的工程意义——这道题的本质是“区间重叠问题”,在嵌入式开发里太常见了:比如传感器数据采集任务调度、串口通信帧的时间分配,本质都是“多个时间区间如何安排资源”,而“最少主持人”就是“最少资源数”的具象化。
1.1 题目核心需求拆解
先明确题目要求(避免像我一刷时漏看细节):
|
参数说明 |
具体含义 |
工程场景对应 |
|---|---|---|
|
n |
活动总数 |
待执行的任务总数 |
|
startEnd[][] |
二维数组,startEnd[i][0]是活动i开始时间,startEnd[i][1]是结束时间 |
任务的开始和截止时间列表 |
|
返回值 |
成功举办所有活动的最少主持人数量 |
完成所有任务的最少硬件资源(如CPU核心、传感器通道) |
举个实际例子:输入n=3,startEnd=[[1,3],[2,4],[3,5]],需要多少主持人?
画个时间轴就清楚了:1-3点第一个活动,2-4点第二个活动(和第一个重叠,需加1个主持人),3-5点第三个活动(此时第一个结束,主持人减1,第二个还在进行,所以总主持人还是2)。答案就是2。
1.2 解题核心思路:把“重叠问题”转化为“排序+遍历”
一刷时我想过“暴力法”:对每个活动,检查它和之前所有活动的重叠情况,记录最大重叠数。但这种方法时间复杂度是O(n²),如果n=1000(嵌入式里任务数可能真这么多),程序就卡了。
二刷时才悟到最优解:将开始时间和结束时间分开排序,用双指针遍历计算最大重叠数,时间复杂度O(nlogn)(排序的时间),空间复杂度O(n)(存储排序后的时间),完全符合嵌入式的性能要求。
核心逻辑用思维导图表示更清晰:
暂时无法在豆包文档外展示此内容
这个思路的本质是:用排序让时间“有序化”,用双指针线性遍历替代嵌套循环,把时间复杂度降下来。这是嵌入式开发里常用的“降维”思想——把复杂问题转化为库函数能处理的简单问题。
二、逐行拆解你的代码:那些看似对的地方,其实藏着坑
先贴你二刷的代码(我标了关键行),咱们逐行扒:
// 比较函数 int funCmp(const void *a, const void *b) { //!!!vip #self 为什么? // return *(int *)a - *(int *)b; // double x = *((*double)a); // double y = *((*double)b); // return (int)(*x - *y); int x = *(int *)a; int y = *(int *)b; if (x > y) { return 1; } else if (x < y) { return -1; } else { return 0; } } int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen) { // 分配内存存储开始和结束时间 int *start = (int *)malloc(n * sizeof(int)); int *end = (int *)malloc(n * sizeof(int)); // 拆分时间 for (int i = 0; i < n; i++) { start[i] = startEnd[i][0]; end[i] = startEnd[i][1]; } // 排序 qsort(start, n, sizeof(start[0]), funCmp); qsort(end, n, sizeof(end[0]), funCmp); int i = 0, j = 0, count = 0, maxNum = 0; // 双指针遍历 while (i < n) { if (start[i] < end[j]) { i++; count++; if (count > maxNum) { maxNum = count; } } else { j++; count--; } } return maxNum; }
2.1 内存分配:为什么必须用malloc,而不是栈数组?
你用了int *start = (int *)malloc(n * sizeof(int)),这个选择是对的,但一刷时我犯过错:直接定义int start[n](变长数组VLA),结果在嵌入式开发板上跑崩了。
这里必须讲透:栈内存和堆内存的区别,这是嵌入式开发的高频坑!
|
对比项 |
栈内存(如int start[n]) |
堆内存(如malloc分配) |
嵌入式场景建议 |
|---|---|---|---|
|
大小限制 |
通常很小(比如Keil里默认栈大小1KB),n稍大就栈溢出 |
受限于RAM总大小,可分配较大空间 |
任务数n可能超过栈容量,必须用堆 |
|
分配/释放 |
自动分配释放,无需手动操作 |
手动malloc分配,必须手动free释放 |
记得free,否则内存泄漏(后面优化会加) |
|
兼容性 |
C99标准才支持变长数组,老编译器不兼容 |
所有C编译器都支持 |
嵌入式编译器可能是老版本,优先用malloc |
你的代码里没写free!虽然在OJ上刷题时不用管(程序结束后系统回收),但在嵌入式里,这个函数如果被反复调用(比如循环处理任务),会导致内存泄漏,最终程序崩溃。后面优化部分会补全。
2.2 排序函数:你注释里的3个问题,终于有答案了
你在funCmp里标了“!!!vip #self 为什么?”,还留了3种写法的注释。这正是这道题的核心考点——qsort的比较函数到底怎么写才对?
先明确qsort的函数原型(C标准库定义):
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
四个参数的含义必须记死,嵌入式开发里调库函数全靠这个:
|
参数 |
含义 |
你的代码对应值 |
|---|---|---|
|
base |
要排序的数组首地址(必须转void*) |
start、end数组 |
|
nmemb |
数组元素个数(size_t类型,本质是无符号整数) |
n(活动总数) |
|
size |
每个元素的字节数(必须准确,否则排序错乱) |
sizeof(start[0])(int类型,4字节) |
|
compar |
比较函数指针,定义两个元素的排序规则 |
funCmp函数 |
问题1:为什么比较函数的参数必须是const void*?
你可能疑惑:我要排序的是int数组,为什么不能直接用int*当参数?
答案是:qsort是通用排序函数,要支持int、double、结构体等所有类型的排序。void*是“无类型指针”,可以接收任何类型的数组首地址;加上const是为了防止在比较函数里修改原数组的值(只读操作更安全)。
所以在比较函数里,必须把void*强制转换成实际的类型指针(比如你的代码里转成int*),才能取值比较。这就是“类型转换”的必要性。
问题2:直接return *(int*)a - *(int*)b 有什么隐患?
你注释了return *(int*)a - *(int*)b,改用了if-else判断。这个改动非常正确!因为直接减法会导致整数溢出,这是嵌入式里的致命bug。
举个例子:如果a指向的是INT_MIN(-2147483648),b指向的是1,那么*(int*)a - *(int*)b = -2147483649,超过了int的最小值(-2147483648),发生溢出,结果变成一个正数(具体值取决于编译器),导致排序错乱。
而if-else判断通过比较大小返回1、-1、0,完全避免了溢出问题。用表格总结两种写法的区别:
|
写法 |
优点 |
缺点 |
适用场景 |
|---|---|---|---|
|
return *(int*)a - *(int*)b |
代码简洁 |
可能整数溢出,导致排序错误 |
确定数值范围不会溢出(如0-100的小整数) |
|
if-else判断大小 |
无溢出风险,稳定可靠 |
代码稍长 |
所有场景,尤其是嵌入式开发(数值范围不确定) |
问题3:注释里的double转换为什么错了?
你写了double x = *((*double)a);,这个写法有两个致命错误:
-
类型转换错误:a是void*,要转成double*应该是
(double*)a,而不是(*double)a(语法错误,编译器会报错)。正确的取值应该是*((double*)a)。 -
类型不匹配:你的start和end数组是int类型,却用double*去解析,会导致内存解析错误。比如int是4字节,double是8字节,取出来的值会是乱码,排序自然错误。
这个错误提醒我们:qsort的比较函数里,转换的类型必须和要排序的数组元素类型一致。如果要排序double数组,才需要转成double*;排序int数组,就必须转成int*。
2.3 双指针遍历:最容易忽略的“边界细节”
双指针部分是代码的核心,你的写法整体正确,但有个细节值得深挖:为什么while循环的条件是i < n,而不是i < n || j < n?
我们用实际例子拆解遍历过程,以输入n=3,start=[1,2,3],end=[3,4,5]为例:
|
步骤 |
i值 |
j值 |
start[i]与end[j]比较 |
count变化 |
maxNum变化 |
说明 |
|---|---|---|---|---|---|---|
|
初始 |
0 |
0 |
- |
0 |
0 |
指针都在起始位置 |
|
1 |
0 |
0 |
1 < 3 |
0→1 |
0→1 |
第一个活动开始,主持人加1 |
|
2 |
1 |
0 |
2 < 3 |
1→2 |
1→2 |
第二个活动开始,和第一个重叠,主持人加1 |
|
3 |
2 |
0 |
3 不小于 3 |
2→1 |
不变 |
第一个活动结束,主持人减1 |
|
4 |
2 |
1 |
3 < 4 |
1→2 |
不变 |
第三个活动开始,和第二个重叠,主持人加1 |
|
5 |
3 |
1 |
i=3 ≥n=3 |
不变 |
不变 |
循环结束,maxNum=2 |
从步骤可以看出:当i遍历完所有开始时间(i=n),剩下的end[j]即使没遍历完,也不会影响最大重叠数了——因为没有新的活动开始,只会有活动结束,count只会减少。所以循环条件用i < n就足够,更简洁高效。
另一个细节:else分支里是先j++再count--,而不是先count--再j++。这是因为end[j]是当前要结束的活动,必须先移动指针再减主持人,否则会多减一次。比如步骤3中,j从0移到1后,才表示第一个活动结束,count减1。
三、代码优化:从“能跑”到“嵌入式生产级”
刷题时的代码只要能过用例就行,但嵌入式开发里,代码要考虑稳定性、兼容性和可维护性。你的代码有3个可以优化的点,改完之后就是生产级代码了。
3.1 优化1:补全内存释放,避免内存泄漏
前面提到,你的代码里没有free(start)和free(end)。在嵌入式里,如果这个函数被多次调用(比如每次接收新的任务列表时调用),堆内存会被不断占用,最终导致内存耗尽。
优化方案:在return之前释放内存。但要注意:必须先把maxNum存起来,再释放,否则释放后就没法返回了。
int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen) { // 处理n=0的边界情况(避免malloc(0)) if (n == 0) { *returnSize = 0; return 0; } int *start = (int *)malloc(n * sizeof(int)); int *end = (int *)malloc(n * sizeof(int)); // 检查内存分配是否成功(嵌入式里内存可能不足) if (start == NULL || end == NULL) { // 分配失败时释放已分配的内存,避免内存泄漏 if (start != NULL) free(start); if (end != NULL) free(end); return -1; // 返回错误码,提示内存不足 } // 拆分时间、排序、双指针遍历...(和之前一样) // 先存下结果 int result = maxNum; // 释放内存 free(start); free(end); // 避免野指针(虽然函数结束后会销毁,但好习惯) start = NULL; end = NULL; return result; }
这里加了两个嵌入式开发的必备操作:
-
检查内存分配结果:malloc可能返回NULL(比如RAM不足),必须检查,否则会导致空指针访问崩溃。
-
分配失败时的清理:如果start分配成功但end失败,要释放start,避免内存泄漏。
3.2 优化2:处理边界情况,覆盖所有输入
你的代码没处理n=0的情况(没有活动)。虽然OJ上可能不会给这种用例,但嵌入式里可能会遇到(比如任务列表为空)。另外,startEnd为NULL的情况也要处理。
完整的边界处理代码:
int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen) { // 边界情况1:没有活动 if (n == 0) { return 0; } // 边界情况2:二维数组为空,或列数不是2(每个活动必须有开始和结束时间) if (startEnd == NULL || *startEndColLen != 2) { return -2; // 返回不同的错误码,区分错误类型 } // 边界情况3:行数和活动数不匹配(防止传参错误) if (startEndRowLen != n) { return -3; } // 后续代码... }
用错误码区分不同的错误原因,方便调试——嵌入式开发里,调试不像PC端那么方便,明确的错误码能快速定位问题。
3.3 优化3:封装工具函数,提高可维护性
如果项目里有多个地方需要排序int数组,每次都写一遍funCmp太麻烦,也容易出错。可以把比较函数封装成通用工具函数,放在公共头文件里。
// 通用int数组升序比较函数(放在common.h里) #ifndef COMMON_H #define COMMON_H #include <stdlib.h> // 功能:int数组升序排序的比较函数 // 参数:a-第一个元素地址,b-第二个元素地址 // 返回值:a>b返回1,a<b返回-1,相等返回0 int intCmpAsc(const void *a, const void *b) { int val1 = *(const int *)a; int val2 = *(const int *)b; if (val1 > val2) { return 1; } else if (val1 < val2) { return -1; } else { return 0; } } // 通用int数组降序比较函数(备用) int intCmpDesc(const void *a, const void *b) { return -intCmpAsc(a, b); } #endif // 在主函数里直接调用 #include "common.h" int minmumNumberOfHost(...) { // ... qsort(start, n, sizeof(int), intCmpAsc); qsort(end, n, sizeof(int), intCmpAsc); // ... }
这样做的好处,嵌入式开发者最有体会:
-
可维护性拉满:如果后续需要修改排序规则(比如改成降序),只需要改
common.h里的函数,不用在所有调用排序的地方逐个修改——我之前维护一个传感器任务调度模块时,就是因为没封装函数,改排序规则改了3个小时,还漏改了1处导致程序崩溃。 -
避免重复造轮子:串口日志排序、传感器数据排序都能直接用这个比较函数,不用每次都写if-else,减少出错概率。
-
新人友好:函数名
intCmpAsc清晰易懂,新人一看就知道是“int数组升序比较”,不用读一堆注释。
3.4 优化4:适配嵌入式特殊场景——时间类型与编译器兼容
刷题时时间是int类型,但嵌入式里时间往往是unsigned int(比如从单片机定时器读取的时间戳,不会是负数),而且不同编译器对qsort的支持有差异(比如Keil C51对void*的转换限制)。这部分优化能让代码适配90%的嵌入式场景。
3.4.1 适配unsigned int时间类型
如果start和end是unsigned int,比较函数要改吗?答案是要!因为unsigned类型的比较不能用int的逻辑(比如0 < 0xFFFFFFFF在unsigned里是真,但转成int会判错)。优化后的比较函数:
// 通用unsigned int数组升序比较函数(嵌入式时间常用) int uintCmpAsc(const void *a, const void *b) { unsigned int val1 = *(const unsigned int *)a; unsigned int val2 = *(const unsigned int *)b; // 用减法判断会溢出(比如val1=0,val2=0xFFFFFFFF),必须用比较运算符 if (val1 > val2) { return 1; } else if (val1 < val2) { return -1; } else { return 0; } } // 对应的主函数修改 int minmumNumberOfHost(int n, unsigned int **startEnd, int startEndRowLen, int *startEndColLen) { unsigned int *start = (unsigned int *)malloc(n * sizeof(unsigned int)); unsigned int *end = (unsigned int *)malloc(n * sizeof(unsigned int)); // 后续逻辑和int版本一致,排序时用uintCmpAsc qsort(start, n, sizeof(unsigned int), uintCmpAsc); // ... }
我之前在STM32开发板上踩过的坑:把unsigned int的时间用int的比较函数排序,导致0点附近的时间(比如0xFFFFFFFE)被当成负数,排序完全错乱。嵌入式里时间、地址这些无符号类型,一定要用对应的比较逻辑!
3.4.2 编译器兼容性处理(以Keil为例)
有些老的嵌入式编译器(比如Keil C51)对ANSI C的支持不完整,qsort的比较函数参数如果不加const会报错,或者malloc需要显式指定内存区域。兼容代码如下:
#ifdef __C51__ // Keil C51环境:指定malloc从外部RAM分配(根据硬件配置调整) #define MALLOC(size) malloc_xdata(size) #else // 其他环境(如GCC、Keil MDK) #define MALLOC(size) malloc(size) #endif // 兼容老编译器的比较函数(去掉const,仅在老编译器使用) #ifdef __C51__ int intCmpAsc(void *a, void *b) #else int intCmpAsc(const void *a, const void *b) #endif { int val1 = *(int *)a; int val2 = *(int *)b; // 比较逻辑不变... }
这样修改后,代码既能在PC端的GCC编译,也能在Keil开发环境下跑通,嵌入式开发的“跨编译器”痛点就解决了。
四、实战测试:从编译到运行,踩遍嵌入式开发的坑
代码优化完不是结束,嵌入式开发讲究“能跑通,更要跑稳”。这部分咱们从测试用例设计、编译命令、运行结果分析全流程实战,用GCC和Keil两种环境演示,新手也能跟着做。
4.1 测试用例设计:覆盖所有嵌入式可能遇到的场景
嵌入式开发里,边界情况最容易出问题。我设计了5个测试用例,覆盖“正常、极端、异常”场景,每个用例都附代码和预期结果:
|
用例类型 编号 |
输入参数 |
预期输出 |
嵌入式场景对应 |
|
正常场景 1 |
n=3,startEnd=[[1,3],[2,4],[3,5]] |
2 |
3个传感器采集任务,部分重叠 |
|
边界场景 2 |
n=0(无活动) |
0 |
任务列表为空 |
|
边界场景 3 |
n=1(单个活动) |
1 |
单个串口通信任务 |
|
极端场景 4 |
n=4,所有活动重叠:[[1,10],[2,9],[3,8],[4,7]] |
4 |
4个任务同时执行,需4个硬件通道 |
|
异常场景 5 |
n=2,startEnd=NULL(数组为空) |
-2(错误码) |
任务列表指针异常(硬件传输错误) |


被折叠的 条评论
为什么被折叠?



