LeetCode #581 Shortest Unsorted Continuous Subarray 数组 单调栈
Description
Given an array of integers where 1 ≤ a[i] ≤ n (n = size of array), some elements appear twice and others appear once.
Find all the elements of [1, n] inclusive that do not appear in this array.
Could you do it without extra space and in O(n) runtime? You may assume the returned list does not count as extra space.
思路
解法一
构造一个已排好序的辅助数组。从该数组开始位置开始往后线性扫描,两两数组相互比较,当数字不同时进行处理,让 start_index
指向要处理的序列的最左端,end_index
指向最右端。最后左右夹逼出来的子数组就是需要被升序排序的子数组。
耗时 116 ms, ranking 5%
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
if (nums.size() < 2) return 0;
vector<int> sorted_nums = nums;
sort(sorted_nums.begin(), sorted_nums.end());
// find the starting position where array need to sort
int start_idx = -1;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] != sorted_nums[i]) {
start_idx = i;
break;
}
}
if (start_idx == -1) return 0;
// find the end position where array need to sort
int end_idx = start_idx;
for (int i = start_idx + 1; i < nums.size(); ++i) {
if (nums[i] != sorted_nums[i]) {
end_idx = i;
}
}
return end_idx - start_idx + 1;
}
};
解法一优化后的解法二
算法思路是:首先从数组开始位置开始,两两数组相互比较,当数字不同时停止。同理,从数组末尾位置开始,再扫描一轮。最终会得到两个 index
,表示无序数据的边界位置(即是最小无序数据、最大无序数据的位置),而中间部分就是最小无序子数组。尽管扫描部分的时间复杂度还是 O(n)
,但耗时会明显下降。
耗时74ms, ranking 20%
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
if (nums.size() < 2) return 0;
vector<int> sorted_nums = nums;
sort(sorted_nums.begin(), sorted_nums.end());
// find the starting position where array need to sort
int start_idx = -1;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] != sorted_nums[i]) {
start_idx = i;
break;
}
}
if (start_idx == -1) return 0;
// find the end position where array need to sort
int end_idx = -1;
for (int i = nums.size() - 1; i >= 0; --i) {
if (nums[i] != sorted_nums[i]) {
end_idx = i;
break;
}
}
return end_idx - start_idx + 1;
}
};
解法三,单调栈
所谓的单调栈 Monotone Stack,就是栈内元素都是单调递增或者单调递减的,有时候需要严格的单调递增或递减。
- 单调递增栈可以找到左起第一个比当前数字小的元素
- 单调递减栈可以找到左起第一个比当前数字大的元素
使用单调栈的一大优势就是线性的时间复杂度,所有的元素只会进栈一次,而且一旦出栈后就不会再进来了。
单调栈是一个看似原理简单,但是可以变得很难的解法。线性的时间复杂度是其最大的优势,每个数字只进栈并处理一次,而解决问题的核心就在处理这块,当前数字如果破坏了单调性,就会触发处理栈顶元素的操作,而触发数字有时候是解决问题的一部分,比如在 Trapping Rain Water 中作为右边界。有时候仅仅触发作用,比如在 Largest Rectangle in Histogram 中是为了开始处理栈顶元素,如果仅作为触发,可能还需要在数组末尾增加了一个专门用于触发的数字。另外需要注意的是,虽然是递增或递减栈,但里面实际存的数字并不一定是递增或递减的,因为我们可以存坐标,而这些坐标带入数组中才会得到递增或递减的数。所以对于玩数组的题,如果相互之间关联很大,那么就可以考虑考虑单调栈能否解题。
比如本题中,单调栈能够找到一个无序数据在递增 / 递减序列中应该要出现的位置。若要保证无序子数组最短,那么就要知道无序数据应该出现的最左、最右位置,而这恰好就是单调栈的性质。
在实现本题的单调栈算法时,进行比较的时候不能只和邻居比,要和最往左 / 右不符合条件的元素比。
值得注意的是,这里的两层循环并不意味着时间复杂度就是 O(n²)
,实际上,Time Complexity = O(n)
, Because each number goes into and goes out stack at most 4 times, so O(4n) = O(n)
耗时52ms, ranking 25%
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
if (nums.size() < 2) return 0;
stack<int> stk; // save indexes of ascending sub-order
// find far left position where array need to sort
int start_idx = INT_MAX;
for (int i = 0; i < nums.size(); ++i) {
while (!stk.empty() && nums[stk.top()] > nums[i]) {
start_idx = min(start_idx, stk.top());
stk.pop();
}
stk.push(i);
}
// all elements are in ascending order
if (start_idx == INT_MAX) return 0;
// clear stack and let it save indexes of ascending sub-order
while(!stk.empty()) stk.pop();
// find far right position where array need to sort
int end_idx = -1;
for (int i = nums.size()-1; i >= 0; --i) {
while (!stk.empty() && nums[stk.top()] < nums[i]) {
end_idx = max(end_idx, stk.top());
stk.pop();
}
stk.push(i);
}
return end_idx - start_idx + 1;
}
};
参考
- 《LeetCode Monotone Stack Summary 单调栈小结》:https://www.cnblogs.com/grandyang/p/8887985.html
- 《油管视频LeetCode 581. Shortest Unsorted Continuous Subarray》:https://www.youtube.com/watch?v=8wHH9txAK34
- 《【最短无序连续子数组】单调栈》:https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/solution/shortest-unsorted-continuous-subarray-by-ikaruga/
- 《C++ 单调栈,击败 88%》:https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/solution/c-dan-diao-zhan-ji-bai-88-by-fxxuuu/