栈和队列的学习(Java实现)
包括栈的实现,使用栈进行编译器检查,使用栈结合逆波兰法和中缀到后缀的转换进行计算器计算。也介绍了栈帧,此外还有队列的简要介绍
3.6 栈ADT
3.6.1 栈模型
栈是限制插入和删除只能在一个位置上的表,该位置是表的末端,叫做栈的顶。对栈的基本操作有push和pop,前者相当于插入,后者则是删除最后插入的元素。最后插入的元素可以通过使用top例程在执行pop前进行考察。对空栈进行pop或者top是栈ADT中的一个错误,当运行push时空间用尽不认为是错误,而是实现限制。
3.6.2 栈的实现
栈有时又叫做LIFO(后进先出)表。由于栈是一个表,所以任何实现表的方法都能实现栈。ArrayList和LinkedList都支持栈操作。
- 栈的链表实现。栈的第一种实现方法是实现单链表。通过在表的顶端插入来push,通过删除表的顶端元素来pop,top只是用来查询顶端的值
- 栈的数组实现。模仿ArrayList的add操作。与每个栈相关的操作是theArray和topOfStack,对于空栈他是-1(也就是初始化操作)。为了将某个元素x推入栈中,我们让topOfStack增1然后置theArray[topOfStack]=x。为了弹出栈元素,我们置返回值为theArray[topOfStack],然后使topOfStack-1
/**
* 栈的数组实现
* @param <T>
*/
public class MyStack<T> {
private static final int DEFAULT_CAPACITY=10;
private T[] elements;
private int topOfStack;
public MyStack(){
clear();
ensureCapacity(DEFAULT_CAPACITY);
}
/**
* 数组扩容
* @param newSize
*/
private void ensureCapacity(int newSize){
if(newSize<topOfStack){
return;
}
T[] olds=elements;
elements=(T[])new Object[newSize];
for(int i=0;i<topOfStack;i++){
elements[i]=olds[i];
}
}
/**
* 初始化栈顶为-1
*/
private void clear(){
topOfStack=-1;
}
public void push(T newElement){
elements[++topOfStack]=newElement;
}
public T pop(){
T element=elements[topOfStack];
topOfStack--;
return element;
}
public T top(){
return elements[topOfStack];
}
}
/*
*栈的测试类
*/
@Test
public void stackTest(){
MyStack<Integer> myStack=new MyStack<>();
myStack.push(123);
myStack.push(125);
myStack.push(44);
myStack.push(79);
System.out.println(myStack.pop());
System.out.println(myStack.pop());
System.out.println(myStack.pop());
System.out.println(myStack.pop());
}
3.6.3 栈的应用
1、检查完整性
- 平衡符号:例如括号是成对出现的,检查是不是有一对等,常用在编译器检查错误上。
- 算法描述:做一个空栈。读入字符直到结尾。如果字符是一个开放符号,则将其推入栈中,如果字符是一个封闭符号,则当栈空时报错。否则,将栈元素弹出。如果弹出符号不是对应的开放符号,则报错,在文件结尾,如果栈非空,则报错。
/*
*为了获得最终的栈的大小,我把栈的大小公开了
*/
@Test
public void stackAppCheckPair(){
MyStack<String> myStack=new MyStack<>();
String checkStr="ddd{AAA0(})";
Single<String> pair1=new Single<>("(",")");
Single<String> pair2=new Single<>("{","}");
MyArrayList<Single<String>> pairList=new MyArrayList<Single<String>>();
pairList.add(pair1);
pairList.add(pair2);
for(int i=0;i<checkStr.length();i++){
String c=String.valueOf(checkStr.charAt(i));
for(Single<String> pair:pairList){
if(c.equals(pair.first)){
myStack.push(c);
}else if(c.equals(pair.end)){
if(myStack.top().equals(pair.first)){
myStack.pop();
}else {
System.out.println("Error");
return;
}
}
}
}
if(myStack.topOfStack!=-1){
System.out.println("Error");
}else {
System.out.println("Success");
}
}
private class Single<T>{
public Single(T first, T end) {
this.first = first;
this.end = end;
}
public T first;
public T end;
}
2、后缀表达式计算
- 采用逆波兰法(后缀)法计算字符串方程
- 算法描述:遇见一个数就把他推入栈中,遇见一个运算符就把两个数弹出来,用运算符计算,再压栈
例子:12 * 3 + 21 + 6 * 2 = 按照逆波兰法,则记为 12 3 * 21 + 6 2 * +
计算:“6523+8*+3+*”
@Test
public void stackAppCalcuator(){
MyStack<String> myStack=new MyStack<>();
List<String> toolList=new ArrayList<>();
toolList.add("+");
toolList.add("-");
toolList.add("*");
toolList.add("/");
String str="6523+8*+3+*";
for(int i=0;i<str.length();i++){
String element=String.valueOf(str.charAt(i));
if(toolList.contains(element)){
int result=0;
int num1=Integer.valueOf(myStack.pop());
int num2=Integer.valueOf(myStack.pop());
if(element.equals("+")){
result=num1+num2;
}else if(element.equals("-")){
result=num2-num1;
}else if(element.equals("*")){
result=num2*num1;
}else if(element.equals("/")){
result=num2/num1;
}
myStack.push(String.valueOf(result));
}else{
myStack.push(element);
}
}
System.out.println(myStack.pop());
}
3、将中缀表达式转为后缀表达式
中缀表达式就是标准的表达式:12 * 3 + 21 + 6 * 2 =
例子:a+b*c+(d*d+f)*g 转化为后缀表达式为abc*+de*f+g*+
下面时算法思路
- 当读到一个操作数的时候,立即把他放在输出中。操作符不立即输出,将读到的操作符推入栈中。
- 当遇到左圆括号的时候我们也要将其推入栈中。从计算一个空栈开始。
- 当遇到一个右括号,那么就将栈元素弹出,将弹出的符号写出,直到遇见一个对应的左括号,但是这个左括号只被弹出并不被输出
- 如果见到任何其他符号 +,*,(,那么我们从栈中弹出栈元素直到发现更低的元素为止。有一个例外,除非在处理一个)的时候,否则绝不从栈中移走(,对于这种操作,+的优先级最低而(的优先级最高。当从栈弹出元素的工作完成后,我们再把操作符压入栈中。
- 如果读到输入的末尾,我们将栈元素弹出直到该栈变成空栈,将符号写到输出中
这个算法的想法是,当看到一个操作符的时候,把他放入栈中。栈代表挂起的操作符,然而,栈中有些具有高优先级的操作符现在知道当他们不再被挂起时要完成做使用,应该被弹出,这样,在把当前操作符放入栈中之前,那些在栈中并在当前操作符之前完成使用的操作符被弹出
/**
*中缀到后缀转换
*/
@Test
public void infixTest(){
MyStack<String> myStack=new MyStack<>();
List<String> toolList=new ArrayList<>();
toolList.add("+");
toolList.add("-");
toolList.add("*");
toolList.add("/");
toolList.add("(");
toolList.add(")");
List<String> numberList=new ArrayList<>();
numberList.add("a");
numberList.add("b");
numberList.add("c");
numberList.add("d");
numberList.add("e");
numberList.add("f");
numberList.add("g");
String str="a+b*c+(d*e+f)*g";
StringBuilder sb=new StringBuilder();
for(int i=0;i<str.length();i++){
String element=String.valueOf(str.charAt(i));
if(numberList.contains(element)){
sb.append(element);
}else if(toolList.contains(element)){
int stackSize=myStack.topOfStack+1;
if(stackSize==0){
myStack.push(element);
}else {
String currElement=element;
for (int j=0;j<stackSize;j++){
String prevElement=myStack.top();//获取栈顶
if(prevElement==null){
break;
}
if(currElement.equals("+")||currElement.equals("-")){//如果当前读是+和-,那么没有比他们优先级更低的了,除非遇到(,否则全部弹出
if(prevElement.equals("(")){
myStack.push(currElement);//压入栈
j=stackSize;//压入之后结束遍历
}else {
sb.append(myStack.pop());//这里还需要判断一下是否全部弹完了,全谈完之后要把刚才新的元素压入
if(j==stackSize-1){
myStack.push(currElement);
}
}
}else if(currElement.equals("*")||currElement.equals("/")){//如果是*或者/,那么优先级位于(和+,-之间,遇到同级的则弹出原来的元素再压入
if(prevElement.equals("(")){
myStack.push(currElement);//压入栈
j=stackSize;//压入之后结束遍历
}else if(prevElement.equals("+")||prevElement.equals("-")){
myStack.push(currElement);//压入栈
j=stackSize;//压入之后结束遍历
}else{
sb.append(myStack.pop());
}
}else if(currElement.equals("(")) {// (的优先级是最高的,所以直接压入就行
myStack.push(currElement);//压入栈
j=stackSize;//压入之后结束遍历
}else {//最后,当前元素为)时,弹到(为止
if(prevElement.equals("(")){
myStack.pop();
j=stackSize;
}else {
sb.append(myStack.pop());
}
}
}
}
}
}
while ((myStack.topOfStack)!=-1){//最后把栈中的运算符全部弹出
sb.append(myStack.pop());
}
System.out.println(sb.toString());
}
把中缀表达式转为后缀表达式之后,再进行计算即可,实现简单的计算器
4、方法调用
当存在方法调用的时候,需要存储的所有的重要信息,例如寄存器的值(对应变量的名字)和返回地址(它可以从程序计数器中得到,一般情况下是在一个寄存器中)等,都要以抽象的方式存在一张纸上,并被置于一个堆的顶部。然后控制转移到新的方法,该方法自由的使用他的一些值代替这些寄存器。如果他又存在其他调用,也遵循这样的方式,当方法返回时,他查看堆顶部的那些纸,并且复原所有寄存器,然后进行返回转移。
这些所有的工作都可以由一个栈来完成,而这正是实现递归的每一种程序设计语言实际发生的事实。所有存储的信息称为活动记录,或者叫做栈帧。在实际的计算机中的栈常常是从内存分区的高端向下增长,很多非Java系统是不检测溢出的,由于可能有很多同时运行的方法,因此栈空间用尽是有可能的。在正常情况下一般不会越出栈空间,发生这种情况常常是由于失控递归造成的。
一般不要使用尾递归(在函数的最后一行递归)
/**
* 尾递归的不当使用,有N个元素就会导致有N个栈帧
* @param list
*/
public void printList(Iterator<String> list){
if(!list.hasNext()){
return;
}else {
System.out.println(list.next());
printList(list);
}
}
/**
* 不用递归打印一个链表,没有用到栈帧
* @param list
*/
public void printList(Iterator<String> list){
while (true){
if(!list.hasNext()){
return;
}
System.out.println(list.next());
}
}
3.7 队列ADT
3.7.1 队列模型
像栈一样,队列也是表。然而队列的插入和删除不在同一端进行(FIFO先进先出)
队列的基本操作是enqueue(在尾部插入)和dequeue(在头部删除)
3.7.2 队列的数组实现
实现思路
- 对于每一个队列的数据结构,我们保留一个数组theArray以及位置front和back,他们代表队列的两端,还要记录实际存在于队列中元素的个数currentSize和back增1,然后theArray[back]=x,如果让元素出栈,则置返回值为theArray[front],currentSize–,front++;
为了解决出队问题,使用循环数组解决。当front和back到达数组的尾端,他就又绕回开头