從leetcode來重新理解棧
棧!
為什麼要用棧來解決問題? 什麼時候,哪些場景需要用到棧? 用棧怎麼簡化演演算法,降低時間複雜度? 用棧降低時間複雜度,解決問題的關鍵是什麼?
我在解題的過程中,總會很自然的想到這些問題。
要理解以上問題,我們不妨再回顧一下棧的特性。
正常迴圈的情況下,陣列的滾動(遊標移動)是向後的,引入棧的時候,則可以有了向前滾動的機會(有了一定的反悔的機會),然後這樣子就能夠解決一些區域性的問題(比如說,尋找相鄰的大的數字)。由於棧還可以對於沒有價值(已經發現了大的數字)的東西刪除,這樣子的遺忘功能,簡化了搜尋空間,問題空間。
毫無疑問,當一個演演算法完全不進行多餘的運算,那麼它是一個時間複雜度最低的演演算法。但我們往往會對一些結果進行重複
具體來說,我認為解題的關鍵在於以下幾個點:
- 入棧應該維持一個怎樣的順序(Ascending?Descending?)(重中之重)
- 出棧時調整結果的策略
- 遍歷的方向(從左到右?從右到左?)
leetcode關於stack
的題蠻多的,拿下面這些題來作例子吧。
456. 132 Pattern
bool find132pattern(int* nums,int numsSize) {
if(numsSize < 3)
return 0;
int* stack = (int*)malloc(numsSize * sizeof(int));
int top = -1;
int* min = (int*)malloc(numsSize * sizeof(int));
min[0] = nums[0];
for (int i = 1; i < numsSize; ++i)
min[i] = min[i-1] < nums[i] ? min[i -1] : nums[i];
for (int j = numsSize - 1; j >= 0; j--)
{
if (nums[j] > min[j]){
while(top != -1 && stack[top] <= min[j])
top--;
if(top != -1 && nums[j] > stack[top])
return true;
stack[++top] = nums[j];
}
}
free(stack);
free(min);
return 0;
}
複製程式碼
先來一趟遍歷,對於每個i
,找到到i為止最小的元素,並儲存為min[i]
。
從右向左遍歷,對每個有潛在可能成為132
模式的j
(滿足num[j] > min[j]
),不斷彈出,判斷是否存在比num[j]
更小的元素,如果有,那麼找到了132
.如果遇到一個棧頂元素大於 num[j]
就應該停止,因為棧內的其他元素都將比num[j]
大,此時入棧,維護了這個遞增的序列。可以這麼說,這個棧保留了這個潛在可能的j
右側的所有元素,並且由棧頂到棧底是一個遞增的序列。
735. Asteroid Collision
int* asteroidCollision(int* asteroids,int asteroidsSize,int* returnSize) {
int* stack = (int*)malloc(asteroidsSize * sizeof(int));
int top = -1;
for (int i = 0; i < asteroidsSize; ++i)
{
if(top != -1 && asteroids[i] < 0 && stack[top] > 0){
if(abs(asteroids[i]) > abs(stack[top])){
while(abs(asteroids[i]) > abs(stack[top]) && top != -1){
if(stack[top] < 0)
break;
top--;
}
if(top == -1 || stack[top] < 0){
stack[++top] = asteroids[i];
continue;
}
}
if(abs(asteroids[i]) == abs(stack[top]))
top --;
}
else
stack[++top] = asteroids[i];
}
*returnSize = top + 1;
int* ret = (int*)malloc((*returnSize) * sizeof(int));
for (int i = 0; i < *returnSize; ++i)
ret[i] = stack[i];
free(stack);
return ret;
}
複製程式碼
在新增一個元素前,之前的序列已經穩定。是一個stable
的序列。
我們直接用一個stack
來儲存已經穩定的序列,讓它從空棧開始新增元素。
只有當前元素為負值,上一個元素為正值時它會發生爆炸。
在這種情況下:如果當前元素絕對值大於前一個元素 ,也就是棧頂元素,那麼棧頂元素彈出,需要注意的是,如果推進時棧頂元素小於零,那麼停止彈出,序列已經穩定,這時將當前元素push進棧。
42. Trapping Rain Water
int trap(int* height,int heightSize) {
int* stack = (int*)malloc(heightSize * sizeof(int));
int top = -1;
int ans = 0;
for (int i = 0; i < heightSize; ++i){
while(top != -1 && height[i] > height[stack[top]]){
int bottom = stack[top--];
//the very biginning can not trap rain
if(top == -1)
break;
int bar = min(height[i],height[stack[top]]) - height[bottom];
//if bar == 0,process forward,distance would increase
int distance = i - stack[top] - 1;
ans += distance * bar;
}
stack[++top] = i;
}
free(stack);
return ans;
}
複製程式碼
一趟遍歷,我們維護一個由棧底到棧頂遞增的棧,當遇到當前元素高於棧頂元素時我們計算蓄水量並彈出棧內元素,當彈出所有小於當前元素時,入棧當前元素,保持棧內的順序。
394. Decode String
class Solution {
public String decodeString(String s) {
Stack<Integer> countStack = new Stack<>();
Stack<String> resStack = new Stack<>();
int idx = 0;
int count = 0;
String res = "";
while(idx < s.length()){
count = 0;
if(Character.isDigit(s.charAt(idx))){
while(Character.isDigit(s.charAt(idx)))
count = 10 * count + s.charAt(idx++) - '0';
countStack.push(count);
}else if(s.charAt(idx) == '['){
resStack.push(res);
res = "";
idx++;
}else if(s.charAt(idx) == ']'){
int repeatTimes = countStack.pop();
StringBuilder sb = new StringBuilder(resStack.pop());
for (int i = 0; i < repeatTimes; i++)
sb.append(res);
res = sb.toString();
idx++;
}else{
res += s.charAt(idx++);
}
}
return res;
}
}
複製程式碼
兩個棧分別處理重複次數和字串
遇到[
時,將上一個res入棧,讓res重置;
遇到]
時,將count彈出,並藉助sb來給現有的res新增重複元;
遇到最後一個]
後,res即為完整的解碼字串;
224. Basic Calculator
int isDigit(char c){
int dis = c - '0';
return (dis >= 0 && dis <= 9) ? 1 : 0;
}
int calculate(char* s) {
int n = strlen(s);
int result = 0;
int number = 0;
int sign = 1;
int top = -1;
// we don't need to push if no '(' contained
//else we push n / 2 times without pop at most
//so set the size to half of the length
int* stack = (int*)malloc(n / 2 * sizeof(int));
for (int i = 0; i < n; ++i)
{
char c = s[i];
if(isDigit(c))
number = 10 * number + c - '0';
else{
switch(c){
case '+':
result += sign * number;
sign = 1;
number = 0;
break;
case '-':
result += sign * number;
sign = -1;
number = 0;
break;
case '(':
stack[++top] = result;
stack[++top] = sign;
//reset
result = 0;
sign = 1;
break;
case ')':
result += sign * number;
//firt-sign,second-result before
result *= stack[top--];
result += stack[top--];
number = 0;
break;
default:
break;
}
}
}
if(number != 0)
result += sign * number;
free(stack);
return result;
}
複製程式碼
與上一題類似的,我們的stack只儲存這一層括號的結果和這一層之前的符號,遇到其他運運算元先處理之前的number,並讓符號變化。
636. Exclusive Time of Functions
int* getParseLog(char const * s){
int* ret = (int*)calloc(3,sizeof(int));
while(*s != ':'){
ret[0] = 10 * ret[0] + *s - '0';
s ++;
}
s ++;
if(*s == 's'){
ret[1] = 1;
s += 6;
}else{
ret[1] = 0;
s += 4;
}
while(*s != '\0'){
ret[2] = 10 * ret[2] + *s - '0';
s ++;
}
return ret;
}
int* exclusiveTime(int n,char** logs,int logsSize,int* returnSize) {
*returnSize = n;
int* ret = (int*)calloc(n,sizeof(int));
int* stack = (int*)malloc((logsSize / 2) * sizeof(int));
int top = -1;
int pre = 0;
int* log;
for (int i = 0; i < logsSize; ++i)
{
log = getParseLog(logs[i]);
if(log[1] == 1){
if(top != -1)
ret[stack[top]] += log[2] - pre;
stack[++top] = log[0];
pre = log[2];
}else{
ret[stack[top]] += log[2] - pre + 1;
top--;
pre = log[2] + 1;
}
free(log);
}
free(stack);
return ret;
}
複製程式碼
我們先定義一個函式解析我們每一個log字串,三個位置分別為id,start/end flag,time
。
我們的棧儲存每個執行函式的id
,每次遇到一個函式的開始,我們讓棧頂id
對應的函式,也就是這個函式的主調函式的執行時間加上它開始時間與pre
的差值,這個差值實際上就是外層函式的獨佔時間。接著入棧當前函式id
,推進pre
,讓它與當前時間相同。再遍歷下一個log
。
當遇到一個函式結束時,此時棧頂元素是與這個函式匹配的,意味著這個函式的生命週期結束 ,增加它的獨佔執行時間,並出棧,保證我們的棧內只儲存那些生命週期還未結束的函式。推進pre
到當前時間+1s
84. Largest Rectangle in Histogram
int largestRectangleArea(int* heights,int heightsSize) {
int* stack = (int*)malloc(heightsSize * sizeof(int));
int top = -1;
int maxArea = 0;
int count = 0;
for (int i = 0; i < heightsSize; ++i)
{
count = 0;
//ensure ascending sequence in the stack
while(top != -1 && heights[i] < stack[top]){
int t = stack[top--];
count++;
maxArea = max(t * count,maxArea);
}
//push the element replaced by heights[i] popped just before
while(count--)
stack[++top] = heights[i];
//push the current element
stack[++top] = heights[i];
}
//calculate the rest in the stack
count = 0;
while(top != -1){
int t = stack[top--];
count++;
maxArea = max(t * count,maxArea);
}
return maxArea;
}
複製程式碼
1、如果已知height陣列是升序的,應該怎麼做?
比如1,2,5,7,8
那麼就是(1*5) vs. (2*4) vs. (5*3) vs. (7*2) vs. (8*1)
也就是max(height[i]*(size-i))
2、使用棧的目的就是構造這樣的升序序列,按照以上方法求解。
但是height本身不一定是升序的,應該怎樣構建棧?
比如2,1,6,3
(1)2進棧。s={2},result = 0
(2)1比2小,不滿足升序條件,因此將2彈出,並記錄當前結果為2*1=2。
將2替換為1重新進棧。s={1,1},result = 2
(3)5比1大,滿足升序條件,進棧。s={1,5},result = 2
(4)6比5大,滿足升序條件,進棧。s={1,6},result = 2
(5)2比6小,不滿足升序條件,因此將6彈出,並記錄當前結果為6*1=6。s={1,result = 6
2比5小,不滿足升序條件,因此將5彈出,並記錄當前結果為5*2=10(因為已經彈出的5,6是升序的)。s={1,result = 10
2比1大,將彈出的5,6替換為2重新進棧。s={1,2},result = 10
(6)3比2大,滿足升序條件,進棧。s={1,3},result = 10
棧構建完成,滿足升序條件,因此按照升序處理辦法得到上述的max(height[i]*(size-i))=max{3*1,2*2,2*3,2*4,1*5,1*6}=8<10