数据结构和算法(四)——栈及其相关算法

简介

栈是一种运算受限的线性表,只允许在一端(栈顶)插入和删除数据。栈主要包含两个操作,入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据,其时间复杂度为O(1)。

栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈

常见算法思路

实现一个栈

示例1: 顺序栈

代码如下(如果之前没写过,最好自己写一次):

 class SequentialStack{

          int capacity = 20;

          int[] stack = new int[capacity];

          int size = -1;

          public int pop(){
              if (size < 0)throw new RuntimeException();
              int res = stack[size];
              size--;
              return res;
          }

          public void push(int val){
              size++;
              if (size >= stack.length){
                  capacity *= 2;
                  int[] temp = new int[capacity];
                  for (int i = 0; i <stack.length ; i++) {
                      temp[i] = stack[i];
                  }
                  stack = temp;
              }
              stack[size] = val;
          }
      }

示例2: 链式栈

代码如下(如果之前没写过,最好自己写一次):

class LinkedListStack{
          class Node{
              Node next;
              Node before;
              int val;
              public Node(int val) {
                  this.val = val;
              }
          }

          Node top = null;

          public int pop(){
              if (top == null)throw new RuntimeException();
              Node node = top;
              top = top.before;
              return node.val;
          }

          public void push(int val){
              if (top == null){
                  top = new Node(val);
                  return;
              }
              Node node = new Node(val);
              top.next = node;
              node.before = top;
              top = node;
          }
      }

示例3: 包含min函数的栈

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 
min、push 及 pop 的时间复杂度都是 O(1)。

算法思路:使用一个辅助栈来保存最小值。当push元素时,如果辅助栈为空则直接插入元素;如果辅助栈不为空,则用push的元素x与辅助栈栈顶元素y进行比较,如果y <= x,则将y插入辅助栈中。当pop元素时,如果pop元素x与辅助栈的栈顶元素y相等,则移除辅助栈中的栈顶元素。这样就可以通过O(1)来实现min函数。

原理:由于栈是先进先出的,仍然保存在辅助栈中的栈顶元素一定未出栈,且是栈中的最小值,所以只需要通过返回辅助栈的栈顶元素就可以用O(1)复杂度实现min函数

代码如下:

class MinStack {

    int[] data;
    int[] min;
    int dataIndex = 0;
    int minIndex = 0;
    int dataCapcity = 100; 
    int minCapcity = 100;

    /** initialize your data structure here. */
    public MinStack() {
        data = new int[dataCapcity];
        min = new int[minCapcity];
    }
    
    public void push(int x) {
       if(dataIndex >= dataCapcity){
           int[] cache = new int[dataCapcity*2];
           dataCapcity *= 2;
           for(int i = 0;i < data.length;i++)
              cache[i] = data[i];
            data = cache;  
       }

       if(minIndex >= minCapcity){
           int[] cache = new int[minCapcity*2];
           minCapcity *= 2;
           for(int i = 0;i < min.length;i++)
              cache[i] = min[i];
            min = cache;  
       }
       data[dataIndex] = x;
       dataIndex++;
       if(minIndex == 0||min[minIndex - 1] >= x){
           min[minIndex] = x;
           minIndex++;
       }
    }
    
    public void pop() {
        if(dataIndex == 0)return;
        dataIndex--;
        if(data[dataIndex] == min[minIndex-1]){
            minIndex--;
        } 
    }
    
    public int top() {
       return data[dataIndex-1];
    }
    
    public int min() {
      return min[minIndex-1];
    }
}

表达式求值

中缀表达式和后缀表达式(逆波兰表达式)

像一般的A*(B - C)这种表达式叫做中缀表达式,之所以叫中缀表达式,是因为它是由相应的语法树的中序遍历的结果得到的。同理后缀表达式(逆波兰表达式)是由相应的语法树的后序遍历的结果得到的

后缀表达式(逆波兰表达式)主要有以下两个优点:

  1. 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  2. 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中

示例

例1:逆波兰表达式求值

根据 逆波兰表示法,求表达式的值。
有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

说明:

整数除法只保留整数部分。
给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

示例1:
输入: ["2", "1", "+", "3", "*"]
输出: 9
解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例2:
输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

示例3:
输入: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]
输出: 22
解释: 
该算式转化为常见的中缀算术表达式为:
  ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

代码如下(来源leetcode的hteason):

class Solution {
	public static int evalRPN(String[] tokens) {
		Stack<Integer> numStack = new Stack<>();
		Integer op1, op2;
		for (String s : tokens) {
			switch (s) {
			case "+":
				op2 = numStack.pop();
				op1 = numStack.pop();
				numStack.push(op1 + op2);
				break;
			case "-":
				op2 = numStack.pop();
				op1 = numStack.pop();
				numStack.push(op1 - op2);
				break;
			case "*":
				op2 = numStack.pop();
				op1 = numStack.pop();
				numStack.push(op1 * op2);
				break;
			case "/":
				op2 = numStack.pop();
				op1 = numStack.pop();
				numStack.push(op1 / op2);
				break;
			default:
				numStack.push(Integer.valueOf(s));
				break;
			}
		}
		return numStack.pop();
	}
}

例2:中缀表达式转化为后缀表达式(逆波兰表达式)

具体算法见中缀表达式转换为后缀表达式

代码如下:

      public static void change(String[] data){
        Stack<String> stack = new Stack<>();
        List<String> result = new ArrayList<>(data.length);
        int index = 0;
        while (index < data.length){
            switch (data[index]){
                case "("://见 ( 直接导入栈
                    stack.push(data[index]);
                    break;
                case "+"://栈为空直接入栈 ;遇到 ( 之前要将栈中元素pop到result,之后入栈
                case "-":
                    if(stack.isEmpty()){
                        stack.push(data[index]);
                    }else {
                        while((!stack.isEmpty())&&(!stack.peek().equals("("))){
                                result.add(stack.pop());
                        }
                        stack.push(data[index]);
                    }
                    break;
                case "*"://栈为空直接入栈 ;遇到 (+- 
                case "/"://之前要将栈中元素pop到result,之后入栈
                    if(stack.isEmpty()){
                        stack.push(data[index]);
                    }else {
                        String item = stack.peek();
                        while((!stack.isEmpty())&&(item.equals("*")||item.equals("/"))){
                            result.add(stack.pop());
                            item = stack.peek();
                        }
                        stack.push(data[index]);
                    }
                    break;
                case ")"://( )之间的元素 pop到result
                    String str = null;
                    while(!(str = stack.pop()).equals("(")){
                        result.add(str);
                    }
                    break;
                default://数字直接放到 result
                    result.add(data[index]);
                    break;
            }
            index++;
        }
        while (!stack.isEmpty()){//将栈中元素全部放到result
            result.add(stack.pop());
        }
          System.out.println(result.toString());
      }

括号匹配

示例1:有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。

示例 1:
输入: "()"
输出: true

示例 2:
输入: "()[]{}"
输出: true

示例 3:
输入: "(]"
输出: false

代码如下:

import java.util.Stack;
class Solution {
    Stack<Character> stack = new Stack<>();
    public boolean isValid(String s) {
        if(s.length()==0)
            return true;
        if(s.length()%2 == 1)return false;    
        char[] n = s.toCharArray();
        for(int i=0;i<n.length;i++){
            char m=n[i];
            switch(m){
                case '}':
                    if(!stack.isEmpty()&&stack.pop()!='{')
                        return false;
                    break;
                case ']':
                    if(!stack.isEmpty()&&stack.pop()!='[')
                        return false;
                    break;
                case ')':
                    if(!stack.isEmpty()&&stack.pop()!='(')
                        return false;
                    break; 
                    default:
                    stack.push(m);
            }
            
        }
        if(stack.size()!=0)
            return false;
        return true;
    }
}

必须掌握的代码实现

  • 用数组实现一个顺序栈
  • 用链表实现一个链式栈
  • 编程模拟实现一个浏览器的前进、后退功能

面试常考的与栈相关的算法题

参考

  • leetcode
  • 牛客网
  • 《剑指offer》
  • 极客时间的《数据结构与算法之美》专栏
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值