1. 题目背景与核心需求
这道题目来自LeetCode经典面试75题中的第8题,编号334,题目名为"递增的三元子序列"。这是一道中等难度的数组处理题目,在各大科技公司的技术面试和机考笔试中频繁出现。题目要求我们判断给定的整数数组中是否存在一个递增的三元子序列,即是否存在三个下标i < j < k,使得nums[i] < nums[j] < nums[k]。
提示:这里的"子序列"不需要连续,只要保持相对顺序即可。例如在数组[1,3,2,4]中,[1,3,4]就是一个有效的递增三元子序列。
这道题之所以成为面试高频题,是因为它考察了以下几个核心能力:
- 对数组遍历和状态维护的理解
- 对贪心算法思想的掌握
- 对时间复杂度和空间复杂度的优化意识
- 边界条件的处理能力
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法与优化思路
2.1 直观的三重循环解法
最直观的解法是使用三重循环枚举所有可能的三元组:
python复制def increasingTriplet(nums):
n = len(nums)
for i in range(n):
for j in range(i+1, n):
for k in range(j+1, n):
if nums[i] < nums[j] < nums[k]:
return True
return False
这种解法的时间复杂度是O(n³),在LeetCode上会直接超时,完全无法通过测试用例。我们需要更高效的算法。
2.2 优化思路:寻找中间元素
观察题目特点,我们可以发现关键在于找到一个"中间人"元素,它左边有比它小的元素,右边有比它大的元素。基于这个观察,我们可以采用双向遍历的方法:
- 从左到右遍历,记录每个位置左侧的最小值
- 从右到左遍历,记录每个位置右侧的最大值
- 最后遍历数组,检查是否存在一个元素,它大于左侧最小值且小于右侧最大值
这种方法将时间复杂度降低到了O(n),空间复杂度也是O(n),是一个显著的优化。
3. 双向遍历法的实现细节
3.1 预处理左右极值数组
首先我们需要创建两个辅助数组:
left_min:left_min[i]表示nums[0..i-1]中的最小值right_max:right_max[i]表示nums[i+1..n-1]中的最大值
python复制def increasingTriplet(nums):
n = len(nums)
if n < 3:
return False
left_min = [0] * n
left_min[0] = nums[0]
for i in range(1, n):
left_min[i] = min(left_min[i-1], nums[i])
right_max = [0] * n
right_max[-1] = nums[-1]
for i in range(n-2, -1, -1):
right_max[i] = max(right_max[i+1], nums[i])
for i in range(1, n-1):
if left_min[i-1] < nums[i] < right_max[i+1]:
return True
return False
3.2 边界条件处理
在实际编码中,有几个边界条件需要特别注意:
- 数组长度小于3时直接返回False
- 第一个元素没有左侧元素,最后一个元素没有右侧元素
- 处理重复元素的情况(如[1,1,1,1,1])
注意:在LeetCode的测试用例中,会包含一些极端情况,比如所有元素相同、严格递增/递减序列等,我们的代码必须能够正确处理这些情况。
4. 更优的贪心算法解法
虽然双向遍历法已经将复杂度优化到了O(n),但我们还可以进一步优化空间复杂度到O(1)。这种方法基于贪心算法的思想:
python复制def increasingTriplet(nums):
first = second = float('inf')
for num in nums:
if num <= first:
first = num
elif num <= second:
second = num
else:
return True
return False
这个解法的核心思想是:
- 维护两个变量
first和second,分别表示当前找到的最小和次小值 - 遍历数组时,如果找到一个数大于
first和second,就找到了满足条件的三元组 - 否则更新
first或second的值
4.1 贪心算法的正确性证明
为什么这种方法能保证正确性?关键在于:
- 当遇到一个数大于
second时,说明已经存在first < second < current的三元组 - 更新
first和second的顺序保证了即使first被更新到较后的位置,second仍然保持着之前某个比first大的值 - 这种方法实际上是在寻找一个"潜在的"三元组,而不需要显式地记录所有可能
5. 算法复杂度分析
让我们比较一下两种方法的复杂度:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 三重循环 | O(n³) | O(1) | 仅用于理解问题,实际不可用 |
| 双向遍历 | O(n) | O(n) | 通用解法,易于理解 |
| 贪心算法 | O(n) | O(1) | 最优解,面试推荐 |
在实际面试中,建议先解释双向遍历法的思路,然后进一步优化到贪心算法,展示你的算法优化能力。
6. 常见错误与调试技巧
6.1 初始化值的陷阱
在实现贪心算法时,first和second的初始值设置很关键。常见错误包括:
- 初始化为0:无法处理负数情况
- 初始化为数组第一个元素:可能导致错误判断
正确的做法是初始化为无穷大(float('inf')),这样可以确保任何数都会比初始值小。
6.2 等号处理的细节
注意代码中的<=而不是<,这是为了正确处理重复元素的情况。例如对于输入[1,1,1,1],应该返回False,如果使用<可能会导致错误判断。
6.3 测试用例设计
为了验证代码的正确性,建议设计以下几类测试用例:
- 常规情况:[1,2,3,4,5] → True
- 无解情况:[5,4,3,2,1] → False
- 边界情况:[1,2] → False
- 重复元素:[1,1,1,1] → False
- 混合情况:[5,1,5,5,2,5,4] → True
7. 同类问题扩展
掌握了这道题的解法后,可以尝试解决以下类似问题:
- LeetCode 300. 最长递增子序列
- LeetCode 673. 最长递增子序列的个数
- LeetCode 354. 俄罗斯套娃信封问题
这些问题都涉及到序列中递增关系的处理,可以运用类似的贪心思想或动态规划方法解决。
在实际面试中,面试官可能会基于这道题进行扩展提问,比如:
- 如果要求返回具体的三元组而不仅仅是判断是否存在,该如何修改代码?
- 如果要求判断是否存在递增的四元组,算法该如何调整?
- 如果数组非常大,内存有限,该如何优化?
8. 面试实战建议
在面试中遇到这类题目时,建议采取以下步骤:
- 先明确问题要求,确认理解正确
- 提出暴力解法并分析其复杂度
- 思考优化方向,逐步改进算法
- 编写代码时注意边界条件
- 用测试用例验证代码正确性
- 讨论可能的扩展问题
记住,面试官不仅关注最终答案,更看重你的解题思路和沟通能力。即使不能立即想到最优解,清晰地表达你的思考过程也非常重要。
我在实际面试辅导中发现,很多同学在实现贪心算法时容易忽略等号的处理,或者在初始化值时犯错。建议在平时练习时就要注意这些细节,养成严谨的编码习惯。
