给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。
如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。
进阶:很容易想到时间复杂度为 O(n^2) 的解决方案,你可以设计一个时间复杂度为 O(n logn) 或 O(n) 的解决方案吗?
枚举 3 是容易想到并且也是最容易实现的。由于 3 是模式中的最大值,并且其出现在 1和 2 的中间,因此我们只需要从左到右枚举 3 的下标 j,那么:
代码:
class Solution {
public boolean find132pattern(int[] nums) {
int n = nums.length;
if (n < 3) {
return false;
}
// 左侧最小值
int leftMin = nums[0];
// 右侧所有元素
TreeMap rightAll = new TreeMap();
for (int k = 2; k < n; ++k) {
rightAll.put(nums[k], rightAll.getOrDefault(nums[k], 0) + 1);
}
for (int j = 1; j < n - 1; ++j) {
if (leftMin < nums[j]) {
Integer next = rightAll.ceilingKey(leftMin + 1);
if (next != null && next < nums[j]) {
return true;
}
}
leftMin = Math.min(leftMin, nums[j]);
rightAll.put(nums[j + 1], rightAll.get(nums[j + 1]) - 1);
if (rightAll.get(nums[j + 1]) == 0) {
rightAll.remove(nums[j + 1]);
}
}
return false;
}
}
如果我们从左到右枚举 1 的下标 i,那么 j,k 的下标范围都是减少的,这样就不利于对它们进行维护。因此我们可以考虑从右到左枚举 i。
那么我们应该如何维护 j,k 呢?在 132 模式中,如果 1<2 并且 2<3,那么根据传递性,1<3 也是成立的,那么我们可以使用下面的方法进行维护:
我们使用一种数据结构维护所有遍历过的元素,它们作为 2 的候选元素。每当我们遍历到一个新的元素时,就将其加入数据结构中;
在遍历到一个新的元素的同时,我们可以考虑其是否可以作为 3。如果它作为 3,那么数据结构中所有严格小于它的元素都可以作为 2,我们将这些元素全部从数据结构中移除,并且使用一个变量维护所有被移除的元素的最大值。这些被移除的元素都是可以真正作为 2 的,并且元素的值越大,那么我们之后找到 1 的机会也就越大。
那么这个「数据结构」是什么样的数据结构呢?我们尝试提取出它进行的操作:
这就是「单调栈」。在单调栈中,从栈底到栈顶的元素是严格单调递减的。当给定阈值 x 时,我们只需要不断地弹出栈顶的元素,直到栈为空或者 x 严格小于栈顶元素。此时我们再将 x 入栈,这样就维护了栈的单调性。
因此,我们可以使用单调栈作为维护 2 的数据结构,并给出下面的算法:
我们用单调栈维护所有可以作为 2 的候选元素。初始时,单调栈中只有唯一的元素 a[n−1]。我们还需要使用一个变量 max_k 记录所有可以真正作为 2 的元素的最大值;
随后我们从 n−2 开始从右到左枚举元素 a[i]:
在枚举完所有的元素后,如果仍未找到满足 132 模式的三元组,那就说明其不存在。
代码
class Solution {
public boolean find132pattern(int[] nums) {
int n = nums.length;
Deque candidateK = new LinkedList();
candidateK.push(nums[n - 1]);
int maxK = Integer.MIN_VALUE;
for (int i = n - 2; i >= 0; --i) {
if (nums[i] < maxK) {
return true;
}
while (!candidateK.isEmpty() && nums[i] > candidateK.peek()) {
maxK = candidateK.pop();
}
if (nums[i] > maxK) {
candidateK.push(nums[i]);
}
}
return false;
}
}
说明 : 方法三思路难度较大,需要在单调栈上进行二分查找。建议读者在完全理解方法二之后,再尝试阅读该方法。
当我们枚举 222 的下标 kkk 时,与方法二相反,从左到右进行枚举的方法是十分合理的:在枚举的过程中,i,j 的下标范围都是增加的。
由于我们需要保证 1<2 并且 2<3,那么我们需要维护一系列尽可能小的元素作为 1 的候选元素,并且维护一系列尽可能大的元素作为 3 的候选元素。
我们可以分情况进行讨论,假设当前有一个小元素 xix_ixi 以及一个大元素 xj 表示一个二元组,而我们当前遍历到了一个新的元素 x=a[k],那么:
如果 x>xj,那么让 x 作为 3 显然是比 x作为 3 更优,因此我们可以用 x 替代 xj;
如果 x 对于其它的情况,xi≤x≤xj,x 无论作为 1 还是 3 都没有当前二元组对应的要优,因此我们可以不用考虑 x 作为 1 或者 3 的情况。 这样一来,与方法二类似,我们使用两个单调递减的单调栈维护一系列二元组 (xi,xj),表示一个可以选择的 1−3 区间,并且从栈底到栈顶 xi 和 x 分别严格单调递减,因为根据上面的讨论,我们只有在 x 然而与方法二不同的是,如果我们想让 x 作为 2,那么我们并不知道到底应该选择单调栈中的哪个 1−3 区间,因此我们只能根据单调性进行二分查找: 在枚举完所有的元素后,如果仍未找到满足 132 模式的三元组,那就说明其不存在。 代码 需要注意的是,我们是在单调递减的栈上进行二分查找,因此大部分语言都需要实现一个自定义比较函数,或者将栈中的元素取相反数后再使用默认的比较函数。 在上面的三种方法中,方法二的时间复杂度为 O(n),最优秀。而剩余的两种时间复杂度为 O(nlogn)的方法中,方法一相较于方法三,无论从理解还是代码编写层面来说都更容易一些。那么为什么还要介绍方法三呢?这里我们可以发现方法一和方法二的不足:
class Solution {
public boolean find132pattern(int[] nums) {
int n = nums.length;
List
复杂度分析
结语:
链接:https://leetcode-cn.com/problems/132-pattern/solution/132mo-shi-by-leetcode-solution-ye89/
来源:力扣(LeetCode)