1. 题目解析与核心思路
LeetCode 896题"Monotonic Array"要求我们判断一个数组是否是单调的。所谓单调数组,指的是数组中的元素要么完全递增(非严格),要么完全递减(非严格)。这个题目看似简单,但考察了我们对数组遍历和逻辑判断的基本功。
1.1 题目要求详解
给定一个整数数组A,我们需要编写一个函数来判断它是否是单调的。具体来说:
- 如果对于所有i <= j,A[i] <= A[j],那么数组是单调递增的
- 如果对于所有i <= j,A[i] >= A[j],那么数组是单调递减的
- 如果数组同时满足以上两种情况(即所有元素相等),也视为单调
1.2 解题思路分析
最直观的解法是遍历数组两次:
- 第一次检查是否单调递增
- 第二次检查是否单调递减
- 如果满足其中任意一种情况,返回true;否则返回false
但这种解法时间复杂度为O(2n),虽然渐进复杂度仍然是O(n),但实际运行时间可以优化。
更高效的解法是在一次遍历中同时检查递增和递减的可能性:
- 初始化两个标志位:isIncreasing和isDecreasing为true
- 遍历数组,比较相邻元素
- 如果发现A[i] > A[i+1],则isIncreasing=false
- 如果发现A[i] < A[i+1],则isDecreasing=false
- 如果两个标志位都为false,可以提前终止遍历
- 最后返回isIncreasing || isDecreasing
这种单次遍历的解法时间复杂度为O(n),空间复杂度为O(1),是最优解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 代码实现与优化
2.1 基础实现(Python)
python复制def isMonotonic(A):
increasing = decreasing = True
for i in range(len(A) - 1):
if A[i] > A[i+1]:
increasing = False
if A[i] < A[i+1]:
decreasing = False
if not increasing and not decreasing:
return False
return increasing or decreasing
2.2 优化技巧
- 提前终止:当发现数组既不递增也不递减时,可以立即返回False,避免不必要的遍历
- 边界处理:空数组或单元素数组默认视为单调
- 简化比较:可以只维护一个方向标志,通过比较前两个元素确定检查方向
2.3 其他语言实现
C++版本:
cpp复制bool isMonotonic(vector<int>& A) {
bool increasing = true;
bool decreasing = true;
for (int i = 0; i < A.size() - 1; ++i) {
if (A[i] > A[i+1]) increasing = false;
if (A[i] < A[i+1]) decreasing = false;
if (!increasing && !decreasing) return false;
}
return increasing || decreasing;
}
Java版本:
java复制public boolean isMonotonic(int[] A) {
boolean increasing = true;
boolean decreasing = true;
for (int i = 0; i < A.length - 1; ++i) {
if (A[i] > A[i+1]) increasing = false;
if (A[i] < A[i+1]) decreasing = false;
if (!increasing && !decreasing) return false;
}
return increasing || decreasing;
}
3. 复杂度分析与性能优化
3.1 时间复杂度
所有实现的时间复杂度都是O(n),因为我们需要遍历整个数组一次(最坏情况下)。最佳情况下(如数组前两个元素就确定不单调),时间复杂度可以是O(1)。
3.2 空间复杂度
空间复杂度为O(1),因为我们只使用了常数个额外变量(两个布尔标志位)。
3.3 性能优化实践
在实际编码中,我们可以通过以下方式进一步优化:
- 减少比较次数:可以先比较前两个元素确定检查方向,然后只检查该方向
- 使用位运算:将两个布尔标志合并为一个整数标志
- 循环展开:对于特别大的数组,可以考虑循环展开以减少分支预测错误
4. 常见错误与调试技巧
4.1 典型错误案例
-
边界条件处理不当:
- 忘记处理空数组或单元素数组的情况
- 数组越界(比较A[i]和A[i+1]时i的范围)
-
逻辑错误:
- 混淆递增和递减的判断条件
- 没有正确维护两个标志位的关系
-
过早优化:
- 试图在确定方向前就进行优化,导致代码复杂且容易出错
4.2 调试技巧
-
测试用例设计:
- 空数组[]
- 单元素数组[1]
- 全等数组[1,1,1]
- 严格递增数组[1,2,3]
- 严格递减数组[3,2,1]
- 非单调数组[1,3,2]
- 长随机数组
-
打印调试:
- 在循环中打印标志位的变化
- 可视化数组元素的走势
-
逐步验证:
- 先实现双遍历的简单版本
- 再优化为单遍历版本
- 最后考虑进一步优化
5. 实际应用与扩展思考
5.1 实际应用场景
单调性判断在实际开发中有广泛应用:
- 时间序列分析:判断数据趋势(如股票价格)
- 数据库优化:索引的单调性影响查询性能
- 算法设计:许多算法(如二分查找)要求输入数据具有某种单调性
5.2 扩展思考
- 严格单调判断:如何修改代码判断严格单调(不允许相等元素)?
- 分段单调判断:如何判断数组是否可以分成k段,每段内部单调?
- 最长单调子数组:如何找到最长的单调连续子数组?
- 多维单调性:如何判断矩阵的行或列是否单调?
5.3 相关LeetCode题目
- 最长递增子序列 (#300)
- 山脉数组 (#941)
- 有效的山脉数组 (#941)
- 摆动序列 (#376)
- 单调栈相关题目 (#496, #503, #739)
6. 个人实战经验分享
在实际解决这个问题时,我发现几个值得注意的点:
-
初始方向确定:有时候数组开头可能有多个相等的元素,这时需要跳过这些元素才能确定真正的趋势方向。例如[1,1,1,2,3]和[1,1,1,0,-1]。
-
代码简洁性:虽然可以通过更复杂的逻辑减少比较次数,但往往得不偿失。保持代码简洁易懂更重要。
-
测试用例覆盖:一定要测试各种边界情况,特别是全等数组和长数组,这些情况容易暴露性能问题。
-
语言特性利用:不同语言有各自的优化方式。例如在Python中,使用内置的all()函数可能更高效,但会失去提前终止的优势。
-
实际性能考量:在LeetCode上提交时,有时候最简单的实现反而运行最快,因为测试用例可能不大,优化带来的收益被额外判断的开销抵消了。
