前言
栈是一种只能在一端进行插入和删除操作的特殊线性表。
其中,允许插入和删除的一端称为栈顶,另一端称为栈底。对于栈的操作有两种:进栈(push)和出栈(pop)。前者相当于插入栈元素,后者则是删除栈元素。
由于对于栈元素的操作只能位于栈顶,因此,栈中最先插入的元素一定位于栈底,而最后插入的元素位于栈顶。
栈又叫做“后进先出(LIFO)”表
对于栈的实现有两种:
- 通过数组实现
- 通过单链表实现
中缀和后缀表达式的转换
现假设有这样一个中缀表达式:
a+b*c+(d*e+f)*g
我们可以通过栈来将该中缀表达式转换为后缀表达式,转换后的结果如下:
abc*+de*f+g*+
中缀表达式到后缀表达式的转换利用一个栈以及一个输出流,首先遇到数字则直接输出到输出流中,而遇到字符则压入栈,再适当的利用字符的优先级弹出栈顶优先级高的字符(优先级:左右括号>乘/除>加/减)。其中只有当右括号入栈时才会弹出栈顶到栈中左括号期间的所有字符,当遇到平等优先级时仍然弹出栈顶元素。
下面我们通过上面的中缀表达式来说明到后缀表达式的状态转换:
(1)首先a被读入,由于它不是运算操作符,于是它被传入输出流,然后,‘+’被读入并且压入栈中。接下来b读入并流向输出流。
(2)接着‘*’号被读入,由于此时栈顶元素为‘+’,优先级小于‘*’,因此不会被弹出栈且‘*’入栈。接着,c被读入并输出。
(3)下一个入栈元素为‘+’,经过我们发现,它的优先级比栈顶元素‘*’低,因此,栈顶元素‘*’被弹出栈并输出到输出流。弹出栈后此时的栈顶元素是’+’,它与此时需要入栈的‘+’拥有相同优先级,因此栈顶元素‘+’弹出栈,该入栈元素‘+’入栈。
(4)下一个被读入的字符是‘(’,由于它具有最高优先级,因此把它放入栈中。然后d读入并输出到输出流中。
(5)继续读入下一个元素‘*’,由于左括号只有遇到右括号时才会被弹出栈,因此‘*’入栈,并读入e输出。
(6)再往后读入的是‘+’,于是我们将‘*’弹出栈,然后将‘+’压入栈,接着读入f并输出。
(7)现在我们准备读入‘)’,此时会弹出栈中到左括号间的所有操作符并输出到输出流中,于是这里将‘*’输出到输出栈中,最后将g读入输出到输出流中。
(8)至此为止我们已经读完了所有的输入,因此我们将栈中的操作符全部弹出并输出,直到栈变成空栈。
在转换过程中我们需要注意以下几点:
- ‘+’和‘-’遇到栈顶元素是‘*’或‘\’时会将该栈顶元素弹出。
- ‘*’和‘\’不会弹出任何栈顶元素。
- ‘(’只有遇到‘)’时才会被弹出,并且‘)’不会入栈。
栈的链表实现
由于栈可以使用链表来实现。如果对于链表的实现有点难以理解可以参照《Java集合中的ArrayList和LinkedList》。我们这里将使用单链表来实现栈。
栈的实现中会有入栈(push)、出栈(pop)方法,并且这都只会针对于栈顶元素进行操作。也有获取栈顶元素的方法(peek)以及判断栈是否为空。
栈的链表实现代码如下:
/**
* @author: zhangocean
* @Date: 2018/10/5 20:23
*/
public class Stack<T>{
private Node<T> top = null;
private class Node<T>{
private T data;
private Node<T> next;
public Node(T data){
this.data = data;
}
}
//入栈
public void push(T data){
Node<T> node = new Node<>(data);
node.next = top;
top = node;
}
//出栈
public T pop() throws Exception {
if(isEmpty()){
throw new Exception("栈已空");
}
T data = top.data;
top = top.next;
return data;
}
//判断栈是否为空
public boolean isEmpty(){
return top == null;
}
//返回栈顶元素
public T peek(){
return top.data;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder("Stack{item=[");
Node<T> theTop = top;
while (theTop != null){
str.append(theTop.data).append(",");
theTop = theTop.next;
}
return str.substring(0, str.length()) + "]}";
}
}
由于我们需要使用单链表来实现栈,因此栈中每个元素都应该有一个结点,该结点会含有数据(data)以及指向下一个结点的链(next链)。首先空栈时栈中没有任何结点,并且栈顶元素为null。
当发生入栈操作时,会创建一个新节点并将入栈元素的值赋给该节点的data属性,然后再将该结点的next链指向栈顶元素,最后将该新节点作为栈顶元素。而出栈时就只需将栈顶元素的值返回,然后将栈顶元素next链上的结点作为新的栈顶元素即可。
最后我们来对这段代码进行测试:
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
stack.push("xyx");
stack.push("is");
stack.push("a");
stack.push("pig");
System.out.println(stack);
System.out.println(stack.peek());
}
输出结果如下:
Stack{item=[pig,a,is,xyx,]}
pig
这里我们可以发现pig是最后一个入栈的,但是它却是第一个打印出来的元素,因为在我们重写栈的toString
方法时,是从栈顶开始输出,直到输出到栈底为止。
栈的数组实现
栈的数组实现避免了链而且也可能是更流行的实现方案。数组实现中有的方法与链表实现中的方法大致相同,不同的是我们这里需要创建一个数组。
/**
* @author: zhangocean
* @Date: 2018/10/5 20:39
*/
public class Stack<T> {
private T[] item;
private int top;
public Stack(int size){
item = (T[]) new Object[size];
top = 0;
}
public void clear(){
top = 0;
}
public boolean isEmpty(){
return top == 0;
}
public int length(){
return top;
}
public T peek(){
if(isEmpty()){
return null;
}
return item[top-1];
}
public void push(T data) throws Exception {
if(top == item.length){
throw new Exception("栈已满");
}
item[top++] = data;
}
public T pop() throws Exception {
if(isEmpty()){
throw new Exception("栈已空");
}
return item[--top];
}
@Override
public String toString() {
StringBuilder str = new StringBuilder("Stack{item=[");
for(int i=0;i<top;i++){
str.append(item[i]).append(",");
}
if(isEmpty()){
return str + "]}";
}
return str.substring(0, str.length()-1) + "]}";
}
}
当创建一个数组栈时我们需要给该栈指定一个大小。在入栈操作时会存在栈的数组已满的情况,可以我选择的是抛出栈满的异常,当然我们也可以在此时对栈进行适当的扩容。
测试代码如下:
public static void main(String[] args) throws Exception {
Stack<Integer> stack = new Stack<>(10);
for(int i=0;i<10;i++){
stack.push(i);
}
System.out.println(stack);
System.out.println(stack.peek());
System.out.println(stack.length());
}
输出结果看下面:
Stack{item=[0,1,2,3,4,5,6,7,8,9]}
9
10
更多文章请关注我的个人博客:www.zhyocean.cn