算法学习--栈

本文深入探讨了栈数据结构的应用,通过实现一个具有min函数的栈,解决了一般问题,并介绍了数组最大矩形计算的两种高效算法。此外,还详细解释了中缀表达式转为后缀表达式的过程以及寻找下一个较大元素的方法,提供了实用的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

包含min函数的栈

题目:定义栈的数据结构,要求添加一个min函数,能够得到栈的最小元素。要求函数min、push以及pop的时间复杂度都是O(1)。

分析:google的一道面试题。我看到这道题目时,第一反应就是每次push一个新元素时,将栈里所有逆序元素排序。这样栈顶元素将是最小元素。但由于不能保证最后push进栈的元素最先出栈,这种思路设计的数据结构已经不是一个栈了。

在栈里添加一个成员变量存放最小元素(或最小元素的位置)。每次push一个新元素进栈的时候,如果该元素比当前的最小元素还要小,则更新最小元素。

乍一看这样思路挺好的。但仔细一想,该思路存在一个重要的问题:如果当前最小元素被pop出去,如何才能得到下一个最小元素?

注意读题:对空间复杂的没有特别的要求,完全可以借助辅助空间!

因此仅仅只添加一个成员变量存放最小元素(或最小元素的位置)是不够的。我们需要一个辅助栈。每次push一个新元素的时候,同时将最小元素(或最小元素的位置。考虑到栈元素的类型可能是复杂的数据结构,用最小元素的位置将能减少空间消耗)push到辅助栈中;每次pop一个元素出栈的时候,同时pop辅助栈。

class MinStack {
    private Stack<Integer> stack = new Stack<>();
    private Stack<Integer> min = new Stack<>();
    public void push(int x) {
        stack.push(x);
        //当x > min.peek() 时,x可不用放进min,因为在stack中,此时min.peek()比x先进,必然比x后出,所以,若将来弹出x后,最小不会变
        if(min.isEmpty() || min.peek() >= x){
            min.push(x);
        }
    }

    public void pop() {
        //对象要用object
        if(stack.peek().equals(min.peek())){
            stack.pop();
            min.pop();
        }
        else{
            stack.pop();
        }
    }

    public int top() {
        return stack.peek();
    }

    public int getMin() {
        return min.peek();
    }
}

数组的最大矩形

Given n non-negative integers representing the histogram’s bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.

histogram

Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3].

histogram_area

The largest rectangle is shown in the shaded area, which has area = 10 unit.

For example,
Given height = [2,1,5,6,2,3],
return 10.

解法一

使用动态规划,用left[i]表示第i个柱子可以最多向左延伸至第left[i]个柱子,形成一个矩形,right[i]则表示向右延伸。遍历两次,分别计算出这两个数组。

再遍历一次,即可求出所有的柱子可以形成的最大的矩形面积。为了减少边界的判断,可以使用哨兵,在两端添加两个柱子高度都为-1.

 public static int getMaxRectangle (int heights[]){
        int ans = 0;
        int n = heights.length;
        int left[] = new int[n+1];
        int right[] = new int[n+1];
        processLR(heights, left, right);
        for(int i=1; i<=n; i++){
            int tmp = (right[i]-left[i]+1) * heights[i-1];
            if( ans < tmp)
                ans = tmp;
        }
        return ans;
    }
    public static void processLR(int heights[], int left[], int right[]){
        int n = heights.length;
        //用临时数组,设置两个哨兵
        int tempArr[] = new int[n+2];
        tempArr[0] = -1;
        for(int i=1; i<=n; i++) tempArr[i] = heights[i-1];
        tempArr[tempArr.length-1] = -1;
        for(int i=1; i<=n; i++){
            int k = i;
            while( tempArr[i] <= tempArr[k-1])
                k = left[k-1];
            left[i] = k;
        }
        for(int i=n; i>0; i--){
            int k = i;
            while(  tempArr[i] <= tempArr[k+1])
                 k = right[k+1];
            right[i] = k;
        }
    }

方法二

在网上发现另外一个使用一个栈的O(n)解法,代码非常简洁,栈内存储的是高度递增的下标。对于每一个直方图高度,分两种情况。1:当栈空或者当前高度大于栈顶下标所指示的高度时,当前下标入栈。否则,2:当前栈顶出栈,并且用这个下标所指示的高度计算面积。而这个方法为什么只需要一个栈呢?因为当第二种情况时,for循环的循环下标回退,也就让下一次for循环比较当前高度与新的栈顶下标所指示的高度,注意此时的栈顶已经改变由于之前的出栈。

public int largestRectangleArea(int[] height) {
  // Start typing your Java solution below
  // DO NOT write main() function
  int area = 0;
  java.util.Stack<Integer> stack = new java.util.Stack<Integer>();
  for (int i = 0; i < height.length; i++) {
    if (stack.empty() || height[stack.peek()] < height[i]) {
      stack.push(i);
    } else {
      int start = stack.pop();
      int width = stack.empty() ? i : i - stack.peek() - 1;
      area = Math.max(area, height[start] * width);
      i--;
    }
  }
  while (!stack.empty()) {
    int start = stack.pop();
    int width = stack.empty() ? height.length : height.length - stack.peek() - 1;
    area = Math.max(area, height[start] * width);     
  }
  return area;
}

中缀表达式转为后缀表达式

题目:给一个中缀表达式(即标准形式的表达式),打印该表达式的后缀表达式。

中缀表达式(Infix Notation)就是常用的将操作符放在操作数中间的算术表达式。前缀表达式和后缀表达式相对于中缀表达式最大的不同就是去掉了表示运算符优先级的括号。

中缀表达式对应于二叉树的中序遍历,后缀表达式对应于二叉树的后序遍历。

例子:1)   a+b*c+(d*e+f)*g,输出:abc*+de*f+g*+

2) X=A+B*(C-D)/E  , 输出:XABCD-*E/+=

算法描述如下:

1.   从左向右扫遍历表达式:
2.   If 当前遍历到的字符 ch 是操作数,则打印
3.   Else If 当前遍历的ch是 ‘(‘, 入栈
4.   Else If 当前遍历的ch是 ‘)’, 不断弹出栈顶元素,直到栈为空或弹出’(‘
5.   Else,
…….5.1 If  上一个操作符的优先级比当前操作符ch的优先级小,或栈是空的就入栈。
……. 5.2 Else, 不断弹出栈顶的操作符,并打印,直到栈为空或当前的操作符ch的优先级大于栈顶的操作符。将当前操作符入栈。
6.  重复2-6步,直到遍历完成
7.  弹出并打印栈中的剩余的操作符

public class InfixPostfix {
    //判断字符ch是否是一个操作数
    static boolean isOperand(char ch) {
        return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
    }
    public static void infixToPostfix(String exp) {
        char expArr[] = exp.toCharArray();
        Stack<Character> stack = new Stack<Character>();
        //1.  从左向右扫遍历达式
        for(int i=0; i<expArr.length; i++){
            char ch = expArr[i];
            //2.  If 当前遍历到的字符 ch 是操作数,则打印
            if( isOperand(ch) ){
                System.out.print(ch);
            }
            //Else If 当前遍历的ch是 ‘(‘, 入栈
            else if(ch == '('){
                stack.push(ch);
            }
            //Else If 当前遍历的ch是 ‘)’, 不断弹出栈顶元素,直到栈为空或弹出'('
            else if(ch == ')'){
                char top = stack.pop();
                while(top != '('){
                    System.out.print(top);
                    top = stack.pop();
                }
            }
            else{
                int r = getRank(ch);
                //5.1 If  上一个操作符的优先级比操作符ch的优先级小,或栈是空的就入栈。
                if(stack.isEmpty() || r > getRank(stack.peek())){
                    stack.push(ch);
                }else{
                    //5.2 Else, 不断弹出栈顶的操作符,并打印,直到栈为空或当前的操作符ch的优先级大于栈顶的操作符。将当前操作符入栈。
                    while( !stack.isEmpty() && r <=getRank(stack.peek()) )
                        System.out.print(stack.pop());
                    stack.push(ch);
                }
            }
        }
        // 7.  弹出并打印栈中的剩余的操作符
        while(!stack.isEmpty()) System.out.print(stack.pop());
        System.out.println();
    }
    static int getRank(char c) {
        switch (c) {
        case '+':
        case '-':
            return 1;
        case '*':
        case '/':
            return 2;
        case '^':
            return 3;
        }
        return -1;
    }
    public static void main(String[] args) {
        String exp = "X=A+B*(C-D)/E";
        infixToPostfix(exp);
        String exp2 = "a+b*(c^d-e)^(f+g*h)-i";
        infixToPostfix(exp2);
    }
}

寻找下一个较大元素

这是一道亚马逊的面试题。给定一个数组,打印出每一个元素的下一个更大的元素( Next Greater Element,NGE ),就叫做NGE问题吧。

一个元素x的下一个更大的元素是指在x的右边第一个比该元素大的元素。如果没有更大的元素,则输出-1。
例如:

1 a) 任何数组,最右边的元素的NGE为-1。
2 b) 对于一个降序排序的数组,所有元素得到NGE为-1
3 c) 对于数组{4, 5, 2, 25}, NGE分别为 5,25,25,-1
4 d) 对于数组{13, 7, 6, 12}, NGE分别为 -1,12,12,-1

方法1 枚举

使用二重循环,分别遍历所有的元素,即可找到该元素的NGE。是否复杂度O(n^2).

方法2 使用栈

从后向前遍历数组,维护一个递减的栈
	public void NEG(int [] arr){
		Stack<Integer> stack = new Stack<>();
		for(int i = arr.length-1;i >= 0;i--){
			if(stack.isEmpty()){
				System.out.print("-1 ");
				
			}
			else{
				while(!stack.isEmpty() && arr[i] >= (int)stack.peek()){
					stack.pop();
				}
				if(stack.isEmpty()){
					System.out.print("-1 ");
				}
				else{
					System.out.print(stack.peek()+" ");
				}
			}
			stack.push(arr[i]);
		}
	} 

注意:打印的顺序也是从后往前的。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值