☰
LeetCode 228汇总区间C语言实现:边界处理与扫描法详解
2026/10/16 5:02:48 网站建设 项目流程

LeetCode 228 这道汇总区间,在题库里躺了很久,难度标的是 Easy,但如果你真在面试现场手写一遍,翻车率比想象中高不少。题目说人话就是:给一个有序无重复的整数数组,把连续出现的数字压成区间,比如 0、1、2 变成 “0->2”,4、5 变成 “4->5”,孤零零的 7 就写成 “7”。用 C 语言实现尤其酸爽,因为除了业务逻辑,你还要自己管二维字符数组、字符串长度、边界收尾,稍不注意就是越界或者漏掉最后一个区间。

我最初刷这题时觉得也太简单了,无非就是遍历一遍,连续就继续走,不连续就切断。结果写出来提交,先是空数组返回出错,再是最后一段区间莫名丢失,折腾了好一会儿才意识到:这题真正的考点根本不是“能不能想到扫描”,而是“能不能把边界处理得干干净净”。下面我把自己的思路演进、图解过程和 C 语言实现细节完整拆开讲一遍,希望帮你少走我当年走过的弯路。

1. 先看懂题目:它到底要你做什么

1.1 题意拆解与示例分析

题目给的是一个升序排列的、无重复元素的整数数组。所谓“汇总区间”,就是把数组中连续的一段整数合并成一个字符串形式的区间。比如数组[0,1,2,4,5,7],因为 0、1、2 是连续的三个数,所以它们合并成"0->2";4、5 连续,合并成"4->5";7 单独一个数,没有邻居和它连续,所以它自己就是"7"。最终返回的是一个字符串数组,在 C 语言里就是char**。

这里最容易被忽略的一点是:单个数字也要作为一个区间输出,只是不用带箭头。换句话说,每个元素最终都必然落在某个区间里,要么是“起点->终点”,要么是“孤点”。这一点想清楚了,空数组和单元素数组的特殊情况也就好办了。

1.2 输出格式决定了边界处理方式

输出要求里藏着两个小坑。第一个坑是箭头的写法,是->,不少同学紧张的时候会写成-或者-->,这种低级的格式错误在笔试判题时直接零分。第二个坑是字符串数组的“长度信息”怎么交给调用者。LeetCode 的 C 语言接口设计比较古老,除了返回char**,还要通过returnSize说明有几个字符串,通过returnColumnSizes说明每个字符串的长度。很多刚开始刷 LeetCode 的同学不熟悉这套接口,代码逻辑写对了,最后忘了给returnSize赋值,照样报错。

1.3 空数组和单元素数组是天然的测试用例

如果输入数组为空,那么一个区间都没有,返回的应该是空数组,returnSize设为 0。如果输入只有一个元素,比如[5],那么它本身就是一个区间,输出是["5"]。这两个极端情况看起来简单,但实际写代码时,很多人会把循环遍历的条件写错,导致空数组时直接访问越界,或者单元素时没有进入循环体、最后漏掉收尾。后面我会专门用一个章节讲这些边界条件,这里先留个印象。

2. 思路演进:从暴力模拟到单次扫描

2.1 第一反应往往是双层循环

看到“连续区间”这个需求,最自然的想法是:外层循环定一个起点,内层循环从起点往后找,只要下一个元素等于当前元素加一,就继续往后走;一旦断开,就把起点到终点这段截出来,然后让外层循环跳到终点的下一个位置重新开始。

这个思路完全正确,时间复杂度也是 O(n),因为每个元素最多被访问两次。但问题是实现起来有点笨重,代码里要同时维护外层循环的游标和内层循环的终点,还容易在“跳转”的时候写乱。尤其是当数组很大、区间很多时,两层循环嵌套在一起,阅读体验很差。面试官大概率会追问一句:能不能只用一个循环搞定?

2.2 单指针扫描才是这题的标准姿势

其实仔细想想,我们只需要一个指针i从头往后遍历,再用一个变量start记录当前区间的起点。每次比较nums[i]和nums[i+1],如果它们相差 1,说明还在同一个连续段里,继续往前走;如果相差不是 1,说明连续段在这里断开了,这时候把start到i这段区间生成字符串,然后把start更新为i+1,继续下一段。

这个方案的关键在于:我们不是“找到终点再输出”,而是在遇到断点时“回头”输出已经走过的区间。因为数组是有序且无重复的,所以判断连续只需要看nums[i+1] - nums[i] == 1,不需要额外判断大小关系。这个条件一写,问题就清晰了很多。

2.3 为什么不需要花哨的双指针技巧

很多人看到“区间”两个字,下意识会想到双指针,觉得一个指向区间起点、一个指向区间终点才专业。但实际上这题用双指针是一种“过度设计”。你当然可以用两个变量,一个start一个end,但end本质上就是遍历指针i,没有必要再单独维护一个变量。反而是单指针加一个起点标记,代码更简洁,也更容易让读代码的人理解逻辑。

如果非要说“双指针”,那start可以看作慢指针,i可以看作快指针,但它们的移动节奏不是“一个追一个”,而是“起点固定、快指针探路,断开时起点跳过去”。这种思想在处理数组连续段问题时非常通用,后面我在扩展章节里还会提到。

3. 图解核心逻辑:区间的生成与收尾

3.1 用一张过程表走通整个样例

纸上谈兵没用,我们直接拿题目给的样例[0,1,2,4,5,7]走一遍。假设当前区间的起点start = 0,遍历指针i从 0 开始:

inums[i]nums[i+1]是否连续动作
001是(1-0=1)继续,i 变为 1
112是(2-1=1)继续,i 变为 2
224否(4-2=2)输出"0->2",start=3,i 变为 3
345是(5-4=1)继续,i 变为 4
457否(7-5=2)输出"4->5",start=5,i 变为 5
57无(越界)-不比较,循环结束

循环结束后,还有一个"7"没有输出,因为它是最后一个元素,后面没有nums[i+1]来触发“断开”判断。所以必须在循环结束后单独补一次收尾。这就是很多人漏掉最后一段区间的根本原因。

3.2 为什么每次“断开”都发生在 i 的位置

注意看上表,当nums[i+1]和nums[i]不连续时,i指向的是当前连续段的最后一个元素,start指向的是当前连续段的第一个元素。所以我们输出的是start到i,而不是start到i+1。这个细节很关键,如果你脑子一热写成start到i+1,就会把下一个新段落的开头元素错误地包含进上一个区间。

可以这样记:当我们发现“前面走不动了”,说明i已经站在当前区间的终点。判断断开的那个nums[i+1],不属于当前区间,而是下一个区间的起点。

3.3 三种边界条件的图解

第一个边界:空数组。没有任何元素,循环条件i < numsSize - 1一开始就不成立,循环体不执行,最后也不需要收尾,直接返回空结果,returnSize = 0。

第二个边界:只有一个元素。循环条件同样不成立,但最后收尾时必须把nums[0]单独转成字符串,否则会丢区间。

第三个边界:整个数组完全连续,比如[1,2,3,4]。循环里一次断点都没遇到,所有元素都还在同一个待输出区间里,此时全靠最后的收尾逻辑把"1->4"输出。所以收尾逻辑不是可有可无的补充,而是主流程的一部分。

4. C语言实现:接口、内存与完整代码

4.1 这个函数签名到底是什么意思

LeetCode 上 C 语言的函数原型一般是这样的:

char **summaryRanges(int *nums, int numsSize, int *returnSize, int **returnColumnSizes);

很多人看到char**和int**就直接懵了。其实想清楚内存模型就简单了:

  • 返回值是char**,意思是“一个指针,指向一个数组,数组里每个元素是char*,也就是一个字符串”。
  • returnSize是“输出参数”,调用者传入一个指针,函数内部通过*returnSize = n告诉调用者返回了几个字符串。
  • returnColumnSizes是“输出参数里的输出参数”,它本身是指针的指针。函数内部要分配一个数组,数组长度等于*returnSize,然后让(*returnColumnSizes)[i]表示第i个字符串的长度(不含结尾的\0)。

换句话说,你要自己malloc三块内存:第一块是char**数组本身,第二块是int*数组用来存每个字符串长度,第三块才是每个字符串的实际内容。很多 C 语言新手在这道题上挂掉,不是因为算法不会,而是因为指针绕不明白。

4.2 完整可运行代码

下面是我整理后的 C 语言实现,逻辑上采用“先统计区间数量、再二次扫描生成字符串”的策略。这样做代码更稳,也避免了在遍历过程中频繁realloc。

#include <stdio.h> #include <stdlib.h> #include <string.h> char **summaryRanges(int *nums, int numsSize, int *returnSize, int **returnColumnSizes) { if (numsSize == 0) { *returnSize = 0; *returnColumnSizes = NULL; return NULL; } // 第一次扫描:统计区间数量 int cnt = 0; for (int i = 0; i < numsSize; i++) { if (i + 1 < numsSize && nums[i + 1] - nums[i] == 1) { continue; } cnt++; } // 分配返回数组和长度数组 char **result = (char **)malloc(sizeof(char *) * cnt); int *colSizes = (int *)malloc(sizeof(int) * cnt); int idx = 0; int start = 0; char buffer[64]; // 足够容纳 INT_MIN 和 INT_MAX 组合 for (int i = 0; i < numsSize; i++) { if (i + 1 < numsSize && nums[i + 1] - nums[i] == 1) { continue; } // 输出区间 start -> i if (start == i) { sprintf(buffer, "%d", nums[start]); } else { sprintf(buffer, "%d->%d", nums[start], nums[i]); } int len = strlen(buffer); result[idx] = (char *)malloc(sizeof(char) * (len + 1)); strcpy(result[idx], buffer); colSizes[idx] = len; idx++; start = i + 1; } *returnSize = cnt; *returnColumnSizes = colSizes; return result; }

这段代码的核心思想是:第一个循环用来统计区间数量,第二个循环真正生成字符串。两个循环的判断条件一模一样,都是“当前元素和下一个元素是否连续”,如果不连续,就说明当前元素是一个区间的结束点。

4.3 先统计区间数再分配,这个顺序别省

有人可能会问:明明可以一边遍历一边malloc字符串并不断realloc整个指针数组,为什么非要先统计一遍区间数量?

因为边遍历边扩容会引入三个麻烦。第一,realloc可能搬移内存地址,如果你在遍历过程中已经让某些指针指向原来的内存块,搬移后旧地址就失效了,容易产生悬垂指针。第二,realloc是 O(容量) 的,多次扩容会让总时间变成 O(n^2) 级别,虽然 LeetCode 数据量小可能感觉不出来,但这种写法本身不够优雅。第三,代码里多了一套“扩容判断”逻辑,容易分散注意力,增加 bug 概率。

先扫描一遍统计数量,成本是 O(n),然后一次性分配刚刚好的空间,再扫描一遍生成内容,总时间依然是 O(n)。两次循环看起来有点浪费,实际代价微乎其微,换来的却是代码结构的极度清晰。

4.4 字符串长度与缓冲区的安全计算

C 语言里没有免拼接的字符串,所以我们用sprintf把数字或区间写到临时缓冲区。

那么缓冲区开多大才安全?32 位int的最小值是-2147483648,最长 11 个字符,最大值2147483647,最长 10 个字符。区间字符串最长的情况是两个边界值都有这么多位,还要加上中间的->两个字符和结尾的\0,总共 11 + 2 + 10 + 1 = 24 个字节。所以我开char buffer[64],空间绰绰有余,不用担心溢出。

如果是在嵌入式环境里想省内存,可以精确到 32 字节,但为了可读性和稳健性,64 字节更合适。这里有个小提醒:不要用char buffer[16]这种凭感觉开的尺寸,万一测试用例是[-2147483648,2147483647],缓冲区直接爆掉,这是典型的未定义行为,排查起来非常恶心。

5. 常见错误与排查技巧实录

5.1 越界、漏尾部、格式错,这老三样最致命

我在实际调试和帮别人 review 代码时,发现这题的错误几乎都集中在三件事上。

第一个是越界访问。最经典的写法是:

for (int i = 0; i < numsSize; i++) { if (nums[i + 1] - nums[i] == 1) { ... } }

当i = numsSize - 1时,nums[i + 1]访问到了数组末尾之后,这是典型的内存越界。正确的循环上界应该是i < numsSize - 1,或者在访问前先判断i + 1 < numsSize。

第二个是漏掉最后一个区间。这个我在图解部分已经强调过,不重复了。简单说就是:收尾代码一定要写,而且要放在循环外面。

第三个是格式错误。->是减号加大于号,中间没有空格。"0 -> 2"和"0->2"是完全不同的两个字符串,判题系统只看字符精确匹配,多一个空格都会错。

5.2 内存分配和 returnSize 赋值错误

先看一个非常典型的错误版本:

*returnColumnSizes = (int*)malloc(sizeof(int) * cnt);

这里没问题。问题通常出在“先分配后忘记赋值”或“返回值是 NULL 但 returnSize 没设 0”。比如空数组时,如果直接返回 NULL,那么必须在返回前设置*returnSize = 0,否则调用者会拿一个错误的大小去遍历空指针。LeetCode 的测试代码有时候会用*returnSize来控制访问范围,所以这个值必须准确。

另外,returnColumnSizes里的长度应该是不含\0的字符串长度,也就是strlen的结果。如果写成len + 1,那你交给判题系统的长度就比实际字符串多了一个结束符,后面的用例对比也会出问题。

5.3 自测用例清单

我强烈建议写完代码后,至少跑一遍下面这张表,把每个边界都照顾到。

输入数组期望输出最容易犯的错
[][]忘记设置 returnSize=0
[3]["3"]循环进不去,收尾遗漏
[1,2,3,4]["1->4"]收尾遗漏
[1,3,5]["1","3","5"]把所有元素合并成一个区间
[-3,-2,-1,5]["-3->-1","5"]负数判断连续时写错条件
[-2147483648,-2147483647,2147483647]["-2147483648->-2147483647","2147483647"]缓冲区溢出或 INT_MIN 处理错误

我见过有人把nums[i+1] - nums[i] == 1写成nums[i] + 1 == nums[i+1],在绝大多数情况下没问题,但有些静态检查工具会报整数溢出风险。改成差值形式后,既安全又清晰,推荐默认使用。

6. 延伸:这道题背后的通用扫描思想

6.1 同类题目的对照

LeetCode 上有一道经典题叫“缺失区间”,意思是给你一个数组和一个下界上界,让你找出所有缺失的数字区间。那道题的最优解思路和汇总区间几乎一样,都是维护一个start,然后依次检查当前元素和start的关系,遇到差值大于 1 就补一个区间,然后更新start。

另外,“合并区间”那道题也是处理区间,但它的输入不是有序数组,而是若干已经给定的区间,需要先排序再合并。相比之下,汇总区间是“从有序数组中天然提取区间”,合并区间是“把可能重叠的人工区间整理成不相交区间”,两者的共同点是:排序或有序性决定了能否用单次扫描完成合并。

还有一个可以顺带练手的场景是“统计连续天数”“合并连续 IP 段”之类的业务问题。底层逻辑一模一样:数据有序,相邻差值为 1 则视为一段,断开则输出一段。把这道题吃透,遇到这类问题就会很自然地想到扫描法。

6.2 面试官追问时可以怎么答

如果面试官在写完代码后继续追问,常见的有三个方向。

第一个方向:如果输入数组是无序的怎么办?答案很简单,先排序,代价 O(n log n),然后沿用同样的扫描逻辑。排序本身不会破坏“无重复”的性质,反而保证了相邻比较的正确性。

第二个方向:如果数字不是整数而是浮点数,还能这样判断连续吗?不能。浮点数的“连续”没有严格定义,差值为 1.0 不代表数学上相邻。这题依赖整数的离散性质,换到浮点数域就要重新定义“区间”的粒度。

第三个方向:能不能用更少的内存?如果你不需要保留原始数组,可以创建结果时直接复用某些临时变量,但无论如何结果字符串本身必须单独存储。相比 O(n) 的输出空间,算法额外使用的内存已经是 O(1),没有进一步压缩的必要。

6.3 我个人的实操体会

这题我刷过不止一遍,后来给朋友讲这道题时,总结了一个特别简单的口诀:判断用 i+1,输出用 i 收口。也就是说,比较连续时向前看一位,但真正决定“当前区间到哪结束”时,用的是i而不是i + 1。这个细节我犯过一次错后彻底记住了。

另外一个小技巧是,调试这种“扫描后分段输出”的题,不要只盯着 LeetCode 的报错信息。先在本地把每一步的start和i打出来,对比自己纸上的推演过程,基本一眼就能看出是哪一步跳错了。我习惯在调试输出里写成:

printf("i=%d, start=%d, nums[%d]=%d\n", i, start, i, nums[i]);

等逻辑确认无误了,再把这些输出删掉。别小看这个习惯,能帮你省下大量反复提交试错的时间。

如果你手头还有别的区间类题目没做明白,我建议先回到这题把扫描法嚼透,因为它是所有“连续段合并”问题的最小范式。代码本身短,但该有的指针操作、边界处理、字符串生成全都有,非常适合作为 C 语言手撕算法的练手题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询