1. 程式人生 > 程式設計 >從leetcode來重新理解棧

從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