news 2026/9/6 2:00:17

从超时到AC;两种解法的深度剖析:力扣3904

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从超时到AC;两种解法的深度剖析:力扣3904

一次看似正确的O(n)解法,为何在LeetCode上惨遭TLE?本文将带你拆解背后的时间复杂度陷阱。

一、问题背景

在刷力扣3904题时,我遇到了一个有趣的现象:两种思路几乎相同的解法,一种超时,一种顺利通过。这不仅仅是代码风格的区别,而是对算法复杂度本质理解的体现。

题目要求我们找到第一个"稳定索引"——即前缀最大值与后缀最小值之差不超过给定值k的最早位置。

先看我的第一版代码(注释部分)和优化后的版本:

// 超时版本(注释部分)力扣3903行 int firstStableIndex(vector<int>& nums, int k) { int n = nums.size(); if (n == 1) return 0; int rightMin = INT_MAX; for (int num : nums) rightMin = min(rightMin, num); int leftMax = INT_MIN; for (int i = 0; i < n; i++) { leftMax = max(leftMax, nums[i]); if (leftMax - rightMin <= k) return i; if (nums[i] == rightMin) { rightMin = INT_MAX; for (int j = i+1; j < n; j++) { rightMin = min(rightMin, nums[j]); } } } return -1; } // AC版本 //两次for循环是o(2n)但是常熟系数一般忽略,于是还是o(n) int firstStableIndex(vector<int>& nums, int k) { int n = nums.size(); vector<int> minValue(n); minValue[n - 1] = nums[n - 1]; for (int i = n - 2; i >= 0; --i) { minValue[i] = min(minValue[i + 1], nums[i]); } int maxValue = INT_MIN; for (int i = 0; i < n; ++i) { maxValue = max(maxValue, nums[i]); if (maxValue - minValue[i] <= k) { return i; } } return -1; }

二、直观理解:两种解法在做什么?

场景比喻

想象你在一条街道上从左向右走,手里拿着一个记录牌(记录左边遇到的最大值),同时你需要知道右边街道上最小的房子高度。

  • 第一种做法:一开始就记住整条街最矮的房子。每当你路过当前最矮的房子时,就重新跑回街道右侧,重新确认最矮的是谁。

  • 第二种做法:出发前,就在每个位置贴好标签:"从这里到终点,最矮的是XXX"。你只需要边走边看标签即可。

显然,第二种做法更聪明——用空间换时间

三、时间复杂度深度分析

超时版本:披着O(n)外衣的O(n²)

表面上看,超时版本只有一个for循环,似乎是O(n)。但陷阱藏在rightMin的更新逻辑中:

if (nums[i] == rightMin) { rightMin = INT_MAX; for (int j = i+1; j < n; j++) { rightMin = min(rightMin, nums[j]); } }

关键问题:这个内部循环在最坏情况下会执行多少次?

考虑一个严格递减数组[5, 4, 3, 2, 1]

  • i=0:rightMin=1,nums[0]=5≠1,不触发

  • i=1:rightMin=1,nums[1]=4≠1,不触发

  • i=2:rightMin=1,nums[2]=3≠1,不触发

  • i=3:rightMin=1,nums[3]=2≠1,不触发

  • i=4:rightMin=1,nums[4]=1,触发!扫描后面0个元素

这种情况其实还好,因为rightMin一直没变。

真正的噩梦递增数组[1, 2, 3, 4, 5]

  • i=0:rightMin=1,nums[0]=1,触发!扫描i+1到末尾(4个元素),找到新的rightMin=2

  • i=1:rightMin=2,nums[1]=2,触发!扫描i+1到末尾(3个元素),找到新的rightMin=3

  • i=2:rightMin=3,nums[2]=3,触发!扫描i+1到末尾(2个元素)

  • i=3:rightMin=4,nums[3]=4,触发!扫描i+1到末尾(1个元素)

  • i=4:rightMin=5,nums[4]=5,触发!扫描0个元素

总操作数:4+3+2+1+0 =10次,约等于n(n-1)/2

对于n=100000,这就是5×10⁹次操作——超时是必然的

AC版本:真正的O(n)

再看AC版本:

vector<int> minValue(n); minValue[n - 1] = nums[n - 1]; for (int i = n - 2; i >= 0; --i) { minValue[i] = min(minValue[i + 1], nums[i]); }

一次预处理,每个位置的后缀最小值已经算好。后续遍历时:

int maxValue = INT_MIN; for (int i = 0; i < n; ++i) { maxValue = max(maxValue, nums[i]); if (maxValue - minValue[i] <= k) { return i; } }

每个元素只访问常数次,总操作数2n,对于n=100000就是20万次操作,快了几个数量级。

四、复杂度对比表

维度超时版本AC版本
时间复杂度O(n²) 最坏O(n)
空间复杂度O(1)O(n)
n=10⁵时操作数≈5×10⁹≈2×10⁵
LeetCode状态TLEAC

五、为什么会写出超时版本?

这是一个很典型的认知偏差

  1. 只看表面循环:看到只有一个for,就想当然认为是O(n)

  2. 忽略隐藏循环:内部重新扫描的代价被低估

  3. 平均情况误导:可能心想"大部分情况不会触发重新扫描",但最坏情况是算法分析必须考虑的

六、如何避免这类错误?

1. 识别"动态更新"的成本

当你需要频繁查询"区间最值"且数据会变化时,警惕每次O(n)的重新计算。

2. 经典优化思路

  • 预处理:如本题的后缀数组

  • 线段树/树状数组:动态区间查询

  • 单调栈/队列:维护滑动窗口极值

  • :动态维护最值

3. 复杂度分析要严谨

不要只数循环层数,要计算每条语句在最坏情况下的总执行次数

七、延伸思考

如果题目要求动态修改数组,那么预处理的后缀数组就不适用了。这时可以考虑:

  • 线段树:O(log n) 查询区间最小值,O(log n) 更新

  • 分块:O(sqrt(n)) 查询,平衡时间和空间

但本题是静态数组,预处理后缀数组是最优解——时间O(n),空间O(n)

八、总结

要点说明
时间复杂度陷阱隐藏循环可能将O(n)变为O(n²)
空间换时间用O(n)空间换取O(n)时间,往往是值得的
最坏情况分析不能仅凭平均情况或直觉判断复杂度
优化思路对区间最值查询,预处理是常用且高效的手段

最后送给读者一句话

写代码时,不仅要看"有几层循环",更要看"每条语句执行多少次"。真正的复杂度,藏在最坏情况的细节里。

如果这篇文章对你有帮助,欢迎点赞收藏,也欢迎在评论区交流讨论.

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

二次元AI学习陪伴插件:猫娘计划如何重构学习体验

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

作者头像 李华
网站建设 2026/9/6 1:57:59

【单片机毕业设计推荐】基于 STM32 或 51 单片机的智能恒温饮水杯控制系统设计 基于 STM32 或 51 单片机的水质 TDS 检测智能水杯设计(025307)

文章目录 20 个相关毕业设计备选题目项目研究背景摘要总体方案核心功能技术路线项目演示关于我们项目案例源码获取 温馨提示&#xff1a;本人主页置顶文章(点我)有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)有 CSDN 平台…

作者头像 李华
网站建设 2026/9/6 1:57:50

Flutter 三方库 flutter_native_timezone 的 OpenHarmony 适配实战(0-1)

Flutter 三方库 flutter_native_timezone 的 OpenHarmony 适配实战&#xff08;0-1&#xff09; 本文记录了将开源 Flutter 三方库 flutter_native_timezone 从零适配到 OpenHarmony / HarmonyOS 平台的完整过程&#xff0c; 包含上游代码基线替换、OHOS 平台脚手架生成、ArkTS…

作者头像 李华
网站建设 2026/9/6 1:56:03

智能体开发实操指南:从概念到Agent落地避坑

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

作者头像 李华
网站建设 2026/9/6 1:55:32

分布式系统全球化部署:架构设计与Kubernetes多区域实践

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

作者头像 李华
网站建设 2026/9/6 1:54:57

家用局域网私有云备份网站搭建

由于家里没有NAS&#xff0c;想着利用家里的台式机作为备份服务器&#xff0c;搭建一个家用局域网内的私有云盘&#xff0c;在此记录一下过程&#xff1a;1. 选择cloudreve软件搭建网站&#xff0c;github上下载安装包cloudreve_4.17.0_windows_amd64.zip&#xff0c;在作为备份…

作者头像 李华