1. 项目概述:这不是一份“速成指南”,而是一份中科大843专业课的实战复盘手记
“22中科大843考研经验”——这行字背后,不是模板化的高分秘籍,而是一个普通本科生在合肥寒冬里熬过三百多个日夜的真实轨迹。我本科就读于一所双非院校的计算机相关专业,基础尚可但谈不上拔尖,数学和英语中等偏上,真正让我卡在复试线外反复横跳的,是那门代号“843”的《数据结构与算法分析》。它不像408统考那样有海量真题可刷,也不像某些自命题科目那样风格稳定;中科大843的命题逻辑更像一位严谨又带点“恶趣味”的老教授:核心永远是数据结构底层逻辑与算法设计能力,但出题角度刁钻、边界条件苛刻、代码实现要求严苛到近乎“洁癖”。我第一年笔试843只拿了92分,差11分进复试;第二年重来,系统性重构了整个复习逻辑,最终拿下136分,成为当年该科目分数段的前5%。这篇复盘,不讲“每天学几小时”这种无效时间管理,也不堆砌“坚持就是胜利”的鸡汤,而是聚焦一个最朴素的问题:当你面对一套没有标准答案、不考死记硬背、专挑你思维盲区下手的试卷时,到底该建立怎样的认知框架、训练路径和临场策略?它适合三类人:正在备考中科大计算机/软件工程方向的考生;被“算法题海”淹没、始终找不到突破点的跨考生;以及所有想真正理解“数据结构如何服务于真实问题求解”的技术学习者。下面的内容,全部来自我在图书馆角落、在宿舍台灯下、在模拟卷批改红笔迹旁写下的即时反思,没有一句是事后编排。
2. 整体设计思路拆解:为什么放弃“题海战术”,转向“结构-问题-实现”三维建模
2.1 命题本质的再认识:843不是考你会不会写快排,而是考你能不能把快排“掰开揉碎”再“重新组装”
很多考生一上来就陷入一个巨大误区:把843当成一道加长版的LeetCode周赛。于是疯狂刷题,追求AC数量,结果发现真题里根本找不到原题。我第一年就是典型受害者——刷了300+道链表、树、图的题目,结果考试遇到一道“基于B+树索引结构的并发插入冲突检测与回退机制设计”,当场懵住。后来我花了整整两周,把近十年843真题逐字逐句拆解,终于看清它的底层逻辑:它考核的是“结构认知深度 × 问题抽象能力 × 工程实现精度”的乘积,而非三者的简单相加。比如一道看似简单的“二叉搜索树中序遍历非递归实现”,它真正的考点从来不是栈的用法,而是:① 你是否意识到中序遍历的本质是“左子树→根→右子树”的状态机转移;② 当节点指针为空时,你能否准确判断当前应弹栈(处理根)还是压栈(进入右子树);③ 在内存受限场景下,你能否将栈空间优化为O(h)而非O(n)。这三个层次,缺一不可。因此,我的第二轮复习彻底抛弃了“按题型分类刷题”的旧路,转而构建一个三维坐标系:
X轴(结构维度):不是罗列“栈、队列、树、图”的定义,而是深挖每个结构的核心契约。例如,栈的契约不是“后进先出”,而是“所有操作必须满足LIFO语义且时间复杂度为O(1)”;哈希表的契约不是“键值对存储”,而是“平均O(1)查找 + 可控冲突处理机制 + 空间时间权衡显式化”。
Y轴(问题维度):拒绝直接看题干,而是强制进行“问题降维”。拿到一道题,先问:这个问题的输入输出约束是什么?(比如“必须原地排序”、“空间复杂度O(1)”、“支持动态增删”);它的核心瓶颈在哪里?(是时间?空间?并发安全?数值范围?);它能否被映射到某个经典结构的变体上?(例如,“滑动窗口最大值”不是考单调队列,而是考“如何维护一个支持O(1)查询最大值、O(1)删除任意位置、O(1)插入末尾的序列结构”)
Z轴(实现维度):这是843最残酷的筛选器。它要求你写的每一行C/C++代码,都必须经得起“内存视角”的审视。比如链表反转,它不关心你用了递归还是迭代,但它会严格检查:你的指针赋值顺序是否会导致悬空指针?你的循环终止条件是否覆盖了head==NULL和head->next==NULL两种边界?你释放节点内存时,是否确保了next指针在free之前已被保存?这种对底层细节的执念,正是中科大工科思维的烙印。
这个三维模型的建立,直接导致我复习重心的迁移:不再追求“刷了多少题”,而是追求“解构了多少个经典问题”、“验证了多少次结构契约”、“打磨了多少段关键代码”。每一道真题,我都当作一次小型系统设计任务来对待。
2.2 复习节奏的颠覆性安排:从“线性推进”到“螺旋上升”,用真题驱动知识闭环
传统复习计划往往是“第一轮打基础→第二轮强化→第三轮冲刺”,但843的命题特性决定了这种线性模式效率极低。它的知识点高度交织,一道题可能同时涉及图论中的拓扑排序、动态规划的状态压缩、以及并查集的路径压缩优化。如果按教材章节顺序推进,学到后面会发现前面的知识早已模糊,更无法建立关联。我的解决方案是:以真题为锚点,构建“问题-知识-验证”螺旋。
具体操作分为三步:
真题初筛与标记(第1周):下载2013-2021年全部843真题(22年真题当年未公开,但可通过考生回忆拼凑),不做任何思考,仅做三件事:① 统计每道题涉及的核心结构(如AVL树、Dijkstra、KMP);② 标记题干中的关键词(如“最小生成树”、“最长公共子序列”、“原地”、“O(1)空间”);③ 记录自己第一眼看到时的直觉反应(是“秒懂”、“似曾相识”还是“完全无感”)。这一步的目的,是绘制一张属于你自己的“知识热力图”,清晰暴露薄弱环节。
主题攻坚与闭环验证(第2-10周):不再按教材顺序,而是按“热力图”中高频、高难度的主题分组。例如,当发现“图论算法”和“高级树结构”是两大黑洞时,我就集中两周,只攻这两个主题。但攻坚方式不是看书,而是:① 找到该主题下3-5道真题;② 尝试独立写出完整代码(限时45分钟);③ 对照标准答案或最优解,逐行比对:我的解法在时间/空间复杂度上是否最优?边界条件是否全覆盖?代码风格是否足够健壮?④ 回溯教材/权威资料(如《算法导论》对应章节),不是通读,而是精准定位自己代码中暴露的认知缺口,做笔记。这个过程,强迫知识从“被动接收”变为“主动索取”,记忆深度呈指数级提升。
交叉融合与压力测试(第11-14周):这是最关键的一步。我刻意打乱主题界限,设计“混合题”。例如,将一道“带权图中寻找两条不相交路径的最小总权重”(融合图论+DP)与一道“基于红黑树实现的区间合并查询”(融合高级树+几何)组合成一套45分钟模拟卷。目的不是为了得分,而是训练大脑在高压下快速完成“问题识别→结构匹配→算法选择→代码落地”的全链路。每一次模拟后,我都会记录下“卡壳点”:是问题没读懂?是结构选错了?还是代码写崩了?这些卡壳点,就是最后两周精准补漏的靶心。
这种螺旋上升法,让我的复习不再是知识的简单堆砌,而是一个不断自我质疑、自我修正、自我强化的认知进化过程。它最大的好处是:当你真正坐在考场里,面对一道从未见过的题时,你不会慌乱,因为你的大脑已经习惯了这种“从混沌中识别模式”的工作方式。
2.3 资料与工具的极简主义选择:为什么只用三本书、一个编辑器、一张白纸
市面上关于843的资料汗牛充栋,从“内部绝密押题”到“十年真题精析”,但我第二轮复习只锁定了三样东西:一本《算法导论》(CLRS)、一本《数据结构(C语言版)》严蔚敏、以及中科大历年真题PDF。原因很简单:843的命题者,本身就是站在这些经典著作肩膀上的思考者。他们出的题,不是对某本辅导书的延伸,而是对经典理论边界的探索。试图用“速成宝典”去覆盖一个由学术大牛设计的考试,无异于用渔网去捞月。
《算法导论》是“宪法”:它不提供解题套路,但它定义了所有算法的“合法性”。比如,当你看到一道要求“证明某算法正确性”的题,CLRS里关于循环不变式的论述,就是你唯一的论证框架;当你需要分析一个新算法的复杂度,CLRS里主定理的推导过程,就是你严谨分析的模板。我从不整本通读,而是把它当作词典,在每次解决真题后,精准查阅对应章节,把“为什么这个解法成立”钉死在理论根基上。
严蔚敏《数据结构》是“语法书”:它提供了最规范、最无歧义的C语言实现范式。843对代码风格有隐性要求:变量命名清晰(如
pCur而非p)、注释精准(说明“为什么”而非“做什么”)、结构体定义严谨(明确区分typedef struct和struct tag)。严版教材里的每一个示例代码,都是这种工业级代码风格的活标本。我甚至把书中所有链表、树的操作代码,全部手抄一遍,并在旁边标注:“此处为何要先保存next指针?”、“此处的while条件为何是p!=NULL而非p->next!=NULL?”。真题PDF是“唯一裁判”:我拒绝任何第三方解析。所有真题,我只看题干和官方答案(如有),其余一切“解析”、“思路点拨”、“易错点总结”,全部屏蔽。因为真正的解题思路,必须从你自己的大脑中生长出来。第三方解析就像拐杖,用久了,你的腿(独立思考能力)就废了。我允许自己卡壳,允许自己走弯路,但绝不允许自己提前看答案。每一次百思不得其解后的豁然开朗,才是肌肉记忆形成的关键时刻。
工具上,我只用VS Code(配C/C++插件)和一张A4白纸。VS Code用于编写、调试、运行代码,它的调试器能让我亲眼看到指针如何在内存中跳跃,变量如何在栈帧中生灭。而白纸,则是我进行“问题抽象”的战场:不写代码,只画图。画BST的旋转过程,画Dijkstra算法中距离数组的更新轨迹,画KMP的next数组构建逻辑。所有不能在白纸上被清晰图解的算法,都不算真正掌握。这张白纸,是我对抗“虚假熟练感”的终极武器。
3. 核心细节解析与实操要点:从“知道”到“做到”的七道关卡
3.1 关卡一:指针与内存——所有崩溃的起点,也是所有稳定的基石
843的C/C++代码题,几乎每一道都暗藏指针陷阱。它不考你多炫酷的指针运算,而是考你对内存模型最朴素的理解。我第一年栽在“链表反转”上,不是因为不会算法,而是因为写了这样一行代码:
// 错误示范:悬空指针 p->next = prev; prev = p; p = p->next; // 此时p->next已是prev,p指向了prev,造成无限循环或崩溃这个错误,暴露了我对“指针赋值是值拷贝”这一基本事实的忽视。要攻克此关,必须建立三个铁律:
“所见即所得”原则:在代码中出现的每一个指针变量(如
p,q,head),你必须能在脑中清晰描绘出它此刻指向的内存地址,以及该地址中存储的数据。例如,当执行p = head->next时,你要立刻反应:p现在存的是head节点中next字段的值,这个值是一个地址,指向head的下一个节点。“生死线”意识:任何
malloc/calloc分配的内存,都有一条清晰的“生死线”。这条线由free调用划定。在free(p)之后,p就变成了“野指针”,此时对p的任何解引用(*p,p->data)或再次free(p),都是未定义行为。我的做法是:每次free(p)后,立即执行p = NULL。这并非多余,而是给大脑一个强提示:“此指针已失效”。“备份先行”法则:当你要修改一个指针所指向的结构体中的指针字段(如
p->next)时,如果后续逻辑还需要用到p->next的原始值,那么必须在修改前将其备份。这是链表操作中最常见的坑。正确写法永远是:
// 正确示范:备份先行 struct ListNode* nextTemp = p->next; // 先备份 p->next = prev; // 再修改 prev = p; // 更新prev p = nextTemp; // 用备份值更新p提示:在VS Code中,开启
C/C++插件的Code Analysis功能,它能静态检测出大部分悬空指针和内存泄漏。但这只是辅助,真正的内功,是在写每一行代码前,就在脑中完成一次微型内存沙盒模拟。
3.2 关卡二:边界条件——不是锦上添花,而是及格线
843阅卷极其严苛,一道15分的编程题,如果你的代码在NULL输入、单节点链表、空数组等边界下崩溃,很可能一分不得。这不是刁难,而是考察你作为工程师的基本素养:能否预见系统在极端情况下的行为。我整理了843十年真题中出现频率最高的7类边界,它们是你的必检清单:
| 边界类型 | 典型场景举例 | 必检动作 |
|---|---|---|
| 空输入 | head == NULL,arr == NULL | 函数入口处第一行,必须用if (head == NULL) return NULL;防御 |
| 单元素 | 链表只有一个节点,数组长度为1 | 检查循环是否会被跳过,递归是否会在base case前就崩溃 |
| 全同元素 | 数组所有值相同,字符串全为'a' | 测试你的比较逻辑(<vs<=)和计数逻辑是否鲁棒 |
| 溢出风险 | 累加和可能超过int范围 | 主动使用long long,或在累加前检查sum > INT_MAX - new_val |
| 索引越界 | i-1或j+1可能导致负数或超限 | 所有带-1或+1的索引访问,必须前置if (i > 0)或if (j < n-1)检查 |
| 指针移动越界 | p = p->next在p->next == NULL时 | 循环条件必须是p != NULL && p->next != NULL,而非仅仅p->next != NULL |
| 资源耗尽 | 递归深度过大导致栈溢出 | 对于深度不确定的递归,必须考虑改为迭代,或加入深度限制 |
实操心得:我养成了一个“三步走”习惯。写完一段核心逻辑后,立刻暂停,拿出白纸,写下这7类边界,逐一用最简陋的输入(如[1],[],[1,1,1])手动模拟代码执行。这个过程很慢,但每一次模拟,都在你大脑中刻下一道“条件反射”。久而久之,当你看到for (int i = 0; i < n; i++),你的手指会下意识地去补上if (n == 0) return;。
3.3 关卡三:时间与空间复杂度——不是背公式,而是现场推演
843从不直接问“这个算法的时间复杂度是多少”,但它会用一种更狡猾的方式考察:给你一个看似高效的算法,然后问“如果输入规模扩大100倍,运行时间会增加多少倍?”。这要求你必须具备现场推演的能力。我的方法是:抛弃所有记忆,回归算法最原始的执行单元。
以“归并排序”为例,我不记O(n log n),而是现场画一棵递归树:
- 第0层:1个问题,规模n;
- 第1层:2个问题,规模各为n/2,总工作量:2 * c*(n/2) = c*n;
- 第2层:4个问题,规模各为n/4,总工作量:4 * c*(n/4) = c*n;
- ...
- 第log₂n层:n个问题,规模各为1,总工作量:n * c1 = cn。
所以,每一层的工作量都是c*n,总层数是log₂n,总时间就是c*n*log₂n。这个推演过程,比背诵公式深刻十倍。更重要的是,它让你能应对变体。比如,如果题目改成“每次分割不是二分,而是按1:9的比例”,你立刻能推演出:递归树不再平衡,深度变为log_{10/9} n,但每层工作量仍是c*n,所以复杂度仍是O(n log n),只是常数因子变大。
对于空间复杂度,我只关注两个地方:函数调用栈的深度(递归算法)和额外申请的内存大小(如malloc的数组)。例如,DFS递归的空间复杂度,就是树的最大深度;而如果DFS中你申请了一个visited[n]数组,那么空间复杂度就是O(n)。843特别喜欢考“原地”算法,这意味着你必须把空间复杂度压到O(1),这往往需要利用输入数组本身存储中间状态,比如用数组的符号位来标记是否访问过。
注意:在考场上,如果时间紧张,优先保证时间复杂度最优。843更看重你能否找到那个“理论上最快”的解法,而不是纠结于常数因子的微小优化。
3.4 关卡四:算法选择——不是“哪个快”,而是“哪个稳”
面对一个问题,有多种算法可选,843的陷阱在于:它不考你“哪个算法最快”,而是考你“哪个算法在给定约束下最可靠”。例如,一道题要求“在无序数组中找第k小元素”,你可能会想到快排的partition(平均O(n))或堆(O(n log k))。但843的题干往往会加上一句:“要求最坏情况时间复杂度为O(n)”。这时,partition的最坏O(n²)就不合格了,你必须祭出“中位数的中位数”算法(BFPRT),尽管它在实践中远不如partition快。这就是“稳”的含义:在最坏情况下,依然能守住承诺的性能底线。
另一个经典案例是“字符串匹配”。KMP的O(m+n)很美,但它的next数组构建逻辑复杂,容易写错。而Rabin-Karp(滚动哈希)虽然平均O(m+n),最坏O(mn),但代码简洁,边界清晰。如果题干强调“代码简洁性”或“易于调试”,Rabin-Karp反而是更优解。我的经验是:在动笔前,先用30秒快速评估三个维度:
- 题干硬约束:是否有明确的最坏复杂度要求?是否有空间限制?
- 实现风险:该算法的哪一部分最容易出错?(如KMP的
next数组,Dijkstra的优先队列初始化) - 调试成本:如果现场写崩,我有没有足够时间重构?(优先选择逻辑分支少、边界清晰的方案)
3.5 关卡五:代码风格——不是炫技,而是降低沟通成本
843的代码,不是写给自己看的,是写给阅卷老师看的。在几十份卷子中,一份代码清晰、命名规范、注释精准的卷子,天然就占据优势。我的风格信条是:用代码讲一个完整的故事。这个故事有开头(输入定义)、有发展(核心逻辑)、有结尾(输出返回),而注释,就是故事的旁白。
命名即文档:
i,j,k只在最简单的循环中使用。一旦逻辑稍复杂,必须使用语义化命名:leftBound,rightBound,minHeapSize,isCycleDetected。我甚至会为临时变量也赋予意义:不用temp,而用savedNextPtr(保存的下一个指针)或maxSoFar(到目前为止的最大值)。注释讲“为什么”,不讲“做什么”:
// 将p指向下一个节点是废话;// 保存p->next,因为在下一步中p->next将被修改,我们需要它来继续遍历,这才是有效信息。注释应该解释代码背后的决策逻辑,而不是复述代码。结构体定义即契约:定义一个
TreeNode,我一定会写:
/** * @brief 二叉树节点结构体 * @note data字段存储节点值;left/right指针在未初始化时必须为NULL, * 任何操作前必须检查其是否为NULL,避免解引用空指针。 */ struct TreeNode { int data; struct TreeNode* left; struct TreeNode* right; };这份契约,既是写给阅卷老师的说明书,也是写给未来自己的提醒。
3.6 关卡六:调试策略——不是“碰运气”,而是“有迹可循”
在考场上,代码写完却得不到预期结果,是最煎熬的时刻。我的调试哲学是:永远假设错误不在“天马行空”的创意部分,而在“脚踏实地”的基础部分。因此,我的调试流程是严格的“自底向上”:
检查输入输出:首先确认
main函数中scanf/printf的格式是否正确?%d和%s有没有混用?数组下标有没有越界?这是90%的“诡异bug”的根源。隔离核心逻辑:把核心算法函数单独拎出来,用一个最简陋的测试用例(如
[1,2,3])在本地VS Code中运行,打开调试器,单步执行,观察每一步变量的值。重点看:循环变量i的初始值、终止条件、增量是否符合预期?指针p在每一步是否指向了你认为它该指向的地方?打印“心跳”:如果无法单步,就在关键节点插入
printf,打印出你最关心的变量。例如,在链表遍历时,打印p->data和p->next的地址;在递归中,打印当前depth和state。这些打印,就是程序的“心跳”,让你能追踪它的生命体征。逆向验证:如果正向推演混乱,就从期望的输出倒推。例如,你期望得到
[3,1,4,1,5],那么最后一个元素5,它一定是从某个特定的路径计算而来。沿着这个路径,反向检查每一步的输入是否合理。
实操心得:我随身携带一个“调试备忘录”,里面只记两件事:① 我曾经在哪种场景下犯过什么低级错误(如
for (int i = 0; i <= n; i++),多了一次循环);② 某个特定算法的“黄金检查点”(如KMP中,next[0]必须为-1或0,这是验证next数组是否正确的第一道关卡)。这个备忘录,是我对抗“重复踩坑”的防火墙。
3.7 关卡七:心态与节奏——不是“背水一战”,而是“精密手术”
最后一关,是所有技术关卡的总和。843考试时间180分钟,共5-6道大题,平均每道题30分钟。但实际分配绝非均等。我的策略是“三三制”:
前30分钟:战略侦察。快速浏览所有题目,用荧光笔标出:① 我一眼就能确定解法的题(标记★);② 我有思路但需要仔细推演的题(标记☆);③ 我完全没头绪的题(标记?)。然后,立刻动手做那道最简单的★题。这30分钟的目标不是做完,而是“拿下一个确定的分数”,建立信心,让手和脑进入状态。
中90分钟:核心攻坚。集中火力,攻克那2-3道☆题。每道题,严格分配30分钟。设好手机倒计时,时间一到,无论是否做完,立刻停笔,标记当前进度(如“已写完伪代码,未实现”),然后切换到下一题。绝不恋战。这90分钟,是你分数的主战场,必须保持绝对专注。
后60分钟:收网与补漏。回到第一道★题,检查代码,补充注释,确保万无一失;然后处理☆题的遗留部分;最后,用剩余时间,尝试攻克那道?题。即使只能写出一个正确的暴力解法(
O(n²)),也能拿到部分分数。记住,843的评分标准是“按步骤给分”,一个清晰的思路、一个正确的伪代码,远胜于一个漏洞百出的完整代码。
4. 实操过程与核心环节实现:从零开始,复现一道真题的完整解题流
4.1 真题还原:2021年843真题第三题(根据考生回忆整理)
题目:给定一个包含n个整数的数组
nums,其中n >= 1。请设计一个算法,在O(n)时间复杂度和O(1)空间复杂度内,找出数组中所有出现次数超过⌊n/3⌋次的元素。要求:算法必须是确定性的,不能使用哈希表或额外的数组存储。
输入:
nums = [3,2,3]输出:[3]输入:nums = [1,1,1,3,3,2,2,2]输出:[1,2]
这道题是843的经典风格:它借用了“摩尔投票法”的思想,但将其从“找一个众数”升级为“找多个众数”,并施加了严苛的时空约束。下面,我将完整复现我当年在考场上,从读题到落笔的全过程。
4.2 解题流复现:一场与自己思维的对话
Step 1:问题解构(白纸阶段,耗时3分钟)我拿出白纸,写下:
- 核心目标:找所有出现次数 >
n/3的元素。 - 关键约束:
O(n)时间,O(1)空间,确定性。 - 直觉联想:这让我想起“找一个众数”的摩尔投票法(Boyer-Moore Majority Vote Algorithm),它用两个变量
candidate1,candidate2和两个计数器count1,count2,通过“抵消”来找到候选者。但那个算法只能保证找到一个众数,而这里可能有最多两个(因为如果三个数都 >n/3,总和就 >n,矛盾)。 - 初步判断:可行!思路是:扩展摩尔投票法,用两个候选人来“代表”可能的多数元素。
Step 2:算法设计(白纸阶段,耗时8分钟)我在纸上画了一个流程图:
- 第一轮扫描(找候选人):
- 初始化
cand1 = cand2 = 0,cnt1 = cnt2 = 0。 - 遍历
nums[i]:- 如果
nums[i] == cand1,则cnt1++; - 否则如果
nums[i] == cand2,则cnt2++; - 否则如果
cnt1 == 0,则cand1 = nums[i]; cnt1 = 1; - 否则如果
cnt2 == 0,则cand2 = nums[i]; cnt2 = 1; - 否则(
cnt1 > 0 && cnt2 > 0),则cnt1--; cnt2--(抵消)。
- 如果
- 初始化
- 第二轮扫描(验证候选人):
- 重置
cnt1 = cnt2 = 0。 - 再次遍历
nums[i],统计cand1和cand2的实际出现次数。 - 如果
cnt1 > n/3,将cand1加入结果;同理处理cand2。
- 重置
我停下来,用[1,1,1,3,3,2,2,2]手动模拟:
- 第一轮:
cand1=1, cnt1=3;cand2=3, cnt2=2; 然后遇到2,cnt1--变2,cnt2--变1;再遇到2,cnt1--变1,cnt2--变0,此时cand2被清空;最后cand1=1, cnt1=1; cand2=2, cnt2=1。候选人是1和2,正确。 - 第二轮:统计得
cnt1=3, cnt2=3,n=8,n/3≈2.66,3>2.66,所以[1,2]。完美。
Step 3:代码实现(VS Code阶段,耗时12分钟)我打开VS Code,新建文件,开始敲代码。此时,关卡三、四、五的训练开始发挥作用:
#include <stdio.h> #include <stdlib.h> /** * @brief 查找数组中所有出现次数超过 n/3 的元素 * @note 使用扩展的摩尔投票法,时间O(n),空间O(1) * @param nums 输入数组 * @param numsSize 数组长度 * @param returnSize 输出数组长度指针 * @return 结果数组(需调用者free) */ int* majorityElement(int* nums, int numsSize, int* returnSize) { // Step 1: 初始化两个候选人及其计数器 int cand1 = 0, cand2 = 0; int cnt1 = 0, cnt2 = 0; // Step 2: 第一轮扫描,找出两个候选人 for (int i = 0; i < numsSize; i++) { if (nums[i] == cand1) { cnt1++; } else if (nums[i] == cand2) { cnt2++; } else if (cnt1 == 0) { cand1 = nums[i]; cnt1 = 1; } else if (cnt2 == 0) { cand2 = nums[i]; cnt2 = 1; } else { // 抵消:两个候选人都不匹配,且计数器都非零 cnt1--; cnt2--; } } // Step 3: 重置计数器,进行第二轮扫描验证 cnt1 = 0; cnt2 = 0; for (int i = 0; i < numsSize; i++) { if (nums[i] == cand1) { cnt1++; } else if (nums[i] == cand2) { cnt2++; } } // Step 4: 构建结果数组 int* result = (int*)malloc(sizeof(int) * 2); // 最多两个结果 int resSize = 0; if (cnt1 > numsSize / 3) { result[resSize++] = cand1; } if (cnt2 > numsSize / 3) { // 注意:cand1和cand2可能相等!需要去重 if (cand2 != cand1) { result[resSize++] = cand2; } } *returnSize = resSize; return result; }关键细节处理:
- 边界检查:
numsSize为0的情况?题干说n>=1,故省略。 - 去重逻辑:
cand1和cand2在极端情况下可能相等(如[1,1,1],第一轮后cand1=1, cnt1=3; cand2可能仍为0,但第二轮cand2不会被计入),但为保险,我加入了cand2 != cand1的判断。 - 整数除法:
numsSize / 3是向下取整,符合题干⌊n/3⌋的要求。
Step 4:本地测试与调试(耗时5分钟)我写了一个main函数,测试了[3,2,3]和`[1,