简介
栈是一种运算受限的线性表,只允许在一端(栈顶)插入和删除数据。栈主要包含两个操作,入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据,其时间复杂度为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 2 + 3 4 + * 也可以依据次序计算出正确结果。
- 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
示例
例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》
- 极客时间的《数据结构与算法之美》专栏