栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
栈也是「线性表」的一种,它的特点是:先进后出,后进先出,只允许在一端插入和删除元素。
允许添加和删除元素的一端称作「栈顶」,另一端称作「栈底」。元素的添加操作被称作「入栈」,也叫「压栈」。元素的删除操作被称作「出栈」,也叫「弹栈」。
1. 栈的实现
JDK内置了栈的实现,对应的类为java.util.Stack,底层采用数组来存储元素。
为了更好的理解栈的思想,这里我们自己实现一遍。栈的功能很简单,可以用数组实现,也可以用链表来实现。
1.1 数组存储
使用数组来存储元素,size属性用来记录元素的数量,不管是入栈还是出栈,都是操作size对应的元素。主要注意的是由于数组定长,一旦入栈的元素超出数组的上限,需要扩容。
public class Stack<T> {
private int size;
private Object[] table;
public Stack() {
table = new Object[16];
}
public void push(T data) {
if (size == table.length) {
// 扩容
Object[] newTable = new Object[table.length << 1];
for (int i = 0; i < table.length; i++) {
newTable[i] = table[i];
}
table = newTable;
}
table[size++] = data;
}
public T pop() {
if (size <= 0) {
throw new RuntimeException("Stack is empty");
}
return (T) table[--size];
}
}
1.2 链式存储
使用单向链表来存储元素,size属性用来记录元素的数量,入栈时采用头插法,操作的永远是链头元素。比数组的实现更加简单,不用担心扩容的问题。
public class LinkedStack<T> {
private int size;
private Node<T> head;
public void push(T data) {
Node<T> node = new Node<>(data, null);
if (head == null) {
head = node;
}else {
node.next = head;
head = node;
}
}
public T pop() {
if (head == null) {
throw new RuntimeException("Stack is empty");
}
try {
return head.data;
}finally {
head = head.next;
}
}
private class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
}
}
2. 栈的应用
根据栈先进后出的特点,这里列举几个栈的应用场景。
2.1 平衡符号
编译器将源代码编译成机器可识别的执行文件前,肯定都要做一个语法校验,语法都错误了,还咋编译啊。语法校验中,有一项几乎是所有语言都需要做的,那就是判断语法中开放符号和封闭符号是否平衡。例如左右括号必须成对匹配。
【分析】
初始化一个空栈,挨个字符遍历源代码,遇到开放符号则入栈,遇到封闭符号则出栈,判断符号是否一致,不一致则校验不通过。源码遍历结束后,判断栈是否空,如果空,则校验通过。
【实现】
public class BalanceSymbol {
private static final Map<Character,Character> map;
private char[] chars;
private Stack<Character> stack;
static {
map = new HashMap<>(3, 1);
map.put('(', ')');
map.put('[', ']');
map.put('{', '}');
}
public BalanceSymbol(String source) {
this.chars = source.toCharArray();
this.stack = new Stack<>();
}
public boolean check() {
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else if (c == ')' || c == ']' || c == '}') {
if (!map.get(stack.pop()).equals(c)) {
throw new RuntimeException("不匹配的符号:" + c);
}
}
}
return stack.isEmpty();
}
public static void main(String[] args) {
String source = "public class BalanceSymbol {" +
"void test(){" +
"int[] arr = new int[5];" +
"System.out.println(arr[0]);" +
"}" +
"}";
new BalanceSymbol(source).check();
}
}
2.2 数据翻转
利用栈的「先进后出,后进先出」的特点,可以很轻松的实现数据的翻转。例如将字符串hello翻转为olleh。
【分析】
初始化一个空栈,将要翻转的数组依次全部入栈,再将出栈的数据依次写回到数组中,就可以实现数据翻转。
【实现】
public class Reverse {
private Stack stack = new Stack<>();
// 翻转字符串
public String reverse(String source) {
char[] chars = source.toCharArray();
for (int i = 0; i < chars.length; i++) {
stack.push(chars[i]);
}
for (int i = 0; i < chars.length; i++) {
chars[i] = (char) stack.pop();
}
return new String(chars);
}
// 翻转数组
public void reverse(int[] source) {
for (int i = 0; i < source.length; i++) {
stack.push(source[i]);
}
for (int i = 0; i < source.length; i++) {
source[i] = (int) stack.pop();
}
}
}
2.3 撤销和恢复
栈结构还可以很方便的用来实现操作的「撤销和恢复」功能。例如:浏览器的前进和后退、文档的撤销和恢复。
【分析】
初始化两个空栈:master和backup,当有新的操作时push到master。撤销操作将master出栈,backup入栈,保存撤销的数据。恢复操作将backup出栈,master入栈恢复数据。
【实现】
public class CancelRenew<T> {
private Stack<T> master = new Stack();
private Stack<T> backup = new Stack();
public void push(T data) {
master.push(data);
}
public void cancel() {
if (master.isEmpty()) {
throw new RuntimeException("暂不支持撤销");
}
backup.push(master.pop());
}
public void renew() {
if (backup.isEmpty()) {
throw new RuntimeException("暂不支持恢复");
}
master.push(backup.pop());
}
public void show() {
Iterator<T> iterator = master.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
public static void main(String[] args) {
CancelRenew<Integer> cr = new CancelRenew<>();
cr.push(1);
cr.push(2);
cr.push(3);
cr.cancel();
cr.show();
}
}
3. 栈结构模拟算式计算过程
使用编程语言进行一个加减法的计算再简单不过了,但是你有没有想过,你写的代码字符是如何被计算机解析并执行的呢?如果给你一个算式字符串,你能够解析并执行吗?
【问题】
给定一个10以内的加减法数学算式字符串,使用程序解析这个算式,并计算得到算式结果。
【分析】
初始化一个空栈,遍历这个字符串,遇到操作数就入栈,遇到运算符则出栈和下一个操作数进行运算,再将结果入栈,最后栈顶的元素就是结果。
【实现】
public class MathCalc {
private static Set<Character> operators;
private Stack stack = new Stack();
static {
operators = new HashSet<>();
operators.add('+');
operators.add('-');
}
public int calc(String expression) {
char[] chars = expression.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (!operators.contains(c)) {
// 操作数,入栈
stack.push(c);
} else {
// 操作符,出栈前一个操作数
int leftNum = Integer.parseInt(String.valueOf(stack.pop()));
int rightNum = Integer.parseInt(String.valueOf(chars[++i]));
if (c == '+') {
stack.push(leftNum + rightNum);
} else {
stack.push((leftNum - rightNum));
}
}
}
return (int) stack.pop();
}
public static void main(String[] args) {
int result = new MathCalc().calc("1+2+3+4-5");
System.out.println(result);
}
}
4. 总结
栈的特点是先进后出,后进先出,也叫作LIFO表,只能在栈顶添加和删除元素。栈的结构和功能都很简单,可以用数组或链表来实现。结合栈的特点,可以用它来实现数据翻转、数据的撤销和恢复等功能。最后用栈模拟了程序是如何解析算式并进行计算的,相信大家对栈这个数据结构有了一定的了解。