数组去重与数组越界:一道NOIP初赛题背后的编程陷阱
2026/9/17 15:50:35 网站建设 项目流程

1. 开篇:一道经典数组去重题,四个选项居然只错了一个

NOIP2008年普及组初赛的题目,放在今天看依然是算法思维训练的好素材。我最近重刷这道题时发现,第16题考察的是数组元素去重,但它的难点不在“会不会去重”,而在于你能不能一眼看出:四个逻辑几乎相同的程序里,哪一个的判断条件写错了。

先说结论,这道题的正确选项是C。但我更想聊的,不是答案本身,而是它在考什么、四个选项之间那一点点微妙的差异,以及如果是我当年在初赛考场上遇到它,应该怎么快速定位“错误源”。

题目大致场景是这样:程序要从键盘读入n个整数,去掉重复的数值后,把剩下的数按照原来的先后顺序输出。这四个选项都是同一个思路:用一个标记数组或者计数数组来记录某个数是否出现过,接下来遍历所有数,遇到“没见过的数”就输出,并且把它标记成“已经见过”。

但这里面有一个非常容易踩坑的细节——重复判断是在输入时做,还是先把所有数存下来再做。四个选项的差异就在这。

2. 为什么数组越界是这种题最隐蔽的坑

这道题的数据范围我记得很清楚:输入的每个数不会超过100。很多同学看到“不超过100”这个条件,第一反应是“那数组开个100不就够了”,如果你这么想,那这道题你大概率会错选。

来看四个选项分别怎么处理这条边界:

A和B的设计思路是完全一样的,区别只在判断条件上。它们先读入x,然后用a[x]这个数组去标记“x这个数字是否出现过”。如果a[x]等于0,说明x还没出现过,那就先把它存进另一个数组b,然后把a[x]置为1。这个逻辑没有任何问题。

但问题出在数组a的容量上。如果你只把a开成101的大小(下标0到100),那当输入的数字恰好等于100时,a[100]这个位置是可以正常访问的。可关键是,很多同学的代码习惯是开a[100],这在C++里下标只能访问到a[99],一旦读入的数字是100,程序就会越界访问。这道题的C选项就是这么被设计出来的。

C选项把a数组开成了100的大小,但它又允许输入数字的范围是1到100。当程序执行到x=100时,访问a[100]就越界了。在初赛这种笔试场景下,程序“看起来”还能跑,但你无法保证它输出的结果是正确的。要知道,数组越界在C++里是未定义行为,它可能恰好没有崩溃,也可能覆盖了其他变量,导致结果完全错误。

3. 四个选项逐个拆解:哪个是安全的,哪个是“隐性炸弹”

我先说A和B为什么是对的程序。

第一,它们把标记数组开得足够大。a数组的尺寸直接开到101以上,保证下标100的访问是合法的,不会越界。

第二,它们在读入时就完成了去重和记录,不需要额外排序,所以输出顺序天然保持输入的先后顺序,符合题目要求。

第三,它们用0和1标记“是否出现过”的思路,清晰、无歧义,在淘汰赛制的判断题里,这就是标准的、正确的方案。

再说C选项错在哪。前面已经提到了,它的a数组大小是100,无法容纳下标100的合法访问。当读入的数据中含有x=100时,程序就会越界。虽然在实际运行中可能不会立即报错,但从原理上讲,这个程序不能保证在所有合法输入下都能产生正确输出。对于初赛这种只看程序执行结果的题目,你没法说它“碰巧能跑通”就算正确。

最后看D选项。D在结构上和A、B略有区别,但它也是正确的:它把去重逻辑写成自定义函数check,每次输出前调用一遍。函数的功能是遍历已记录的数组,看当前这个数是否已经出现过。只能说它多了一层函数调用,但逻辑本身没有问题,而且它对数组长度的要求也更宽松,不存在C那种越界风险。

我把这四个选项的关键差异整理成一张表,方便你对照:

选项标记数组容量核心判断方式是否安全
A足够大直接按值访问标记数组
B足够大直接按值访问标记数组
C100(越界)直接按值访问标记数组,但容量不足
D视数据而定自定义函数线性查找去重

C选项说白了就是“思路对、容器不够”的典型代表。它和A、B唯一的区别,就在于那个数组大小的定义。

4. 这道题真正想教给你的:从“写出正确”到“确保安全”

说实话,在真实比赛里,没有人会给你提供一个“恰好越界也不会崩”的运行环境。初赛喜欢出这种题,就是因为它在考察你有没有形成安全编程的肌肉记忆

我当年学数组的时候,老师反复强调一句话:数组下标永远要往大了开,不要刚刚好。你要存100以内的数,就开105、110甚至更大一点;你要处理长度不确定的字符串,就预留两倍空间。这不是浪费,这是编程的基本修养。

再说说这道题背后隐含的知识点。它考的是三个东西:

第一,标记数组去重的思想。你用一个数组记录某个元素是否出现过,遍历一次原数据,把“第一次遇见”的元素挑出来,这就是最基础的去重方案,时间复杂度O(n),空间换时间。

第二,数组越界带来的隐患。C选项不是逻辑写错,是资源分配写错。这种错误在笔试里最容易被忽略,因为它不报错、不提示,它就是静默地产生未定义行为。

第三,对输入数据范围的理解。题目说“每个数不超过100”,那你的程序必须保证对x=100这种情况依然正确,而不是假设“一般不会考那么极端的边界值”。

5. 如果考试时遇到这种题,我的做题顺序

这里分享一个我自己的做题习惯。碰到这种给出四段相似代码让你挑错的题,我不会逐行读代码,而是按下面这个顺序排查:

第一步,看数组声明。先把所有数组的大小列出来,对照数据范围计算一遍,看有没有越界风险。这一步能解决一半以上的“找茬题”。

第二步,看循环边界。尤其是for循环的起止条件,是i<=n还是i<n,是i从0开始还是从1开始,这个配合数组下标看,经常能发现隐藏问题。

第三步,看判断条件。像这道题里“if(a[x]==0)”和“if(a[x]==1)”虽然只差一个数字,但逻辑完全相反。我一般会把判断条件翻译成自然语言,比如“如果这个数还没出现过,就输出它”,再跟题目要求做比对。

第四步,看特殊情况。输入最极端值会怎样?输入重复数据会怎样?数据量最大时会不会超时?把边界值代进去走一遍,比读十遍代码都有用。

这套流程用在十年前是有效的,放在现在面对更复杂的初赛题也一样适用。它本质上不是“做题技巧”,而是调试程序的基本功。

6. 结尾留个问题给你

C选项那种“数据范围恰好卡在数组边界”的陷阱,其实在现实中经常发生。比如你写一个统计函数,输入数据的取值范围是用户告诉你的,但用户说谎了或者数据源变了,你的数组就不够用了。这提醒我们,写代码的时候,永远别相信“数据的最大值就是这么多”这种话,预留余量是成本最低的风险控制手段。

如果你今天第一次接触数组去重,我建议你把A和D两个程序都亲手敲一遍,跑几组数据感受一下它们的差异。A的效率更高,D的代码可读性更好,没有绝对的对错,只有不同场景下的取舍。至于C,你可以在本地把数组故意改小,然后输入一个100试试看会发生什么——注意,有的编译器会直接崩溃,有的会静默出错,这正是未定义行为的可怕之处。

搞懂这一道题,比你刷十道重复题更有价值。

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

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

立即咨询