題目地址:
https://leetcode.com/problems/132-pattern/
給定一個數組,問這個數組中是否存在三個數滿足並且。
法1:模擬出棧。我們可以將數組從大到小排列,然後按照這個順序進棧,看看能不能模擬出某個出棧順序剛好等於原數組。如果能模擬出來,說明不存在,否則說明存在。代碼如下:
import java.util.*;
class Solution {
public boolean find132pattern(int[] nums) {
// 如果數組空或者長度不足3,顯然沒有
if (nums == null || nums.length < 3) {
return false;
}
// 接下來把數組中數字存到一個list裏然後從大到小排序
List<Integer> list = new ArrayList<>();
for (int num : nums) {
list.add(num);
}
list.sort((x, y) -> -Integer.compare(x, y));
// 接着開始模擬出棧順序
Deque<Integer> stack = new ArrayDeque<>();
int idx = 0;
for (int i = 0; i < list.size(); i++) {
stack.push(list.get(i));
// 如果發現棧頂數字與數組中數字匹配,就將其出棧,並把下標移動一位
while (!stack.isEmpty() && stack.peek() == nums[idx]) {
stack.pop();
idx++;
}
}
// 如果數組空了,說明原序列可以構成出棧序列,即沒有132模式,返回false,否則返回true
return !stack.isEmpty();
}
}
時間複雜度,空間。
算法正確性證明:
先證明一個結論,對於入棧序列,那麼其出棧的序列(意思是將按順序進棧,進棧中途可以讓數字出棧,並且出棧順序是),一定不會存在但是。並且這個結論的反面也是成立的,如果某個序列,滿足對任意,一定不成立,那麼這個序列一定是某個出棧的順序。
舉個例子來說,就是如果入棧順序是,那麼這三個數所有的排列中,唯一不可能是出棧序列的是。原因是,如果要出現這個序列的話,必須得先出棧並且出棧後棧裏仍然還有兩個數字,也就是說在進棧的時候,必須中途不能出棧,那麼進棧完畢後棧是(左邊是棧底,右邊棧頂)。此時先出棧,接下來的出棧順序一定是先出來,所以的順序出棧是不可能的。通過枚舉可以驗證別的排列都可能成爲出棧的順序。
接下來證明上面的結論。
首先證明必要性,與上面的證明類似,若某個出棧順序是,則一定不會存在但是。
其次證明充分性,如果的某個排列,對任意,一定不成立,我們來證明它可以構成出棧序列。用數學歸納法。對於的情況,可以通過暴力枚舉證明。假設對結論也成立,當時,入棧序列是,我們構造出棧序列。由於最先出棧,所以小於的數字必須先全部入棧,棧爲,然後率先出棧構成出棧序列的第一個數。接下來在序列裏去尋找這個數,設整個序列是,由於不允許出現"“這個模式,所以從到,這些數都是大於的(否則就是一個”“模式,其中),也就是在之後入棧。然而我們還可以證明(此處的意思是作爲集合相等),如若不然,就會存在,再接上裏面的任意一個數就會產生”"模式,與假設矛盾。如此一來就有,接下來用歸納假設,這個序列是可以構造爲出棧序列的,直到把彈出後,繼續用類似的證明方法,就可以證明整個序列是可構造的。由數學歸納法知結論正確。
要說明算法的正確性只需要將上面證明結論裏的入棧順序顛倒過來即可。
法2:單調棧。考慮數組,那麼對於兩個數和並且來講,如果有,我們需要儘可能的在中找最小值來構成模式。所以我們想到用一個數組使得存放,然後從後向前check有沒有模式。同時用一個單調非嚴格(嚴格也可以做,代碼裏多一步判斷即可,但複雜度不會有變化)遞減的棧存儲“”的候選者,然後從後向前遍歷數組來枚舉“”。
具體思路是,從後向前遍歷數組,然後將和作比較。如果,那麼不可能成爲“”,也不可能成爲“”,原因是之前的數都小於等於,所以直接略過;如果,那麼此時就要考察棧裏有沒有作爲“”的數字,所以先把小於等於的全pop掉,如果能找到小於的數,那就找到了模式,返回true;如果棧空或者棧頂數已經大於等於,那麼由於棧單調,棧裏都不可能存在“”,所以就將進棧。如此遍歷數組直到找到答案,或找不到則最後返回true。
現在給出算法的簡略證明。算法的本質是在枚舉,我們只需要證明棧裏面存儲的都是“”的候選者即可。現在從數組後向前開始遍歷。首先,如果發生,由於比其前面的數字都要小於等於,所以不可能成爲“”,也不可能成爲“”,所以可以略過。如果發現了第一個,此時棧裏爲空,所以不能作爲“”出現,但是仍然有可能作爲“”出現,所以先入棧。接着如果發現了第二個,此時如果棧頂數小於等於,那麼這個數就會小於所有以及其之前的數,所以它就永遠不會作爲“”出現,所以可以出棧;如果其正好在和之間,那就找到了;如果其大於等於,那麼說明不可能作爲“”出現了,因爲其後面能作爲""出現的唯一候選者不滿足條件,然而還是有可能作爲“”出現,所以將其進棧。我們可以看出,對於每一種情況,棧裏始終存放的都是能作爲“”的候選者,而且棧恰好是從棧底到棧頂是(非嚴格)遞減排序的。所以說,如果數組中存在“”模式的話,當遍歷到第一個可以作爲“”的數字時,棧裏的數字已經排好序了,只需驗出那個作爲“”的數即可。也就是說,如果數組中存在“”模式的話,算法一定找到並且會返回true。所以算法正確。
代碼如下:
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
public boolean find132pattern(int[] nums) {
// 判斷顯然不存在的情形
if (nums == null || nums.length < 3) {
return false;
}
Deque<Integer> stack = new ArrayDeque<>();
// 開始算前綴min
int[] min = new int[nums.length];
min[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
min[i] = Math.min(min[i - 1], nums[i]);
}
// 從後向前遍歷數組
for (int j = nums.length - 1; j >= 0; j--) {
if (nums[j] > min[j]) {
while (!stack.isEmpty() && stack.peek() <= min[j]) {
stack.pop();
}
if (!stack.isEmpty() && stack.peek() < nums[j]) {
return true;
}
stack.push(nums[j]);
}
}
return false;
}
}
時空複雜度。