从leetcode来重新理解栈

584 阅读8分钟

栈!

为什么要用栈来解决问题? 什么时候,哪些场景需要用到栈? 用栈怎么简化算法,降低时间复杂度? 用栈降低时间复杂度,解决问题的关键是什么?

我在解题的过程中,总会很自然的想到这些问题。

要理解以上问题,我们不妨再回顾一下栈的特性。

正常循环的情况下,数组的滚动(游标移动)是向后的,引入栈的时候,则可以有了向前滚动的机会(有了一定的反悔的机会),然后这样子就能够解决一些局部的问题(比如说,寻找相邻的大的数字)。由于栈还可以对于没有价值(已经发现了大的数字)的东西删除,这样子的遗忘功能,简化了搜索空间,问题空间。

毫无疑问,当一个算法完全不进行多余的运算,那么它是一个时间复杂度最低的算法。但我们往往会对一些结果进行重复的计算,那么栈的引入就是为了解决这样的问题,栈存储了一些重要的运算结果,用于和接下来的元素进行比较

具体来说,我认为解题的关键在于以下几个点:

  • 入栈应该维持一个怎样的顺序(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,5,6,2,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,1,5},result = 2

(4)6比5大,满足升序条件,进栈。s={1,1,5,6},result = 2

(5)2比6小,不满足升序条件,因此将6弹出,并记录当前结果为6*1=6。s={1,1,5},result = 6

2比5小,不满足升序条件,因此将5弹出,并记录当前结果为5*2=10(因为已经弹出的5,6是升序的)。s={1,1},result = 10

2比1大,将弹出的5,6替换为2重新进栈。s={1,1,2,2,2},result = 10

(6)3比2大,满足升序条件,进栈。s={1,1,2,2,2,3},result = 10

栈构建完成,满足升序条件,因此按照升序处理办法得到上述的max(height[i]*(size-i))=max{3*1, 2*2, 2*3, 2*4, 1*5, 1*6}=8<10