1. 问题背景与理解
今天我想分享一道LeetCode上看似简单但暗藏玄机的题目——896. Monotonic Array(单调数列)。这道题在周赛中出现的频率相当高,我在实际刷题过程中发现,很多同学(包括最初的我)都会掉入一些典型的思维陷阱。
题目要求我们判断一个数组是否是单调的,即要么完全递增,要么完全递减。举个具体例子:
- [1,2,2,3] 是单调递增
- [6,5,4,4] 是单调递减
- [1,3,2] 则不是单调的
表面上看这题很简单,但当我尝试在100ms内完成时,发现需要特别注意几个关键点:如何处理连续相等元素、如何高效判断单调性方向、以及如何避免不必要的计算。下面我就详细拆解这道题的优化解法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础解法与问题分析
2.1 直观的双遍历法
最直观的解法是分别检查数组是否为递增或递减:
python复制def isMonotonic(nums):
return (all(nums[i] <= nums[i+1] for i in range(len(nums)-1)) or
all(nums[i] >= nums[i+1] for i in range(len(nums)-1)))
这种方法虽然清晰,但需要遍历数组两次,时间复杂度O(2n)。当数组很大时(比如LeetCode的测试用例中n可达50000),这种解法就不够高效。
2.2 单次遍历的优化思路
更聪明的做法是在一次遍历中同时检查递增和递减的可能性。具体思路是:
- 初始化两个标志位:increasing = decreasing = True
- 遍历数组,比较相邻元素:
- 如果nums[i] > nums[i+1],则不可能递增,increasing = False
- 如果nums[i] < nums[i+1],则不可能递减,decreasing = False
- 最后如果increasing或decreasing任一为True,则数组是单调的
这种解法只需要一次遍历,时间复杂度优化到O(n)。但在实际实现时,我发现有几个细节需要注意:
- 当increasing和decreasing都变为False时,可以提前终止遍历
- 相等的元素不影响单调性,需要特别处理
3. 最优解实现与性能分析
3.1 最终优化代码
经过多次测试和优化,我得到了以下能在100ms内完成的Python解法:
python复制def isMonotonic(nums):
incr = decr = True
for i in range(len(nums)-1):
if nums[i] > nums[i+1]:
incr = False
if nums[i] < nums[i+1]:
decr = False
if not incr and not decr:
return False
return incr or decr
3.2 关键优化点解析
-
提前终止机制:当发现既非递增也非递减时立即返回False,避免不必要的后续比较。这在随机数组测试中能显著减少比较次数。
-
并行检查:同时维护incr和decr两个标志位,而不是先检查一种情况再检查另一种。这种并行处理方式比串行检查更高效。
-
边界条件处理:
- 空数组或单元素数组自动视为单调
- 所有元素相等的数组既是递增也是递减
3.3 复杂度分析
- 时间复杂度:最优情况O(1)(前两个元素就确定非单调),最差情况O(n)
- 空间复杂度:O(1),只使用了常数个额外变量
4. 测试用例与验证
为了确保代码的正确性,我设计了以下几组测试用例:
-
基本测试:
python复制assert isMonotonic([1,2,2,3]) == True # 单调递增 assert isMonotonic([6,5,4,4]) == True # 单调递减 assert isMonotonic([1,3,2]) == False # 非单调 -
边界测试:
python复制assert isMonotonic([]) == True # 空数组 assert isMonotonic([1]) == True # 单元素 assert isMonotonic([2,2,2,2]) == True # 全等 -
性能测试:
python复制# 大数组测试 big_inc = list(range(100000)) big_dec = list(range(100000,0,-1)) big_random = [random.randint(0,100) for _ in range(100000)] assert isMonotonic(big_inc) == True assert isMonotonic(big_dec) == True assert isMonotonic(big_random) == False
5. 常见错误与避坑指南
在解决这个问题时,我遇到了几个典型的坑,这里分享给大家:
-
忽略相等元素:
- 错误做法:只检查严格递增或递减,导致[1,2,2,3]被判为非单调
- 正确做法:使用<=和>=而非<和>
-
方向判断混乱:
- 错误做法:先确定方向再检查,这在[1,1,1,2,1]这类数组中会出错
- 正确做法:同时维护两个标志位
-
过早优化:
- 错误做法:试图在第一次比较时就确定方向,难以处理前几个元素相等的情况
- 正确做法:保持灵活性直到方向明确
-
不必要的变量:
- 错误做法:存储整个比较结果数组
- 正确做法:只需维护当前状态
6. 算法扩展与应用
虽然这道题本身很简单,但它涉及的模式在实际开发中很常见:
- 数据流监控:监控系统指标是否持续增长/下降
- 交易分析:判断股票价格趋势
- 传感器数据:检测设备读数变化趋势
理解这种单调性检查的算法,可以帮助我们快速识别数据的整体趋势。对于更复杂的情况,比如判断某个子序列是否单调,也可以基于这个思路进行扩展。
在实际工程中,我们可能还需要考虑:
- 滑动窗口内的单调性检查
- 允许小幅波动的"近似单调"判断
- 多维数据的单调性分析
7. 不同语言的实现对比
为了更深入理解这个算法,我对比了几种常见语言的实现方式:
7.1 Java实现
java复制public boolean isMonotonic(int[] nums) {
boolean inc = true, dec = true;
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] > nums[i+1]) inc = false;
if (nums[i] < nums[i+1]) dec = false;
if (!inc && !dec) return false;
}
return inc || dec;
}
7.2 C++实现
cpp复制bool isMonotonic(vector<int>& nums) {
bool inc = true, dec = true;
for (int i = 0; i < nums.size() - 1; ++i) {
if (nums[i] > nums[i+1]) inc = false;
if (nums[i] < nums[i+1]) dec = false;
if (!inc && !dec) return false;
}
return inc || dec;
}
7.3 JavaScript实现
javascript复制function isMonotonic(nums) {
let inc = true, dec = true;
for (let i = 0; i < nums.length - 1; i++) {
if (nums[i] > nums[i+1]) inc = false;
if (nums[i] < nums[i+1]) dec = false;
if (!inc && !dec) return false;
}
return inc || dec;
}
从这些实现可以看出,核心逻辑在各语言中高度一致,主要区别在于语法细节。这也说明了算法本身的普适性。
8. 性能优化进阶
对于特别大的数组,我们还可以考虑以下优化策略:
- 并行计算:将数组分块,在不同线程/核心上并行检查单调性
- 向量化操作:利用SIMD指令同时比较多个元素
- 内存访问优化:确保数据访问模式是缓存友好的
不过对于LeetCode题目来说,单次遍历的解法已经足够高效。这些进阶优化更适合实际工程中的性能关键场景。
9. 相关题目推荐
为了巩固单调性检查的相关技能,我推荐练习以下LeetCode题目:
-
- Longest Increasing Subsequence(最长递增子序列)
-
- Increasing Triplet Subsequence(递增的三元子序列)
-
- Valid Mountain Array(有效的山脉数组)
-
- Longest Mountain in Array(数组中最长的山脉)
这些题目都涉及数组元素的顺序关系分析,掌握单调性的判断方法对解决它们很有帮助。
10. 个人刷题心得
在解决这个问题的过程中,我总结了几个重要的经验:
- 不要轻视简单题:即使是Easy题目也可能有优化的空间和陷阱
- 测试用例设计:要考虑各种边界情况,特别是相等元素和空数组
- 提前终止是优化关键:一旦确定结果就立即返回,避免不必要计算
- 保持代码简洁:清晰的逻辑比复杂的优化更重要
这道题也提醒我,在面试中遇到简单问题时,应该主动讨论各种可能的解法和优化空间,而不仅仅是写出能通过的代码。
