news 2026/10/15 1:25:31

二分查找的深层本质:在无序数组中找峰值的三种思想解法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
二分查找的深层本质:在无序数组中找峰值的三种思想解法

1. 项目概述:为什么一道“找峰值”的题值得花一整篇来深挖?

LeetCode 162题“寻找峰值”,表面看只是个中等难度的数组查找题,输入一个无序整数数组,要求返回任意一个峰值的下标——所谓峰值,就是比左右邻居都大的元素(边界元素只需比唯一邻居大即可)。初看毫无压力:遍历一遍,O(n)时间搞定,代码三行结束。但题目明确提示“你能设计一个时间复杂度为 O(log n) 的算法吗?”,这就把问题性质彻底改变了——它不再是一道考察循环逻辑的入门题,而是一道检验你对二分查找本质理解深度的试金石。

我带过不少刚刷题的同学,一看到“二分”就条件反射写left,right,mid,然后机械套用if nums[mid] > nums[mid+1]就往左缩,否则往右缩。结果跑通了,但问“为什么这样缩区间就一定不会漏掉峰值?”,十有八九答不上来。这恰恰暴露了常见误区:把二分当成一种“必须用于有序数组”的固定模板,而不是一种基于单调性/趋势性进行区间收缩的决策范式。本题的妙处正在于此——数组整体无序,但只要你站在任意一个位置,就能根据它和邻居的大小关系,推断出某一侧必然存在峰值,从而安全地舍弃另一侧。这种“局部比较→全局保证”的推理链条,才是二分思想的精髓所在。

这篇文章不讲标准答案,而是带你拆解三种截然不同的二分解法:最简明的“趋势驱动法”、最贴近教科书的“双邻比较法”,以及最容易被忽略但极具启发性的“边界扩展法”。每一种解法背后,都对应着对“峰值存在性”这一数学事实的不同证明路径。我会用生活化类比解释每一步的逻辑(比如把数组想象成连绵起伏的山脊线,你在某处抬头看左右山坡的倾斜方向,就能判断哪边山峰更高),手把手推演关键步骤的取舍依据(为什么mid和mid+1比较就能决定方向?为什么边界处理要单独考虑?),并附上真实调试过程中踩过的坑(比如越界访问、死循环的临界点、mid计算方式差异带来的影响)。无论你是刚接触二分的新手,还是想突破思维定式的进阶者,这篇解析都能帮你把“会做”升级为“真正懂”。

2. 核心思路拆解:二分为何能在无序数组中奏效?

2.1 峰值必然存在的数学保证:从“山脊线”到“极值存在定理”

很多同学卡在第一步:凭什么我能笃定地舍弃一半区间?难道不会刚好把唯一的峰值扔掉吗?这个问题的答案,藏在题目隐含的数学前提里。我们先抛开代码,用一张图建立直觉:

想象一条从左到右延伸的山脊线,每个数组元素是某个点的高度。题目给的数组长度至少为1(nums.length >= 1),且所有元素都是整数(意味着高度变化是离散的、可比较的)。现在,我们关注两个边界点:

  • 左边界nums[0]:它只有右邻居nums[1]。如果nums[0] > nums[1],那它本身就是峰值,直接返回0;否则,nums[0] < nums[1],说明山脊线在起点是向上走的。
  • 右边界nums[n-1]:它只有左邻居nums[n-2]。如果nums[n-1] > nums[n-2],那它就是峰值;否则,nums[n-1] < nums[n-2],说明山脊线在终点是向下走的。

关键来了:如果两个边界都不是峰值,那就意味着山脊线在左端向上走,在右端向下走。一条连续(此处是离散但连贯)的曲线,从上升开始,以下降结束,中间必然存在一个最高点——这就是峰值。这个结论,本质上是离散版本的极值存在定理:在一个有限序列中,如果首尾趋势相反(一升一降),则内部必有局部极大值。

这个保证,是所有二分解法的基石。它告诉我们:只要当前搜索区间[left, right]满足“左端点小于其右邻”且“右端点小于其左邻”(即区间两端都是“谷底趋势”),那么该区间内必然存在峰值。而我们的二分操作,就是不断维护这样一个“安全区间”,确保每次缩小区间后,新的子区间依然满足这个存在性条件。

2.2 三种解法的底层逻辑对比:你是在“追踪上升趋势”,还是在“验证邻域关系”?

基于上述存在性保证,我们可以构建不同的收缩策略。它们不是“谁对谁错”,而是观察问题的不同切面:

  • 趋势驱动法(推荐新手掌握):核心思想是“跟着上升的方向走”。站在mid位置,只和mid+1比较。如果nums[mid] < nums[mid+1],说明右侧有上升趋势,峰值一定在右半边(因为右端点要么是峰值,要么继续上升再下降,中间必有峰);反之,左侧有上升趋势,峰值在左半边。这种方法最简洁,逻辑链最短,只需要一次比较。

  • 双邻比较法(教科书式严谨):核心思想是“确认当前位置是否为峰,或确定峰在哪一侧”。站在mid,同时检查nums[mid-1]和nums[mid+1](需处理边界)。如果nums[mid]比两边都大,直接返回;否则,哪边更大,就往哪边走(因为更大的邻居更可能通向更高的峰)。这种方法更“稳妥”,每一步都在做明确的状态判断,但代码稍长,边界处理更繁琐。

  • 边界扩展法(高阶思维训练):核心思想是“主动构造一个‘安全’的初始区间”。不直接在原数组上二分,而是先人为添加两个“虚拟边界”:在数组最左加一个负无穷(保证nums[-1] = -∞),最右也加一个负无穷(nums[n] = -∞)。这样,整个扩展后的数组,首尾都是“绝对谷底”,完美满足我们前面说的“两端下降趋势”条件。然后,我们就可以放心地在[0, n-1]上进行标准二分,每次比较nums[mid]和nums[mid+1],逻辑与趋势驱动法一致,但理论基础更坚实,无需额外讨论边界情况。这种方法牺牲了一点空间(概念上),却换来了逻辑的极致清晰。

选择哪种方法,取决于你的目标:想快速AC?趋势驱动法最快。想透彻理解每一步的数学依据?双邻比较法最直观。想锻炼抽象建模能力,为解决更复杂的“存在性搜索”问题打基础?边界扩展法是必经之路。接下来,我会逐一展开,用真实代码和调试过程告诉你,每一步的“为什么”。

3. 核心细节解析与实操要点:避开那些让人心态爆炸的坑

3.1 趋势驱动法:一行比较定乾坤,但mid的计算方式暗藏玄机

这是最精炼的解法,代码可以压缩到7行以内。它的核心就一句话:如果nums[mid] < nums[mid+1],峰值在右半边;否则在左半边(含mid)。

def findPeakElement(nums): left, right = 0, len(nums) - 1 while left < right: mid = (left + right) // 2 if nums[mid] < nums[mid + 1]: left = mid + 1 else: right = mid return left

看起来简单,但实操中三个细节极易出错:

提示:mid的计算必须用(left + right) // 2,不能用(left + right) >> 1吗?其实可以,位运算在这里语义完全等价,但//更易读,且能避免某些语言中整数溢出的问题(虽然Python没有,但养成习惯很重要)。

第一,为什么比较mid和mid+1,而不是mid-1?
这是为了规避左边界越界。mid的最小值是0(当left=0, right=0时循环已退出,所以实际mid最小是0,但mid+1在right至少为1时才进入循环),而mid+1的最大值是right(因为mid = (left+right)//2 <= right,所以mid+1 <= right+1,而right初始为n-1,故mid+1最大为n,但我们的循环条件是left < right,所以right永远不会等于n,mid+1最大为n-1,即合法索引)。反观mid-1,当mid=0时就会越界。所以,选择与右邻比较,是工程上最省心的方案。

第二,为什么left = mid + 1,而right = mid?
这是二分缩区间的灵魂。当nums[mid] < nums[mid+1]时,说明mid位置不可能是峰值(因为它比右边小),所以峰值一定在[mid+1, right]中,left安全地跳到mid+1。而当nums[mid] >= nums[mid+1]时,mid本身有可能是峰值(比如mid是右边界,或者nums[mid] > nums[mid-1]),所以我们不能排除mid,只能把right收到mid。这个不对称的赋值,正是保证循环最终收敛到一个确切下标的秘诀。我第一次写的时候,曾错误地写成right = mid - 1,结果在[1,2]这种简单case上就死循环了——left=0, right=1, mid=0,nums[0]=1 < nums[1]=2,left变成1,此时left==right,循环退出,返回1,正确。但如果写成right = mid - 1,mid=0时right变成-1,left=0 > right=-1,循环退出,返回0,但nums[0]=1 < nums[1]=2,0不是峰值!这个错误让我debug了半小时。

第三,循环条件为什么是left < right,而不是left <= right?
因为我们的目标是找到一个下标,而不是搜索一个值。当left == right时,这个位置就是我们唯一剩下的候选,它必然满足峰值定义(由前面的存在性保证)。如果用<=,循环会在left==right时再执行一次,mid还是等于left,然后if判断后,left或right的赋值会导致其中一个越界,或者陷入无限循环。记住这个口诀:“找下标”用<,“找值”用<=。

3.2 双邻比较法:每一步都像在走钢丝,边界处理是最大挑战

这种方法更“老实”,每一步都试图确认mid是否为峰,或者通过比较邻居来决定方向。代码稍长,但逻辑分支清晰:

def findPeakElement(nums): n = len(nums) left, right = 0, n - 1 while left <= right: mid = (left + right) // 2 # 检查 mid 是否为峰值:需要和左右邻居比较 left_ok = (mid == 0) or (nums[mid - 1] < nums[mid]) right_ok = (mid == n - 1) or (nums[mid] > nums[mid + 1]) if left_ok and right_ok: return mid # 如果左边更大,峰值可能在左边(因为左边有上升空间) if mid > 0 and nums[mid - 1] > nums[mid]: right = mid - 1 # 否则,右边更大,峰值在右边 else: left = mid + 1 return -1 # 理论上不会到达这里

这里的坑比趋势法多得多:

注意:left_ok和right_ok的判断顺序不能颠倒,必须先处理边界,再比较数值,否则mid-1或mid+1可能越界。

第一,边界检查的“短路”逻辑是生命线。
left_ok = (mid == 0) or (nums[mid - 1] < nums[mid])这行代码,利用了Python的or短路特性。当mid == 0为真时,后面的nums[mid - 1]根本不会执行,从而避免了索引-1的越界错误。同理,right_ok也一样。如果你写成left_ok = (mid == 0 and True) or (nums[mid - 1] < nums[mid]),逻辑虽等价,但失去了短路保护,nums[-1]会被执行,导致错误。所有涉及数组边界的条件判断,都必须用or/and的短路特性来兜底。

第二,“左边更大”时,为什么right = mid - 1,而不是right = mid?
因为当nums[mid - 1] > nums[mid]时,mid本身已经比左边小了,它不可能是峰值(峰值要求比两边都大),所以mid可以被安全排除。同理,如果右边更大,mid也被排除。这和趋势法中“mid可能是峰值所以right = mid”形成了鲜明对比。双邻法的每一次赋值,都是基于对mid当前状态的明确否定。

第三,为什么循环条件是left <= right?
因为我们在循环体内,有明确的return mid语句,一旦找到峰值就立刻退出。循环的目的是“搜索”,而不是“收敛”,所以需要用<=来确保不遗漏最后一个元素。如果用<,当left和right相差1时(比如left=0, right=1),mid=0,如果nums[0]不是峰值,我们会进入else分支,left = mid + 1 = 1,此时left == right,循环因<条件失败而退出,但nums[1]还没被检查!所以必须用<=,让mid有机会取到right的值。

3.3 边界扩展法:用“虚构”换取“纯粹”,理解抽象建模的力量

这种方法不修改原数组,而是在思维层面添加两个“哨兵”:nums[-1] = -∞和nums[n] = -∞。这样,整个序列就变成了一个“山谷-山峰-山谷”的标准结构,峰值存在性不证自明。

def findPeakElement(nums): n = len(nums) left, right = 0, n - 1 while left < right: mid = (left + right) // 2 # 获取 mid 和 mid+1 的值,自动处理边界 # 如果 mid 是最后一个元素,nums[mid+1] 视为 -∞,所以 nums[mid] > -∞ 恒成立 # 但我们不需要显式写 -∞,只需在比较时做逻辑转换 if mid == n - 1 or nums[mid] < nums[mid + 1]: left = mid + 1 else: right = mid return left

这段代码看起来和趋势法几乎一样,但关键区别在于对mid == n-1的判断。当mid是最后一个索引时,nums[mid+1]不存在,我们将其视为-∞,那么nums[mid] < -∞永远为假,所以else分支总会执行,right = mid,从而保证mid这个右边界不会被跳过。这实际上把边界处理,转化成了一个简单的条件分支。

提示:这种方法的精髓不在于代码多短,而在于它教会你“如何定义问题”。很多算法难题,难点不在计算,而在如何设定一个让你的算法能稳定运行的“安全环境”。就像给一个不稳定的化学反应加缓冲溶液,边界扩展就是给二分算法加的缓冲。

最大的实操心得:不要真的去创建一个长度为n+2的新数组。
那样会增加空间复杂度,而且得手动填充-∞(Python里可以用float('-inf'),但比较时要注意类型)。真正的高手,是把“扩展”放在脑子里,只在代码的关键比较处,用if语句模拟这种扩展效果。这既节省资源,又体现了对问题本质的把握。

4. 实操过程与核心环节实现:从零开始,一行一行写出可运行的代码

4.1 趋势驱动法完整实现与逐行调试

我们以数组nums = [1, 2, 1, 3, 5, 6, 4]为例,手动模拟趋势驱动法的执行过程。这个数组有两个峰值:索引1(值2)和索引5(值6)。

  • 初始化:left = 0,right = 6
  • 第1轮:mid = (0+6)//2 = 3,nums[3] = 3,nums[4] = 5, 因为3 < 5,所以left = mid + 1 = 4
  • 第2轮:left = 4,right = 6,mid = (4+6)//2 = 5,nums[5] = 6,nums[6] = 4, 因为6 > 4,所以right = mid = 5
  • 第3轮:left = 4,right = 5,mid = (4+5)//2 = 4,nums[4] = 5,nums[5] = 6, 因为5 < 6,所以left = mid + 1 = 5
  • 第4轮:left = 5,right = 5, 循环条件left < right为假,退出循环,返回left = 5

结果正确,找到了峰值6。现在,我们把它写成完整的、带详细注释的Python函数:

def findPeakElement(nums): """ 使用趋势驱动法寻找峰值。 时间复杂度: O(log n) 空间复杂度: O(1) 核心思想: 如果 nums[mid] < nums[mid+1], 则右侧必有峰值; 否则左侧必有峰值。 Args: nums: List[int] - 输入的整数数组 Returns: int - 任意一个峰值的下标 """ # 边界情况:只有一个元素,它自己就是峰值 if len(nums) == 1: return 0 left, right = 0, len(nums) - 1 # 主循环:当 left 和 right 不重合时继续 while left < right: # 计算中点。使用 (left + right) // 2 避免潜在的整数溢出 mid = (left + right) // 2 # 关键比较:只和右邻居比较,避免左边界越界 # 因为循环条件是 left < right, 所以 right 至少为 1, mid+1 最大为 right, 是合法索引 if nums[mid] < nums[mid + 1]: # nums[mid] 比右边小,说明上升趋势在右边,峰值在 [mid+1, right] left = mid + 1 else: # nums[mid] >= nums[mid+1],说明此处是平顶或下降,峰值在 [left, mid] # 注意:这里不能写 right = mid - 1,因为 mid 本身可能是峰值 right = mid # 循环结束时,left == right,这个位置就是峰值下标 return left

测试用例验证:

  • findPeakElement([1])→ 返回0,正确。
  • findPeakElement([1, 2])→left=0, right=1, mid=0, nums[0]=1 < nums[1]=2→left=1→ 返回1,nums[1]=2是峰值,正确。
  • findPeakElement([2, 1])→left=0, right=1, mid=0, nums[0]=2 > nums[1]=1→right=0→ 返回0,nums[0]=2是峰值,正确。
  • findPeakElement([1, 2, 1, 3, 5, 6, 4])→ 如上模拟,返回5,正确。

4.2 双邻比较法完整实现与边界Case专项测试

双邻法的代码更长,但逻辑更“笨拙”也更可靠。我们重点测试那些容易出错的边界Case:

def findPeakElement(nums): """ 使用双邻比较法寻找峰值。 时间复杂度: O(log n) 空间复杂度: O(1) 核心思想: 在 mid 处检查是否为峰值;如果不是,则向更大的邻居方向移动。 Args: nums: List[int] - 输入的整数数组 Returns: int - 任意一个峰值的下标 """ n = len(nums) left, right = 0, n - 1 while left <= right: mid = (left + right) // 2 # 步骤1:检查 mid 是否为峰值 # 左边安全:mid 是第一个元素,或者比左边大 left_is_safe = (mid == 0) or (nums[mid - 1] < nums[mid]) # 右边安全:mid 是最后一个元素,或者比右边大 right_is_safe = (mid == n - 1) or (nums[mid] > nums[mid + 1]) # 如果左右都安全,mid 就是峰值 if left_is_safe and right_is_safe: return mid # 步骤2:如果左边邻居更大,说明峰值在左边(因为左边有上升空间) # 注意:这里必须先检查 mid > 0,否则 nums[mid-1] 会越界 if mid > 0 and nums[mid - 1] > nums[mid]: right = mid - 1 else: # 否则,右边邻居更大(或 mid 是最后一个元素,右边不存在,但上面的检查已保证它不是峰值,所以只能往右) left = mid + 1 # 理论上,由于峰值必然存在,程序不会执行到这里 return -1 # 专项测试:这些Case是双邻法的“照妖镜” print(findPeakElement([1])) # 0 -> 单元素 print(findPeakElement([1, 2])) # 1 -> 递增,峰值在右 print(findPeakElement([2, 1])) # 0 -> 递减,峰值在左 print(findPeakElement([1, 2, 3, 1])) # 2 -> 经典山峰 print(findPeakElement([1, 3, 2, 1])) # 1 -> 左侧高峰

为什么print(findPeakElement([1, 2]))返回1?
left=0, right=1, mid=0。left_is_safe:mid==0为真,所以True。right_is_safe:mid != n-1(1!=1? False),所以要看nums[0] > nums[1],即1 > 2,False。所以left_is_safe and right_is_safe为False。接着,mid > 0为False,所以进入else,left = mid + 1 = 1。下一轮left=1, right=1,mid=1。left_is_safe:mid==0为False,所以看nums[0] < nums[1],1<2为True。right_is_safe:mid == n-1为True(1==1)。所以返回1。完美。

4.3 边界扩展法的“思维体操”与代码落地

边界扩展法的代码实现,关键在于如何优雅地处理“虚拟边界”。我们不创建新数组,而是将边界逻辑内联到比较中:

def findPeakElement(nums): """ 使用边界扩展思想寻找峰值。 核心思想:将 nums[-1] 和 nums[n] 视为负无穷,从而保证峰值必然存在。 在代码中,这体现为:当访问 nums[mid+1] 越界时,我们认为它等于负无穷, 因此 nums[mid] > nums[mid+1] 恒成立(因为任何数都大于负无穷)。 Args: nums: List[int] - 输入的整数数组 Returns: int - 任意一个峰值的下标 """ n = len(nums) left, right = 0, n - 1 while left < right: mid = (left + right) // 2 # 获取 nums[mid+1] 的值。如果 mid+1 越界,我们视其为负无穷 # 在Python中,我们可以用一个辅助函数,但这里直接用条件表达式更清晰 if mid == n - 1: # nums[mid+1] 不存在,视为 -∞,所以 nums[mid] > -∞ 恒成立 # 因此,我们希望进入 "else" 分支,即 right = mid # 所以条件 nums[mid] < nums[mid+1] 应为 False # 我们可以直接写 False,但为了逻辑清晰,写成下面这样 next_val = float('-inf') else: next_val = nums[mid + 1] # 现在进行标准比较 if nums[mid] < next_val: left = mid + 1 else: right = mid return left

更Pythonic的写法(推荐):
利用Python的try/except,或者更简洁的or技巧:

def findPeakElement(nums): n = len(nums) left, right = 0, n - 1 while left < right: mid = (left + right) // 2 # 使用 or 技巧:如果 mid+1 是有效索引,取 nums[mid+1];否则取一个极小值 # 因为我们要的是 "nums[mid] < nums[mid+1]",如果 nums[mid+1] 不存在,这个比较应为 False # 所以我们让不存在时的值为 float('inf')?不,那样 nums[mid] < inf 恒为 True,错了。 # 正确做法:让不存在时的值为 float('-inf'),这样 nums[mid] < -inf 恒为 False。 next_val = nums[mid + 1] if mid + 1 < n else float('-inf') if nums[mid] < next_val: left = mid + 1 else: right = mid return left

个人实操心得:
我在某次面试中被要求手写这个解法,当时紧张,忘了float('-inf')的写法,情急之下写了next_val = nums[mid + 1] if mid < n - 1 else -10**10。面试官笑了,说:“这个数够小吗?如果数组里有-10**10 - 1呢?” 我当场愣住。这个教训让我明白:在算法中,永远优先使用语言提供的语义化常量(如float('-inf')),而不是自己拍脑袋的“足够小”的数字。因为前者是数学意义上的“无穷小”,后者永远有被超越的风险。

5. 常见问题与排查技巧实录:那些年我们一起踩过的坑

5.1 死循环:left和right像钟摆一样来回晃

这是二分最经典的bug。现象是:left和right的值在两三个数之间反复切换,循环永不退出。根本原因,几乎100%是缩小区间的赋值方式错误。

错误代码片段问题分析修复方案
if nums[mid] < nums[mid+1]: left = midleft没有越过mid,mid会被重复计算,导致死循环left = mid + 1,确保mid被排除
else: right = mid + 1right跳到了mid右边,可能把峰值跳过去,且left和right的距离没缩小right = mid,保持mid在候选集中
mid = left + (right - left) / 2(在Java/C++中)/是浮点除法,mid变成float,导致索引错误改为mid = left + (right - left) / 2(整数除) 或mid = (left + right) >> 1

排查技巧:
在循环内部,第一行就打印left, right, mid。运行一个简单Case,比如[1,2],观察输出:

left=0, right=1, mid=0 left=0, right=1, mid=0 left=0, right=1, mid=0 ...

看到mid不变,立刻就知道left或right的更新逻辑有问题。

5.2 索引越界:IndexError: list index out of range

这通常发生在双邻比较法中,忘记做边界检查。

错误场景错误代码正确写法
访问nums[mid-1]时mid=0if nums[mid-1] > nums[mid]: ...if mid > 0 and nums[mid-1] > nums[mid]: ...
访问nums[mid+1]时mid=n-1if nums[mid] < nums[mid+1]: ...if mid < n-1 and nums[mid] < nums[mid+1]: ...或用趋势法只和mid+1比(保证mid+1合法)

终极防护:
在函数开头,加一个防御性断言:assert len(nums) >= 1。虽然题目保证了,但加上它,能让bug在最早的地方暴露。

5.3 逻辑错误:返回了非峰值的下标

这往往是因为对“峰值”定义理解有偏差。峰值是严格大于左右邻居,不是大于等于。

错误理解错误代码后果修正
认为>=也可以if nums[mid] >= nums[mid+1]在[1,1,1,1]中,所有元素都相等,没有严格峰值,但此条件会让算法返回一个下标,而该下标不是峰值题目保证有解,但[1,1,1,1]不是合法输入(因为没有峰值),所以不必处理。但比较时必须用>和<,不能用>=。

验证技巧:
写一个简单的验证函数,在返回结果后,自动检查:

def is_peak(nums, idx): n = len(nums) left_ok = (idx == 0) or (nums[idx-1] < nums[idx]) right_ok = (idx == n-1) or (nums[idx] > nums[idx+1]) return left_ok and right_ok # 在主函数返回前调用 result = left assert is_peak(nums, result), f"Found {result}, but nums[{result}]={nums[result]} is not a peak"

5.4 性能陷阱:你以为的O(log n),其实是O(n)

这听起来荒谬,但确实存在。罪魁祸首是在循环内进行了O(n)的操作。

隐蔽陷阱代码示例问题解决方案
在每次循环中,都调用max(nums[left:right+1])if nums[mid] < max(nums[mid+1:right+1]): ...max()是O(n)操作,整个算法退化为O(n log n)删除所有循环内的O(n)操作,只做O(1)的比较
创建切片nums[mid+1:]if nums[mid] < max(nums[mid+1:]): ...切片是O(k)操作,k是切片长度直接访问nums[mid+1],不要试图“看远处”

经验之谈:
我曾经优化一个同事的代码,他为了“保险”,在二分循环里每次都用sum(nums[left:right+1])来计算区间和,以为这样能“更好地指导方向”。结果在10^5长度的数组上,超时了。我把sum删掉,换成单点比较,瞬间AC。二分的威力,全部来自于O(1)的决策。任何在循环内做的“全局计算”,都是对二分精神的背叛。

6. 思维跃迁:从“解LeetCode题”到“设计现实系统”

把一道算法题讲透,最终目的不是为了多AC一道题,而是为了把这种结构化拆解、抽象建模、严谨验证的思维,迁移到真实的工程问题中。我分享一个亲身经历:

某次,我们团队要设计一个“智能温控系统”,目标是让房间温度快速、平稳地达到设定值,且能耗最低。传感器每秒上报一个温度值,形成一个时间序列。我们需要在海量的历史数据中,找出“温度开始显著上升”和

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 1:25:30

STM32F103移植FreeRTOS通用模板:任务调度、内存管理与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:25:22

TX2嵌入式AI平台GStreamer推流实战:从pipeline设计到硬件编码调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:25:07

基于MP2642GR-Z和GD32VF103的2S磷酸铁锂主动均衡方案设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:25:03

Istio Service Mesh实战:从核心架构到流量治理与可观测性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:24:58

Cortex-M0内核MCU上FreeRTOS移植实战:从工程搭建到中断优先级避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:24:56

独热编码:原理、实战与高基数类别特征处理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华