版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
字节跳动/腾讯/阿里技术面
面经大全:项目深挖与算法攻坚文档类型:面试指南与全真面经题库
适用对象:目标为互联网头部企业技术岗位(软件开发工程师、后端开发工程师、算法工程师)的校招与社招求职者
核心承诺:本书提供项目深挖方法论体系与50道核心算法攻坚真题的完整解析、30道项目深挖高频追问与高分示范作答、6套全真模拟技术面试场景演练、4个可直接填写的面试准备工具模板、12个常见面试误区及避坑策略、以及附录中的8项关键资源索引。摘要本书专为冲击字节跳动、腾讯、阿里巴巴等互联网头部企业技术岗的求职者定制,聚焦技术面试中最具决定性的两大模块——项目深挖与算法攻坚。全书以独创的“项目叙事金字塔”模型和“算法思维五步法”为核心方法论,深度拆解50道高频算法真题(涵盖数组与字符串、链表与树、动态规划与贪心、图与搜索、系统设计五大专题,每题提供完整题干、多解法对比、逐行代码注释与时空复杂度分析),并配备30道项目深挖追问的满分示范作答。本书独特价值在于:不堆砌零散面经,而是提供可复用的思维框架与沟通模板,助力求职者在面试中系统性地展现技术深度、工程素养和问题解决能力。配套提供4个面试准备工具模板(项目经历梳理表、技术问题复盘矩阵、算法题型速查表、面试后自我评估表)、12个常见失误深度剖析及避坑指南,以及8项关键学习资源推荐。使用说明与学习目标本书建议按照“先方法论,后实战题库”的顺序阅读,首先深入理解项目深挖的叙事框架和算法攻坚的思维模型,再通过真题演练巩固技巧。每道算法真题的解析结构统一为:题目描述与约束条件、至少两种解法的思路对比、最优解法的完整代码实现(带逐行注释)、时空复杂度严格推导、同类题型举一反三。项目深挖部分的追问例题均提炼自真实面试场景,建议先自行思考作答,再对照高分示范进行复盘。每章节末的“本章小结”为该章节精华的浓缩,建议在面试前反复回顾。配套工具模板为可打印的实战工具,建议在准备阶段完整填写并持续迭代。学习完成后,求职者应能够独立完成算法题目的多维度分析、结构化项目经历的流畅阐述,并在压力追问下保持清晰的逻辑表达。适用人群与阅读路径建议适用人群核心痛点推荐阅读路径行动指示应届毕业生(校招)项目经历单薄,不知如何展开;算法基础扎实但缺乏面试技巧第一章全部→第二章算法攻坚(重点刷题)→第三章项目深挖(学习将课设包装为亮点项目)→第五章全真模拟立刻开始填写“项目经历梳理表”,为每个项目提炼三个技术亮点1-3年社招开发者日常工作与面试考察脱节;系统设计能力薄弱;不知如何在项目陈述中体现技术深度第一章(跳读考情)→第三章项目深挖(重点学习叙事金字塔模型)→第四章系统设计专题→第五章全真模拟将当前工作中的核心项目用叙事金字塔模型重构,录音自听并反复打磨3年以上资深开发者需要展示架构能力和技术领导力;面临系统设计和高难度算法的双重挑战第三章项目深挖(学习高阶叙事技巧)→第二章专题三、四、五(动态规划、图、系统设计)→第五章全真模拟后两套准备一个“技术难题复盘矩阵”,梳理职业生涯中三个最具挑战性的技术决策跨行业转行者缺乏行业认可的工程实践;技术栈不完全匹配;简历筛选通过率低第一章(全面学习面试全流程)→第二章(高强度刷题建立信心)→第三章(学习用通用项目经验匹配目标岗位要求)→第六章误区避坑选定一个目标方向,用60天时间完成核心算法题库的练习,同时准备两个可深度展示的个人项目第一章互联网大厂技术面试全景透视1.1头部企业技术面试流程深度解析互联网大厂的技术面试体系经过多年迭代,已形成一套成熟、标准化、侧重实操能力评估的多轮筛选机制。理解这套机制背后的设计逻辑,是制定有效面试策略的前提。字节跳动、腾讯、阿里巴巴的技术面试流程虽各有侧重,但核心框架高度一致,通常包含以下五个阶段。第一阶段:简历筛选与技术初筛(HR/招聘系统)此阶段通常不涉及直接的面试官接触,但却是淘汰率最高的环节。简历筛选的核心标准并非关键词的简单匹配,而是对技术深度、项目成果和工程经验的综合判断。一份合格的简历需要在10秒内传递三个核心信息:掌握什么核心技术栈、在什么规模的项目中应用过这些技术、带来了什么可量化的结果。第二阶段:技术笔试/在线编程测试(OnlineAssessment)绝大多数校招岗位和部分社招岗位设有此轮。平台通常采用牛客网、赛码网或自研系统,时长90至120分钟,题型涵盖选择题(数据结构、操作系统、计算机网络基础)、编程题(算法实现)和少量的问答题。此阶段的通过率通常在20%至40%之间,核心筛选目标是基础编码能力不扎实的候选人。备考的关键在于高强度、有针对性的刷题,且必须在指定平台模拟真实限时环境。第三阶段:技术一面(骨干工程师/潜在同事)技术一面是深度技术面试的真正开始,面试官通常是未来可能并肩工作的同事或一线技术骨干。此轮面试的结构高度标准化:首先是简短的自我介绍(1至2分钟),随后进入核心的项目深挖环节(15至25分钟),面试官会围绕简历上的一到两个核心项目进行层层递进式的追问,从技术选型、架构设计、难点攻克、性能优化到最终效果复盘,力求全方位还原候选人在项目中的真实参与度和技术贡献度。之后是算法与数据结构考察环节(20至30分钟),通常为1至2道中等难度的在线编程题,要求在白板、共享编辑器或在线平台上手写代码并解释思路。最后是5至10分钟的提问环节。一面面试官的核心评价维度是:基础是否扎实、代码是否干净、沟通是否顺畅、是否具备解决实际问题的工程思维。第四阶段:技术二面(团队主管/资深工程师)二面面试官通常是一面的上级或团队中的技术专家,考察维度从“能不能干活”升级为“能不能把事情干好、能不能解决复杂问题”。项目深挖的深度和广度显著提升,追问更加聚焦于系统架构层面的取舍、面对高并发与高可用场景的设计思路、以及技术风险的预判与控制。算法题目的难度显著上探,可能出现动态规划、复杂图算法或需要综合运用多种数据结构的复合型题目。部分面试官会引入场景设计题或系统设计题的雏形,例如“设计一个短网址系统”或“设计一个实时排行榜”。二面面试官的核心评价维度是:技术视野是否开阔、架构能力是否具备、解决问题是否有方法论、在高难度挑战下能否保持逻辑清晰。第五阶段:技术三面/交叉面试(更高层级主管/其他团队负责人)此轮通常不限于技术细节的纠结,而是综合评估候选人的技术潜力、发展意愿、文化契合度和团队协作能力。项目深挖可能上升到业务价值层面,例如“这个项目对公司/产品的核心指标产生了什么影响”“如果重新做一次,你会如何改进”。算法考察的比重降低,取而代之的是更为开放的设计题、技术视野题(如“你对某个前沿技术趋势的看法”)和行为面试题(如“如何处理与产品经理的冲突”)。此轮面试官的核心评价维度是:技术价值观是否匹配、是否有持续学习和成长的动力、沟通与协作能力是否良好、未来能否成为团队的技术骨干或领导者。在部分企业,此轮之后可能还有HR面,主要涉及薪资期望、职业规划、入职时间等非技术层面的沟通,但技术评估在交叉面结束时已基本尘埃落定。本章小结:互联网大厂技术面试的本质是一场系统性、多维度、层层递进的技术能力体检。从一面到终面,评估焦点从基础编码能力逐步上移至架构设计能力、工程思维和技术领导力。每个阶段的面试官角色不同、考察目标不同,求职者必须在每轮面试前精准定位该轮次的核心评价维度,并相应地调整项目陈述的重点和算法准备的难度预期。可执行动作:针对目标公司的面试流程绘制一张流程图,并标注每轮面试的考察焦点和准备策略,将其贴在复习区域显眼位置。1.2技术面试的评价模型:面试官到底在看什么表面上看,技术面试是在考察“知不知道”和“会不会做”。但在一线面试官的认知框架中,对候选人的评估远不止于此。本节将拆解面试官心中的隐性评价模型,帮助你在每一个回答中精准命中评分点。面试官的核心评估框架可以归纳为“技术能力四象限模型”,四个象限分别对应不同的能力层级和考察方式。象限一:知识与记忆(Memory&Knowledge)此维度考察的是对计算机科学基础知识、编程语言特性、框架原理的记忆准确性和知识广度。典型问题如“请解释TCP三次握手的过程”“Java中HashMap的底层实现原理是什么”“React的虚拟DOM机制如何工作”。这是最基础的维度,但也是许多求职者失分的重灾区。失分原因往往不是“不知道”,而是“说不清楚”——要么逻辑混乱、丢三落四,要么停留在表面、无法深入原理层。高分策略:任何关键概念的回答,严格遵循“定义先行——核心机制拆解——关联知识延伸”的三段论结构。例如回答HashMap原理时,首先准确给出其数据结构本质(数组加链表/红黑树),然后拆解put操作的哈希计算、冲突解决、扩容触发等核心流程,最后自然地关联到线程安全问题(引出ConcurrentHashMap)或与TreeMap的对比,展示知识的体系化程度。象限二:编码与实现(Coding&Implementation)此维度考察的是将算法思路快速、准确地转化为高质量代码的能力。面试官关注的远不止“代码能否通过测试用例”,还包括:代码风格的规范性(命名、缩进、注释)、边界条件的处理周全性(空指针、数值溢出、非法输入)、编码过程中的思维外化程度(能否边写边清晰地解释当前步骤的意图)、以及对时间和空间复杂度的自觉意识。高分策略:养成“先沟通,后编码”的习惯。在正式落笔前,向面试官简要说明你选择的算法思路、数据结构、时间和空间复杂度的预期,以及你打算如何处理关键的边界情况。在编码过程中,像一位技术文档的作者一样书写,变量名自解释、逻辑分段用空行隔开、对复杂判断条件写上简短注释。完成代码后,主动用一两个测试用例进行心智运行,演示代码的正确性。象限三:问题解决与工程思维(ProblemSolving&EngineeringThinking)此维度是区分“能写代码的人”和“合格的工程师”的关键分水岭。当面对一个真实世界的模糊需求(如“设计一个秒杀系统”)时,候选人能否将其拆解为可控的子问题,能否识别出关键的技术矛盾和约束条件,能否在多个可行方案之间做出有理有据的权衡,能否预见到方案上线后可能面临的风险。高分策略:对于设计题或开放式问题,使用“需求澄清——约束定义——系统抽象——组件拆解——关键路径深挖——权衡折中——风险预估”的结构化思维框架来组织回答。永远不要直接跳入技术细节,而是先用30秒时间向面试官确认你的理解和范围边界。在阐述方案时,主动对比替代方案的优劣,并明确说出“在某某场景下我会选择A方案,原因是……而在另一个场景下B方案更合适,因为……”,充分展现你的技术判断力和工程成熟度。象限四:沟通协作与个人特质(Communication&Culture)技术能力再强,如果沟通中词不达意、拒绝回应追问、在压力下情绪失控,也很难通过面试。此维度考察的是候选人的逻辑表达能力、面对压力追问的反应、对技术热情的真伪、以及是否具备同理心和团队协作意识。高分策略:将面试视为一次技术交流,而非考试。主动与面试官建立连接——在回答项目经历时,观察面试官的表情和反馈,适时调整讲解的节奏和深度;在被连续追问时,保持冷静的微笑,将追问视为深入展示自己的机会而非刁难;在遇到确实不会的问题时,诚实地说明自己的知识边界,并表达明确的学习意愿和学习路径,这远比支支吾吾地编造答案更能赢得尊重。本章小结:在互联网大厂的技术面试中,面试官通过一场45至60分钟的对话,同时在这四个维度上对候选人进行动态评分。你的每一句回答,都可能在四个象限上同时产生加分或减分效应。一个算法题做出来但全程沉默不语的人,会在“编码与实现”得分却在“沟通”上失分;一个口若悬河但代码里漏洞百出的人,则恰好相反。顶尖的面试表现,是在四个象限上都呈现出高于平均线的水准,并在“问题解决与工程思维”上展现出令人印象深刻的亮点。可执行动作:回顾自己最近一次模拟面试或真实面试的录音(如有),在四象限模型中对每一句发言进行归类,识别出自己的优势象限和短板象限,有针对性地进行强化训练。1.3面试中的压力追问与应对心法压力追问(StressQuestioning)是大厂技术面试中令人闻之色变的环节,但它并非面试官的恶意刁难,而是一种标准化的评估技术,旨在探测候选人在极端情况下的知识边界、思维韧性和心理素质。本节将揭秘压力追问的常见套路与应对心法。压力追问的三种常见形式第一种:连续否定型。面试官对你的回答不断提出质疑——“你确定吗?”“真的不会有问题吗?”“再想想?”即使你的回答是正确或合理的,面试官也会持续制造怀疑气氛,观察你是否会动摇并轻易改变立场。第二种:深度钻探型。面试官沿着你项目中一个看似不起眼的细节一路向下深挖,从应用层到框架层、从框架层到操作系统层、从操作系统层到硬件层,直到你触及知识边界为止。这种追问的目的并非期望你全都能答出,而是观察你在未知面前的反应模式。第三种:场景突变型。在你完成算法编码后,面试官突然修改题目条件——“如果输入规模扩大1000倍呢”“如果内存只有原来的十分之一呢”“如果要求在线性时间内完成呢”,要求你在数分钟内调整方案。这种追问测试的是思维灵活性以及对不同解法的适用场景是否有深层理解。应对压力追问的五大心法心法一:稳住呼吸,承认焦虑是正常的生理反应。当被连续否定或追问到盲区时,心跳加速、手心出汗是所有人都会经历的反应。此时不要急于辩解或反驳,先做一个明显的停顿(2至3秒),深呼吸一次,这个动作不仅能让你的大脑获得宝贵的缓冲,也向面试官传递出“我能控制局面”的从容信号。心法二:区分“事实”与“解释”。当面试官说“你确定吗”时,不要立刻自我怀疑并推翻原有回答。正确的回应是:“我理解您的质疑,让我重新审视一下我的推导过程。首先,关于X这一点,我确定是正确的,理由是……其次,关于Y部分,可能存在我没有考虑周全的边界情况,比如……让我针对这个边界再分析一下……”这种回应既展现了自信,又展现了严谨的工程师思维。心法三:将未知点转化为已知框架。当深度钻探触及你的知识边界时,最常见的失败回答是直接沉默或者说“我不知道”。高情商的应对方式是:“这个问题涉及到底层的Z机制,坦白说我没有深入研读过源码级别的实现。但基于我对上层U机制和V机制的理解,我的推测是……为了验证这个推测,在面试结束后我会优先查阅官方文档的W章节。”这个回答同时完成了三件事:诚实承认边界、展示推断能力、表明学习意愿和学习方法。心法四:为场景变化建立应对模板。当面试官突然修改题目条件时,不要慌乱地推翻原有方案。按照固定的思维框架逐步响应:首先,快速分析新条件改变了问题的哪个核心约束(是时间还是空间、是单机还是分布式、是读多写少还是写多读少);然后,指出原有方案中哪些部分仍然有效、哪些部分需要调整;最后,有针对性地给出调整后的思路或替代方案。这个框架能保证你在突发场景下仍然保持逻辑层次。心法五:在无法给出确切答案时提供你的“思考路线图”。面试官想看到的不是你全知全能,而是你不知道时如何思考。当遇到一个完全陌生的技术问题时,可以使用如下话术模板:“这是一个我尚未涉足过的领域,但如果让我从零开始解决这个问题,我的思路会是——第一步,通过K文档或L社区确认问题的准确定义和现有解决方案的边界;第二步,尝试将其映射到我所熟悉的M领域进行类比推理;第三步,建立一个最小可行原型来验证核心假设。在这个过程中,我会特别关注N风险。”这样的回答展现了成熟工程师面对未知时的系统化探索路径,其价值远超一个死记硬背的标准答案。本章小结:压力追问不是面试的对立面,而是面试的深化。面试官用它来区隔那些仅准备过“标准答案”的候选人与那些真正具备深度思考能力和心理韧性的候选人。将压力追问视为一次难得的展示机会——展示你如何在信息不完整、环境有压力的情况下依然保持理性、逻辑与风度。可执行动作:与一位同伴进行压力追问模拟练习,让同伴在你回答每个问题后都连续追问三次“为什么”“还有呢”“再深入一层”,直到你触及知识边界,然后刻意练习上述五大心法中的承接话术。第二章算法攻坚:核心题型与高分模板2.1算法面试的沟通框架:STAR-M模型在正式进入真题详解之前,本节先交付一个在算法面试中屡试不爽的高分沟通模型——STAR-M模型。它为你在与面试官的口头交流中提供了一个清晰的叙事结构,确保你的思考过程被完整、专业地呈现。STAR-M模型包含五个阶段。阶段一:Situation澄清与确认(10%时间)拿到题目后,不要立刻开始写代码。用30至60秒时间完成以下动作:用自己的话向面试官重新描述题目需求,确保理解无误;主动确认输入数据的约束条件(如数组长度范围、元素取值范围、是否存在重复元素等);确认输出要求(如返回下标还是数值、多个答案时返回任意一个还是全部);通过一个简单的示例输入输出与面试官对齐期望。这个阶段的核心目标是消除任何隐性歧义,同时向面试官展示你严谨的工程师习惯。示范话术:“好的,让我确认一下对题目的理解。给定一个整数数组和一个目标值,需要找出数组中两个元素,使它们的和等于目标值,并返回这两个元素的下标。我的理解是否正确?另外想确认,数组是否可能包含负数?是否允许同一个元素被使用两次?”阶段二:Task思路阐述与复杂度预判(20%时间)在明确需求后,向面试官阐述你准备采用的解题思路。此时不需要代码级别的细节,而是从宏观层面描述你的算法策略。最关键的是,必须同时给出该思路的时间和空间复杂度预估,并简要说明为何选择这个方案(是在时间与空间之间的某种权衡,还是因为某个暴力解法不可行)。示范话术:“我准备用哈希表来解决这个问题。核心思路是:遍历数组时,对于当前元素X,计算目标值减去X的差值,然后检查这个差值是否已经在哈希表中。如果在,说明找到了答案;如果不在,就把当前元素的值和下标存入哈希表。这样只需要一次遍历,时间复杂度O(n),空间复杂度O(n)用于存储哈希表。虽然相比暴力双循环的O(n^2)多用了空间,但在n较大时时间优势明显。”阶段三:Action编码实现与思维外化(50%时间)这是落笔写代码的阶段。必须边写边讲,将你的编码思维实时外化。每定义一个变量时说明其用途,每写一段逻辑时简述其意图。代码风格保持高可读性:变量名使用自解释的全称、在关键判断或循环处添加简洁注释、处理边界情况(如空数组、长度不足等)时显式写出保护性判断。阶段四:Review自主测试与边界验证(15%时间)代码完成后,不要等着面试官提示。主动用一组测试用例进行心智运行:首先用题目给出的示例输入进行正向验证;然后构造一个能触发边界条件的用例(如空输入、最小值、最大值);最后再用一个更复杂的随机用例演示代码的鲁棒性。用笔或光标逐行追踪变量变化,确保每一步都符合预期。示范话术:“好的,代码已经写完了。让我来跑几个测试用例验证一下。首先用题目给的示例——数组[2,7,11,15],目标9。初始时map为空,遍历第一个元素2,差值7不在map中,存入{2:0};遍历第二个元素7,差值2在map中,返回[0,1],符合预期。接下来测试边界情况,空数组——直接返回空结果;只有一个元素的数组——同样返回空结果,因为无法组成两个元素的和。最后用一个包含重复元素的用例来验证——数组[3,3],目标6,第一个3存入map,第二个3发现差值3已在map中,正确返回[0,1]。”阶段五:Maturity复杂度分析与优化展望(5%时间)在所有功能验证完成后,做最后的技术升华。清晰陈述最终方案的时空复杂度,并用一两句话指出在什么场景下可能需要不同的方案,或者如果输入规模进一步扩大应该考虑什么替代设计。这展现了你的技术视野和对问题域的系统性理解。示范话术:“最终方案的时间复杂度是O(n),空间复杂度是O(n)。如果输入数组已经排好序,我们可以用双指针法将空间复杂度优化到O(1);如果数据规模极大、无法一次性加载到内存,可能要考虑基于外部排序和分块处理的设计,但这已经超出了当前题目的约束范围。”本章小结:STAR-M模型将一场算法面试从“交出正确答案”升级为“展示完整的工程思维过程”。面试官的评分离不开你在每个阶段的表现。哪怕最终代码有小瑕疵,只要在S和T阶段展示了清晰的分析、在R阶段主动发现了问题并提出修正,最终得分依然可能远高于一个沉默写完却忽略边界的人。可执行动作:在之后的每一次刷题练习中,都严格遵循STAR-M模型进行模拟,哪怕你是一个人在练习,也要大声地完成每一个阶段的表述,直到它内化为你的本能反应。2.2专题一:数组与字符串(10道核心真题完整解析)本节精选10道在字节跳动、腾讯、阿里巴巴面试中高频出现的数组与字符串算法题。每题按照“题目描述→多解思路对比→最优解完整实现→复杂度分析→举一反三”的结构展开,确保你完全掌握每种题型的解题范式。第1题:三数之和(3Sum)题目描述:给你一个包含n个整数的数组nums,判断nums中是否存在三个元素a、b、c,使得a+b+c=0。请找出所有和为0且不重复的三元组。注意,答案中不可以包含重复的三元组。示例输入:nums=[-1,0,1,2,-1,-4]
示例输出:[[-1,-1,2],[-1,0,1]]
约束条件:数组长度范围0<=nums.length<=3000,元素值范围-10^5<=nums[i]<=10^5。思路一:暴力三重循环最直接的思路是使用三层嵌套循环枚举所有可能的三元组组合,判断其和是否为零。时间复杂度为O(n^3),在n=3000时运算规模达到270亿次,在面试中显然无法通过。此解法仅有思路层面的讨论价值,不具备实用性。思路二:哈希表辅助(Two-Sum转化)将三数之和转化为两数之和问题。遍历数组,固定第一个数nums[i],然后在剩余部分寻找两数之和为-nums[i]的组合。两数之和可以用哈希表在线性时间内完成。但此方法的最大难点在于去重——三元组的重复判断需要在哈希表层面做额外的标记处理,代码复杂度显著升高,且在最坏情况下(所有元素相同)哈希表的冲突处理会带来性能退化。总体时间复杂度O(n^2),空间复杂度O(n)。思路三:排序加双指针(最优解,强烈推荐)先将数组排序,然后固定一个元素nums[i]作为三元组的第一个数,在i之后的区间使用左右双指针寻找另外两个数。排序使得去重变得极其简洁:跳过连续相同的第一个元素;在找到一个有效三元组后,同时跳过左右指针指向的重复元素。时间复杂度O(n^2),空间复杂度仅O(1)(不考虑排序的栈空间或结果存储空间)。这是面试中首选的标准答案。最优解完整实现(带逐行注释)//函数功能:寻找数组中所有和为0的不重复三元组
//输入:整数数组nums
//输出:所有满足条件的三元组列表
publicList<List<Integer>>threeSum(int[]nums){
//存储最终结果的容器
List<List<Integer>>result=newArrayList<>();
//边界处理:数组长度小于3,无法组成三元组,直接返回空结果
if(nums==null||nums.length<3){
returnresult;
}
//第一步:对数组进行升序排序,这是双指针法能够工作的前提
//排序后,相同的元素会相邻排列,为去重提供了极大便利
Arrays.sort(nums);
//获取数组长度,用于后续循环的边界判断
intn=nums.length;
//第二步:遍历数组,固定三元组的第一个元素
//注意循环终止条件为n-2,因为至少需要为另外两个元素留出位置
for(inti=0;i<n-2;i++){
//剪枝优化:如果当前元素已经大于0,由于数组已排序,
//其后的所有元素也都大于0,三数之和不可能等于0,直接结束循环
if(nums[i]>0){
break;
}
//去重步骤一:跳过与上一个元素相同的第一个元素
//这里必须使用i>0的条件,防止数组越界
//此判断的精妙之处在于:它允许了元组内部出现重复数字(如[-1,-1,2]),
//但禁止了相同三元组的重复出现
if(i>0&&nums[i]==nums[i-1]){
continue;
}
//初始化左右双指针
//左指针从当前元素的下一个位置开始
intleft=i+1;
//右指针从数组末尾开始
intright=n-1;
//第三步:在左指针小于右指针的条件下,不断收缩查找区间
while(left<right){
//计算三数之和
intsum=nums[i]+nums[left]+nums[right];
//情况A:三数之和恰好为0,找到一个有效三元组
if(sum==0){
//将当前三元组加入结果集
result.add(Arrays.asList(nums[i],nums[left],nums[right]));
//去重步骤二:跳过左指针右侧与当前left值相同的所有元素
//循环条件必须同时检查left<right,防止指针越界
while(left<right&&nums[left]==nums[left+1]){
left++;
}
//去重步骤三:跳过右指针左侧与当前right值相同的所有元素
while(left<right&&nums[right]==nums[right-1]){
right--;
}
//两侧指针同时向中间移动,寻找下一组可能的解
left++;
right--;
}
//情况B:三数之和小于0,说明需要增大总和,
//由于数组有序,将左指针右移以取更大的值
elseif(sum<0){
left++;
}
//情况C:三数之和大于0,说明需要减小总和,
//将右指针左移以取更小的值
else{
right--;
}
}
}
//返回最终的不重复三元组结果集
returnresult;
}时间复杂度严格推导:排序步骤的时间复杂度为O(nlogn)。外部for循环执行O(n)次,内部的while循环在每次for迭代中最多执行O(n)次操作(左右指针各移动n次),因此双指针部分整体为O(n^2)。总时间复杂度为O(n^2+nlogn),在大O意义上即为O(n^2)。空间复杂度:若不计结果列表的存储,算法额外使用的空间仅为几个指针变量,为O(1)。若计入排序所需空间(取决于具体排序算法实现),在最坏情况下为O(n)。举一反三:本题的核心技巧——排序加双指针消除重复三元组——可以推广到所有“在有序数组中寻找满足特定条件的两个或多个元素”的问题。例如:最接近的三数之和(三数之和最接近目标值)、四数之和(固定前两个元素后降级为三数之和)、有效三角形的个数(满足两边之和大于第三边的三元组计数)。关键心法是:排序不仅是为了让双指针能够工作,更是为了让去重变得像比较相邻元素一样简单。第2题:最长无重复字符子串(LongestSubstringWithoutRepeatingCharacters)题目描述:给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度。示例输入:s="abcabcbb"
示例输出:3(解释:因为无重复字符的最长子串是"abc",所以其长度为3)
示例输入:s="bbbbb"
示例输出:1
约束条件:字符串长度范围0<=s.length<=5*10^4,字符集为英文字母、数字、符号和空格。思路一:暴力枚举枚举所有可能的子串起点和终点,对于每个子串检查是否包含重复字符。子串数量为O(n^2),每个子串的查重需要O(n)时间,总时间复杂度O(n^3)。在n达到5万时完全不可行。思路二:滑动窗口加哈希集合(清晰直观)维护一个可变大小的窗口[left,right],窗口内始终保持无重复字符。当尝试将right指针右移时,如果新字符不在窗口内,则扩展窗口并更新最大长度;如果新字符已在窗口内,则收缩left指针,将重复字符移出窗口。使用哈希集合存储窗口内的字符,判断和移除均为O(1)操作。总体时间复杂度O(n),因为left和right指针各自最多移动n次。空间复杂度O(k),k为字符集大小,在最坏情况下不超过min(n,字符集大小)。思路三:滑动窗口加哈希映射(一步跳跃优化)在思路二的基础上,哈希集合中的left指针需要一步一步右移直到重复字符被移出。如果改用哈希映射存储每个字符最近一次出现的下标,当发现重复字符时,left可以直接跳跃到max(当前left,重复字符上次出现位置+1),避免了一个一个步进的冗余操作。时间复杂度仍为O(n),但在重复字符密集的场景下实际运行速度更快。最优解实现(哈希映射优化版,带注释)publicintlengthOfLongestSubstring(Strings){
//边界处理:空字符串直接返回0
if(s==null||s.length()==0){
return0;
}
intn=s.length();
//用于记录全局最大无重复子串长度
intmaxLength=0;
//哈希映射:键为字符,值为该字符最近一次出现在字符串中的下标
Map<Character,Integer>charIndexMap=newHashMap<>();
//left指针代表当前无重复窗口的左边界
//right指针代表当前窗口的右边界,通过for循环逐步扩展
for(intright=0,left=0;right<n;right++){
charcurrentChar=s.charAt(right);
//检查当前字符是否已在窗口内出现过
if(charIndexMap.containsKey(currentChar)){
//关键步骤:将左边界跳跃到重复字符上一次出现位置的下一个位置
//使用Math.max确保left不会向左回退(处理重复字符在left之前的情况)
left=Math.max(left,charIndexMap.get(currentChar)+1);
}
//无论是否重复,更新当前字符的最新位置
charIndexMap.put(currentChar,right);
//计算当前窗口的长度并尝试更新最大值
//窗口长度公式:right-left+1
maxLength=Math.max(maxLength,right-left+1);
}
returnmaxLength;
}时间复杂度严格推导:整个算法仅对字符串进行一次遍历,right指针移动n次。在每次迭代中,哈希映射的查询和插入操作均为均摊O(1)。left指针的更新通过直接赋值完成而非循环递增,故总时间复杂度严格为O(n)。空间复杂度:哈希映射中最多存储字符集大小的键值对,而字符集最多为所有可能的Unicode字符,但在实际面试场景中通常限定为ASCII或扩展ASCII,故视为O(1)或O(min(n,128/256))。举一反三:滑动窗口是字符串与数组问题中最高频的优化技巧之一。任何涉及“连续子数组/子串满足某种条件”的问题,都应首先考虑滑动窗口模型。经典的同类型题目包括:至多包含两个不同字符的最长子串、替换后的最长重复字符(允许替换K次字符)、长度最小的子数组(和大于等于目标值的最短连续子数组)。核心的滑动窗口模板为:右指针负责扩展窗口以满足条件,左指针负责在条件被破坏后收缩窗口以恢复条件,答案在窗口滑动的动态过程中进行记录。第3题:接雨水(TrappingRainWater)题目描述:给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。示例输入:height=[0,1,0,2,1,0,1,3,2,1,2,1]
示例输出:6
约束条件:n==height.length,1<=n<=2*10^4,0<=height[i]<=10^5。思路一:按列暴力求解对于每一列i,分别向左和向右扫描找到最高的柱子leftMax和rightMax,当前列的储水量为min(leftMax,rightMax)-height[i](如果差值为正)。时间复杂度O(n^2),空间复杂度O(1)。在数据量达到2万时会超时。思路二:动态规划预处理最大值(经典优化)思路一的瓶颈在于每一列都要重新扫描左右两侧的最高值。我们可以通过两次遍历预先计算出每个位置i的左侧最高值和右侧最高值,存入两个数组。第一遍从左向右扫描,leftMax[i]=max(leftMax[i-1],height[i]);第二遍从右向左扫描,rightMax[i]=max(rightMax[i+1],height[i])。之后只需一次遍历即可计算出总储水量。时间复杂度O(n),空间复杂度O(n)。思路三:双指针(最优解,空间O(1))思路二虽然时间复杂度已经最优,但使用了额外的O(n)空间。双指针法将空间复杂度优化至O(1),同时保持O(n)的时间复杂度。核心洞察:储水量由两侧的较小值决定。我们维护左右指针和一个动态更新的leftMax与rightMax。如果当前leftMax小于rightMax,则可以确定左指针处的水量(因为右端已经有更高的柱子兜底),处理完左指针后将其右移;反之处理右指针。这样只需要一次遍历和几个变量。最优解实现(双指针,带注释)publicinttrap(int[]height){
//边界处理:柱子少于3根无法形成凹槽储水
if(height==null||height.length<3){
return0;
}
intn=height.length;
//左指针,初始指向数组最左端
intleft=0;
//右指针,初始指向数组最右端
intright=n-1;
//记录左指针遍历过的最高柱子高度
intleftMax=0;
//记录右指针遍历过的最高柱子高度
intrightMax=0;
//累积总储水量
inttotalWater=0;
//双指针在相遇之前持续移动
while(left<right){
//更新左右各自遇到的最高柱子
leftMax=Math.max(leftMax,height[left]);
rightMax=Math.max(rightMax,height[right]);
//核心逻辑:比较leftMax和rightMax的大小
//如果leftMax较小,说明左指针处的储水量完全由leftMax决定
//因为右侧有更高的柱子(至少是rightMax)在兜底
if(leftMax<rightMax){
//当前左指针位置的储水量=leftMax-当前高度
//因为leftMax已经取过max,所以这个差值必定非负
totalWater+=leftMax-height[left];
//左指针右移,处理下一列
left++;
}
//反之,右指针处的储水量由rightMax决定
else{
totalWater+=rightMax-height[right];
//右指针左移
right--;
}
}
returntotalWater;
}时间复杂度严格推导:while循环在每次迭代中要么左指针右移、要么右指针左移,两个指针总共移动n次,故时间复杂度为O(n)。空间复杂度:仅使用了有限的几个变量,为O(1)。举一反三:接雨水问题本质上是在二维平面上求“被围合区域的面积”。其核心思维——用双指针配合双侧极值动态决策——可以推广到以下问题:盛最多水的容器(双指针同时向中间收缩)、柱状图中最大的矩形(单调栈经典应用)、二维接雨水(带边界的最短路径问题)。特别值得留意的是,接雨水问题使用双指针而柱状图最大矩形使用单调栈,原因在于前者关心两侧的“短板”而后者关心以每根柱子为高度的最大延伸区间。理解数据结构选择的“为什么”,比记住结论重要得多。第4题:最小覆盖子串(MinimumWindowSubstring)题目描述:给你一个字符串s和一个字符串t,返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串""。注意,t中重复字符的出现次数也必须被覆盖。示例输入:s="ADOBECODEBANC",t="ABC"
示例输出:"BANC"
约束条件:m==s.length,n==t.length,1<=m,n<=10^5,s和t均由英文字母组成。思路一:暴力枚举加验证枚举s的所有子串,对每个子串验证是否包含t的全部字符。子串数量O(m^2),每个验证需要O(m)统计字符频率,总时间复杂度O(m^3)。在数据规模达10万时完全无法使用。思路二:滑动窗口加计数数组(标准模板)使用两个计数数组(或哈希映射),一个记录t中每个字符的需求量,另一个记录当前窗口中各字符的出现次数。右指针不断扩展窗口,当窗口满足条件时(窗口内包含了t的所有字符),记录当前可行解,然后尝试收缩左指针以寻找更短的满足条件的窗口。关键在于如何高效判断窗口是否满足条件:维护一个“已满足的字符种类数”变量matched,当某个字符的窗口计数从“不足需求”变为“恰好满足需求”时,matched加一;当matched等于t中不同字符的数量时,窗口满足条件。时间复杂度O(m+n),空间复杂度O(字符集大小),即O(1)。最优解实现(滑动窗口加计数优化,带注释)publicStringminWindow(Strings,Stringt){
//边界处理
if(s==null||t==null||s.length()<t.length()){
return"";
}
intm=s.length();
intn=t.length();
//数组索引对应ASCII字符,记录t中每个字符的需求数量
//数组大小128覆盖标准ASCII字符集
int[]need=newint[128];
for(inti=0;i<n;i++){
need[t.charAt(i)]++;
}
//窗口中各字符的当前计数
int[]window=newint[128];
//已经满足需求的不同字符种类数
intmatched=0;
//t中不同字符的种类总数
intdistinctChars=0;
for(intcount:need){
if(count>0){
distinctChars++;
}
}
//记录最小窗口的起始位置和长度
//minStart初始为-1表示尚未找到任何可行解
intminStart=-1;
intminLen=Integer.MAX_VALUE;
//滑动窗口的左边界
intleft=0;
//右边界不断向右扩展
for(intright=0;right<m;right++){
charrc=s.charAt(right);
//仅当当前字符是t中需要的字符时,才更新窗口计数和matched状态
if(need[rc]>0){
window[rc]++;
//当窗口内某字符的计数恰好达到需求时,
//说明该类字符已满足需求,matched增一
if(window[rc]==need[rc]){
matched++;
}
}
//当窗口已经满足t的所有字符需求时,
//尝试收缩左边界以获得更小的窗口
while(matched==distinctChars){
//更新最小窗口记录
intcurrentLen=right-left+1;
if(currentLen<minLen){
minLen=currentLen;
minStart=left;
}
//准备移除左边界字符
charlc=s.charAt(left);
//仅当左边界字符是需求字符时,更新窗口计数和matched状态
if(need[lc]>0){
//如果移除该字符后,窗口内该字符数少于需求数,
//则该字符类别不再满足,matched减一
if(window[lc]==need[lc]){
matched--;
}
window[lc]--;
}
//左边界右移,收缩窗口
left++;
}
}
//如果minStart仍为-1,说明从未找到可行窗口,返回空字符串
returnminStart==-1?"":s.substring(minStart,minStart+minLen);
}时间复杂度严格推导:右指针遍历字符串s一次,左指针同样最多遍历s一次(因为只在while循环中右移,不会回退),因此总时间复杂度为O(m)。need和window数组的大小固定为128,空间复杂度为O(1)。举一反三:最小覆盖子串是滑动窗口类问题的“母题”之一,其变体在面试中频繁出现。掌握本题的窗口收缩模板后,以下题目均可轻松应对:字符串的排列(判断s中是否包含t的一个排列)、找到字符串中所有字母异位词、串联所有单词的子串(将单词视为基本单元)。核心模板动作:右扩满足条件→记录可行解→左缩破坏条件→右扩重新满足条件,循环往复。第5题:螺旋矩阵(SpiralMatrix)题目描述:给你一个m行n列的矩阵matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。示例输入:matrix=[[1,2,3],[4,5,6],[7,8,9]]
示例输出:[1,2,3,6,9,8,7,4,5]
约束条件:m==matrix.length,n==matrix[i].length,1<=m,n<=10。思路一:模拟遍历(直观但边界处理繁琐)模拟螺旋的行走轨迹,按顺序访问每个元素,并将已访问的元素做标记(修改原数组或使用visited布尔数组)。使用方向变量控制当前移动方向(右、下、左、上),当遇到矩阵边界或已访问元素时切换方向。时间复杂度O(mn),空间复杂度O(1)(如果直接修改原数组)或O(mn)(如果使用visited数组)。思路二:分层法(边界清晰,推荐)将矩阵看作一层一层的“壳”,从外向内逐层剥开。每一层由四条边组成:上层(左→右)、右层(上→下)、下层(右→左)、左层(下→上)。定义四个边界变量top、bottom、left、right,每遍历完一层就将对应边界向内收缩。这种方法不需要额外的visited数组,代码的边界逻辑也更为清晰。最优解实现(分层法,带注释)publicList<Integer>spiralOrder(int[][]matrix){
List<Integer>result=newArrayList<>();
//边界处理:空矩阵直接返回
if(matrix==null||matrix.length==0){
returnresult;
}
//定义矩阵的四个边界(均为索引,闭区间)
inttop=0;
intbottom=matrix.length-1;
intleft=0;
intright=matrix[0].length-1;
//预计结果的总元素数量
inttotalElements=matrix.length*matrix[0].length;
//当结果集中的元素数量尚未达到预期总数时,持续循环
//循环的每一轮处理一层(四条边)
while(result.size()<totalElements){
//步骤一:从左向右遍历上层边界
//遍历范围:left到right(均为包含)
for(intcol=left;col<=right&&result.size()<totalElements;col++){
result.add(matrix[top][col]);
}
//上层遍历完毕,上边界向下收缩一行
top++;
//步骤二:从上向下遍历右侧边界
for(introw=top;row<=bottom&&result.size()<totalElements;row++){
result.add(matrix[row][right]);
}
//右侧遍历完毕,右边界向左收缩一列
right--;
//步骤三:从右向左遍历下层边界
//注意:当top已超过bottom时(即只剩一行已被步骤一处理),跳过此步骤
for(intcol=right;col>=left&&result.size()<totalElements;col--){
result.add(matrix[bottom][col]);
}
//下层遍历完毕,下边界向上收缩一行
bottom--;
//步骤四:从下向上遍历左侧边界
//注意:当left已超过right时(即只剩一列已被步骤二处理),跳过此步骤
for(introw=bottom;row>=top&&result.size()<totalElements;row--){
result.add(matrix[row][left]);
}
//左侧遍历完毕,左边界向右收缩一列
left++;
}
returnresult;
}时间复杂度严格推导:每个矩阵元素被访问且仅被访问一次,故时间复杂度为O(m*n),这是任何遍历算法能达到的最优时间复杂度。空间复杂度:除结果列表外,仅使用固定数量的变量,为O(1)。举一反三:分层遍历的思想可以推广到所有与二维矩阵边界处理相关的问题:螺旋矩阵II(按螺旋顺序填充数字)、旋转图像(原地旋转90度,也是逐层处理)、对角线遍历(类似但方向不同)。掌握“定义边界、逐层收缩”的模式后,这类题目将变得有章可循。第6题:合并区间(MergeIntervals)题目描述:以数组intervals表示若干个区间的集合,其中单个区间为intervals[i]=[start_i,end_i]。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。示例输入:intervals=[[1,3],[2,6],[8,10],[15,18]]
示例输出:[[1,6],[8,10],[15,18]](解释:区间[1,3]和[2,6]重叠,将它们合并为[1,6])
约束条件:1<=intervals.length<=10^4,intervals[i].length==2,0<=start_i<=end_i<=10^4。思路一:暴力检查两两合并检查所有区间对,将重叠的合并,重复此过程直到不再有可合并的区间对。时间复杂度O(n^2)到O(n^3),在n=10^4时无法接受。思路二:排序后线性扫描(标准最优解)核心洞察:如果区间数组按照起始点升序排列,那么任何两个可以合并的区间在排序后的数组中是相邻的。证明:如果区间A与区间C可以合并,且排序后存在区间B位于A和C之间,由排序性质可知A.start<=B.start<=C.start。若A与C可合并,则A.end>=C.start,进而A.end>=B.start,因此A与B也可合并。所以我们可以将所有区间排好序,然后一次线性扫描完成合并——只需要检查相邻两个区间是否有重叠即可。时间复杂度O(nlogn)来自排序,扫描本身O(n);空间复杂度O(n)用于存储结果。最优解实现(带注释)publicint[][]merge(int[][]intervals){
//边界处理:空数组或单元素数组直接返回
if(intervals==null||intervals.length<=1){
returnintervals;
}
//第一步:将区间数组按照起始值升序排列
//使用Arrays.sort配合自定义比较器(或lambda表达式)
//排序是此算法的核心前提,使得所有可合并区间必然相邻
Arrays.sort(intervals,(a,b)->Ipare(a[0],b[0]));
//使用ArrayList存储合并结果(因为最终数组大小不确定)
List<int[]>merged=newArrayList<>();
//将第一个区间作为当前正在合并的区间
int[]currentInterval=intervals[0];
merged.add(currentInterval);
//第二步:遍历剩余的区间,与当前合并区间进行比较
for(inti=1;i<intervals.length;i++){
//当前区间的结束值
intcurrentEnd=currentInterval[1];
//下一个区间的起始值
intnextStart=intervals[i][0];
//下一个区间的结束值
intnextEnd=intervals[i][1];
//判断两个区间是否重叠:
//如果当前区间的结束值大于等于下一个区间的起始值,说明有重叠
if(currentEnd>=nextStart){
//发生重叠,需要合并
//合并后的结束值取两者结束值的较大者
currentInterval[1]=Math.max(currentEnd,nextEnd);
}else{
//没有重叠,将下一个区间设为新的当前区间并加入结果集
currentInterval=intervals[i];
merged.add(currentInterval);
}
}
//将List转换为二维数组返回
returnmerged.toArray(newint[merged.size()][]);
}时间复杂度严格推导:排序步骤O(nlogn),线性扫描O(n),总时间复杂度O(nlogn)。空间复杂度:结果列表使用O(n)空间,排序可能需要O(logn)到O(n)的额外空间(取决于排序算法的实现)。举一反三:区间合并问题是处理区间重叠的“原子操作”,在众多场景中有应用:会议室预定冲突检测(是否能参加所有会议)、插入新区间(在已排序的不重叠区间列表中插入新区间并合并)、删除被覆盖区间(移除被其他区间完全覆盖的区间)、区间列表的交集。这类问题的核心步骤永远都是:先排序,再线性处理相邻关系。第7题:下一个排列(NextPermutation)题目描述:整数数组的一个排列就是将其所有成员以序列或线性顺序排列。整数数组的下一个排列是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典序从小到大排列在一个容器中,那么数组的下一个排列就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列(即数组已是最大字典序排列),则必须将其重新排列为字典序最小的排列(即升序排列)。必须原地修改,只允许使用额外常数空间。示例输入:nums=[1,2,3]
示例输出:[1,3,2]
示例输入:nums=[3,2,1]
示例输出:[1,2,3]
约束条件:1<=nums.length<=100,0<=nums[i]<=100。思路一:生成所有排列后查找枚举数组的全排列,排序后找到当前排列的下一个。全排列数量为n!,在n=100时完全不可行。思路二:标准算法(两遍扫描,最优解)字典序下一个排列的生成有标准的数学算法,可以归结为以下步骤:
第一步:从右向左找到第一个顺序对(i,i+1),满足nums[i]<nums[i+1]。此时从i+1到末尾的子数组必然是降序排列的。
第二步:如果找不到这样的i,说明整个数组是降序的(最大值排列),直接反转整个数组得到最小值排列即可。
第三步:如果找到了i,从右向左找到第一个大于nums[i]的元素nums[j](因为i右侧是降序的,所以这个j一定存在)。
第四步:交换nums[i]和nums[j]。
第五步:将i+1到末尾的子数组反转(由于原来是降序,反转后变为升序,即该部分的最小排列)。
时间复杂度O(n),空间复杂度O(1)。最优解实现(带注释)publicvoidnextPermutation(int[]nums){
//边界处理
if(nums==null||nums.length<=1){
return;
}
intn=nums.length;
//第一步:从右向左找到第一个顺序对
//i从倒数第二个元素开始向左扫描
inti=n-2;
while(i>=0&&nums[i]>=nums[i+1]){
i--;
}
//第二步:判断是否找到了这样的i
if(i>=0){
//找到了i,说明存在更大的字典序排列
//第三步:从右向左找到第一个大于nums[i]的元素
//由于i右侧是降序排列,大于nums[i]的最小元素就是从右向左第一个大于nums[i]的元素
intj=n-1;
while(j>=0&&nums[j]<=nums[i]){
j--;
}
//第四步:交换nums[i]和nums[j]
swap(nums,i,j);
}
//如果i<0,说明整个数组是降序排列的
//此时跳过交换步骤,直接反转整个数组即可得到最小排列
//第五步:反转i+1到末尾的子数组
//如果是降序数组,i为-1,reverse(0)会反转整个数组,同样符合要求
reverse(nums,i+1,n-1);
}
//辅助方法:交换数组中的两个元素
privatevoidswap(int[]nums,inta,intb){
inttemp=nums[a];
nums[a]=nums[b];
nums[b]=temp;
}
//辅助方法:反转数组中从start到end(含两端)的子数组
privatevoidreverse(int[]nums,intstart,intend){
while(start<end){
swap(nums,start,end);
start++;
end--;
}
}时间复杂度严格推导:寻找i需要O(n),寻找j需要O(n),反转子数组需要O(n),总时间复杂度O(n)。空间复杂度:仅使用几个临时变量,O(1)。举一反三:下一个排列算法的核心在于对字典序本质的理解:要找到“紧接在当前排列之后的更大的排列”,等价于找到一个“尽可能靠右的交换”,且交换后要让右半部分处于最小状态。这个思维可以推广到:上一个排列(对称操作)、第k个排列(阶乘数制与康托展开)、全排列去重生成(结合排序与剪枝)。第8题:寻找旋转排序数组中的最小值(FindMinimuminRotatedSortedArray)题目描述:已知一个长度为n的数组,预先按照升序排列,经由1到n次旋转后得到输入数组。例如原数组nums=[0,1,2,4,5,6,7]在变化后可能得到:旋转4次得到[4,5,6,7,0,1,2],旋转7次得到[0,1,2,4,5,6,7]。给你一个元素值互不相同的数组nums,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的最小元素。必须设计时间复杂度为O(logn)的算法。示例输入:nums=[3,4,5,1,2]
示例输出:1
约束条件:n==nums.length,1<=n<=5000,-5000<=nums[i]<=5000,nums中所有整数互不相同。思路一:线性扫描遍历数组找到第一个比前一个元素小的元素。时间复杂度O(n),不满足题目要求的O(logn)。思路二:二分查找(最优解)旋转排序数组虽然整体不是有序的,但它由两个各自有序的子数组拼接而成。最小元素恰好是这两个子数组的分界点。核心观察:将中间元素nums[mid]与数组最右元素nums[right]比较。如果nums[mid]<nums[right],说明mid到right这段是有序且递增的,最小值一定在左半部分(包括mid本身),所以right=mid;如果nums[mid]>nums[right],说明最小值一定在右半部分(因为左半部分的元素都比right大),所以left=mid+1。当left与right重合时,该位置即是最小值。时间复杂度O(logn),空间复杂度O(1)。最优解实现(带注释)publicintfindMin(int[]nums){
//边界处理
if(nums==null||nums.length==0){
thrownewIllegalArgumentException("数组不能为空");
}
intleft=0;
intright=nums.length-1;
//当left和right重合时,找到最小值
while(left<right){
//计算中间位置,使用无符号右
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 珍惜校园时光,共创美好未来,四年级主题班会课件
- 强电电工证考试题及答案
- 汽车基础考试试题及答案
- 2026北美咨询面试题及答案
- 2026比尔盖茨面试题及答案
- 2026笔画结构化面试题及答案
- 2026边境管理员面试题及答案
- 2026编辑记者岗位面试题目及答案
- 2026编外的面试题目及答案
- 2026兵团监狱面试题目及答案
- 安全应急处置措施清单
- T/SHPTA 047-2023塑料电器用改性丙烯腈-丁二烯-苯乙烯共聚物(ABS)及其合金专用料
- 《低温等离子体技术简介》课件
- 《作业风险管控》课件
- 四川省康定市大槽门金矿资源储量核实报告
- 《泵与风机》课件-第八章 泵与风机的运行
- 中华民族共同体概论课件专家版10第十讲 中外会通与中华民族巩固壮大(明朝时期)
- 北师大版四年级下册数学计算题200道及答案
- 活性污泥法操作控制要点
- 消毒供应中心考试试题
- GB/T 4437.1-2023铝及铝合金热挤压管第1部分:无缝圆管
评论
0/150
提交评论