栈是限制插入和删除都只能从一个位置上操作的表,该位置是表的末端,表的末端一般叫做栈顶,对栈的基本操作有push(进栈) 插入 和 pop(出栈)删除,前者相当于在表最后的位置上插入元素,后者相当于删除最后进入栈的元素,俗称也叫先进后出表,后进先出表,我们对栈的基本操作也就是 push和pop,一般栈的模型就是存在某个元素位于栈的顶部,并且该元素是唯一可见的元素,
由于栈是一个表,所以任何实现标的方法都可以实现栈,显然Arrylist和LinkedList都支持栈的操作,大多数的时候他们都是最合理的实现,偶尔设计一种特殊目的实现可能会更快 比如 如果被放到栈上的元素是基本类型 因为对栈的操作都是常数时间操作,因为就是添加和删除么 涉及到一些元素的移动,所以除非在非常特殊的环境下 这是不可能产生任何明显改进的,由于这些特殊的时机, 我们将给出两个流行的实现方法,一种方法使用链式结构,而另一种方法使用数组,二者均简化了 ArrayList和LinkendList中的逻辑,
栈的链表实现
栈的第一种实现方式是使用单链表,通过在表的顶端插入元素 来实现push 通过在链表的顶部来删除元素来实现pop,top操作只是考察表顶端元素 并返回它的值,有时pop操作和top操作合二为一,
栈的数组实现
数组实现栈的方式 避免了链而且可能是更流行的解决方案,由于模仿ArrayList的add操作。因此相应的实现方法非常简单,与每个栈相关联的操作是theArray和topStack,对于空栈它是-1,这就是空栈初始化的做法,为将某个元素x推入栈中,我们使topStack增加1,然后置theArray【topStack】= x,为了弹出栈顶的元素,我们置返回值为theArrary【topStack】然后使topStack减1;
也就是说存储还是按照数组存储 每次插入都是向 数组的尾部插入一个元素,并且这个数组内容的实际元素个数 其实就是topStack+1,然后增加的时候删除元素的时候 也是将topStack(其实就是索引)位置上的元素删除,删除以后索引减1;
需要我们注意的是 上述操作 不仅是以厂常数时间操作 而且是以非常快速的时间运行,在某些机器上,若在带有自增自减寻址功能的寄存器上操作,则(整数)的push和pop都可以写成一条机器指令,最现代的计算机将栈作为它的指令系统的一部分。这样的事实强化了一种观念就是,栈很可能是继数组之后 又一个被定义为计算机科学中的最基本的数据机构,
应用
如果我们把操作限制在一张表上进行,那么这些操作将会执行的很快, 这些少量的操作非常强大而且重要
平衡符号
编译器检查程序的语法错误,但是常常由于缺少一个符号(如遗漏一个花括号或者是注释起始符)引起编译器列出上百行的诊断,可真正的错误并没有找出来,在这种情况下,一个有用的工具就是检验是否每件事情都能组成一对儿的程序,于是每一个右花括号、右方括号、右圆括号必然对应其相应的左括号,序列【()】是合法的,但是【(】是错误的,单单是为了校验这种错误 是肯定不会编写一个大程序的,事实上检验这些错误其实是很容易的,为了简单起见,我们仅就圆括号、方括号、和花括号、进行检验,并忽略其他出现的任何字符,
这个简单的算法用到了一个栈,
做一个空栈,读入字符直到文件结尾,如果字符是一个开放符号(就是上面说的三种括号的左括号),就将其推入栈中,如果字符是一个封闭符号,则空栈时报错,否则将栈元素弹出,如果弹出的符号不是对应的开放符号,则报错,在文件结尾,如果栈非空则报错,
我们可以确信这个算法是可以正确运行的,很清楚,它是线性的,事实上它只需要对输入进行一趟检验,因此它是 on-line 联机的,是相当快的,当报错时,决定如何处理需要做附加的工作,-例如判断可能导致错误的原因,
后缀表达式
假设我们有一台便携式的计算器并且想要计算一趟外出购物的花费,为此 我们将一列数字相加并将结果诚意1.06;它是所购物品的价格以及附加的地方销售税,如果购物各项消费为4.99、5.99、6.99,那么输入这些数组的自然方式是:
4.99+5.99+6.99*1.06 =
随着计算器的不同,这个结果或者是所要的答案19.05、或者是科学答案18.39,最简单的四功能计算器都将给出第一个答案,但是许多先进的计算器是知道乘法优先级高于加法的,另一方面有些商品是要加收税的 而有些却不需要,因此如果只有第一项和最后一项需要加税的,那么计算的顺序会是:
4.99*1.06+5.99+6.99*1.06
将在科学计算器上给出的正确答案(18.69)而在简单计算器上给出的错误的答案19.37,科学计算器一般包含括号,因此我们总可以通过加括号的方法得到正确的答案,但是使用最贱的计算器需要我们记住中间的结果,
该例子的典型计算顺序可以是将4.99和1.06相乘的结果存为A,然后将5.99和A相加,在将结果存入A,我们在将6.00*1.06的结果存为B,最后将A+B的结果存入A,我们可以将这种操作顺序书写如下:
4.99 1.06*5.99+6.991.06*+
这个记法叫做后缀,或逆波兰记法,其求值过程恰好就是上面所描述的过程,计算这个问题最容易的方法就是使用一个栈,当见到一个数时就将它推入栈中,在遇到一个运算符时该算符就作用于从该栈弹出的两个数上,在将两个数的结果推入栈中,例如后缀表达式
6523+8*+3+*
计算如下
将前四个字符放入栈中 、
topStack -> 3
2
5
6
然后读到一个运算符 + 所以 3 和2 从栈中弹出 并且它们的和5 被压入栈中
topStack ->5
5
6
接着 8 进栈
topStack ->8
5
5
6
然后遇到一个*符号
8和5 出栈 并将结果40进栈
topStack -> 40
5
6
接着又遇到一个加号+
40和5出栈 并将结果45 进栈
topStack -> 45
6
接着遇到数字3 进栈
topStack ->3
45
6
接着遇到+
3、45出栈 和48进栈
topStack ->48
6
接着遇到*
48 6出栈相乘 288 压入栈中
topStack->288
计算一个后缀表达式花费的时间是O(N),因为对输入中的每个元素的处理都是由一些栈操作组成从而花费常数的时间,该算法的计算式非常简单的,注意当一个表达式以后缀记号出现以后,没有必要知道任何优先的规则,这是一个明显的优点
中缀到后缀的转化
栈不光可以用来计算后缀表达式的值,而且还可以用栈将每一个标准形式的表达式(或者叫做中缀表达式)转换成后缀表达式,我们通过只允许操作+,*(,),并坚持普通的优先级法则而将一般的问题浓缩成小规模的问题,此外还要进一步假设表达式是合法的,假设将中缀表达式
a+b*c+(d*e+f)*g
转化成后缀表达式。正确的答案是 abc* +de*f+g*+
当读到一个操作数的时候,立即把它放到输出中,操作符不立即输出,从而必须先存在某个地方,正确的做法是将这些已经见到但尚未放到输出中,当遇到左圆括号时我们也要将它推入栈中,计算从一个空的栈开始,如果见到一个右括号,那么就将栈元素弹出,将弹出的符号写出直到遇到一个(与之对应的)左括号,但是这个左括号只被弹出,不被输出,如果我们见到任何其他的字符(+,-*(),那我们从栈中弹出元素,直到发现优先级更低的元素为止,有一个例外就是;除非是在处理一个)的时候,否则我们绝不从栈中移走左括号,对于这种操作 + 的优先级更低,而(的优先级最高, 当从栈中弹出元素的工作完成后,我们在将操作符亚茹栈中。
最后读到输入的末尾,我们将栈元素弹出直到该栈变为空栈,将符号写到输出中,
这个算法的想法是 当看到一个操作符的时候,把它放到栈中,栈代表被挂起的操作符,栈中有些具有高优先级的操作符现在知道当它们不在被挂起的时候要完成使用,应该被弹出,这样,在把当前操作符放到栈中之前,那些在栈中并在当前操作符之前要完成使用的操作符要弹出,
圆括号的使用 增加了复杂因素,所以当左括号是一个输入符号时我们可以把它看成是一个高优先级的操作符(使得原本挂起的操作符仍然是挂起的),
为了立即诶这种算法的运行机制
我们将上面的a+b*c+(d*e+f)*g 中缀表达式 转化为后缀表达式
首先 a被写入输出中
输出 a
然后遇到 + 栈顶是空的 所以先放入栈顶
输出 a 栈 -> +
然后遇到b
输出ab 栈 ->+
然后遇到*
*的优先级大于 栈顶元素+ 将*操作符压入栈顶
输出ab 栈- > *
+
然后遇到 c
输出abc 栈 -> *
+
然后遇到+
+的优先级小于栈顶元素 *,*输出,栈顶元素 为+,+优先级不高于 新的+ 故弹出 并输出
输出: abc*+ 栈 -> +
然后遇到( 直接压入栈中
输出:abc*+ 栈 -> (
+
然后遇到d
输出 abc*+d 栈 -> (
+
然后遇到*
左括号 进到栈里后 优先级最小 直到遇到右括号
输出:abc*+d 栈 -> *
(
+
然后遇到e
输出:abc*+de 栈 -> *
(
+
然后遇到再然后是+
*的优先级 要高+,* 弹出 并输出 然后将新的+ 压入栈中
输出abd*+de* 栈 - > +
(
+
然后遇到的是f 输出
输出 abd*+de*f 栈 -> (
+
然后遇到的是) 现在讲栈中元素弹出 直到遇到(
输出 abd*+de*f+ 栈中元素 栈 ->+
然后读到的是* *的优先级要高于栈顶元素 所以压入栈 不输出
输出:abd*+de*f+ 栈 -> *
+
最后读到的是g 输出 并将栈中元素依次输出 直到栈为空
输出 abd*+de*f+g*+。
与前面相同 这种转换只需要O(N)的时间并经过一趟输入后工作完成,可以通过指定减法和加法有相同的优先级以以及乘法和除法有相同的优先级,而将减法和除法添加到指令集中去,
方法调用
检测平衡符号的算法提出了一种在编译的过程中语言和面向对象语言中实现方法调用的方式,这里的问题是,当调用一个新方法时,主调例程的所有局部变量需要由系统存储起来,否则被调用的新方法将会重写由主调例程的变量所使用的内存, 不仅如此,该主调例程的当前位置也必须要存储,以便在新方法运行完后知道向哪里转移,这些变量,一般由编译器指派给机器的寄存器,但存在某些冲突(通常所有的方法都是获取指定给1号寄存器的某些变量,)特别是涉及递归的时候,该问题类似于平衡符号的原因在于把,方法调用和方法返回基本上类似于开括号和闭括号,两者相同的想法应该是行的痛的,当存在方法调用的时候,需要存储的所有重要的信息,诸如 寄存器的值(对应变量的名字)和返回地址(它可以从程序计数器得到,一般情况下是放在一个寄存器上,)都要以抽象的方式存在“一张纸上”,并被置于一个堆得顶部,然后控制转移到新的方法,该方法自由的用它的一些值 代替这些寄存器,如果它又进行其他方法的调用,以此类推当该方法返回时,它查看堆顶部的那张纸 ,并复原所有的寄存器,然后进行返回转移,
显然,所有全部工作均可以由一个栈来完成,而这正是在实现递归的每一种设计语言实际发生的事实,所存储的信息或称为活动记录,或者称为栈帧,在典型情况下需要做些微调,当前的环境是由栈顶描述的,因此一条返回语句就可给出前面的环境,
在实际计算机中 栈常常是从内存分区的高端乡下增长,而在许多非java系统中是不检测溢出的,由于有太多的同时运行着的方法,因此栈空间用尽的情况是可能发生的,
在正常情况下我们不该越出栈空间,发生这种情况通常是由失控递归 (无基准的情形)引起的, 因为每次递归调用的时候 都要将原例程的变量 。变量的地址 寄存器的值 等等存储在栈顶 若一直无方法返回则 会有不断的 方法变量 进栈 直到 栈空间溢出,
下面是我编写的练习代码
package javabean.adt.List; import java.util.EmptyStackException; /** * 栈是限制插入和删除只能在一个位置上进行的表,该位置就是表的末端 ,这种表叫做栈, * 对栈的基本操作有 push 进栈 和pop 出栈,前者相当于插入,后者相当于删除最后插入的元素,最后插入的元素 * 可以通过top例程 在执行pop之前进行考查,对空栈 进行top 和 pop 一般认为是栈的一个错误,另一方面 * 当运行push时空间用尽是一个实现限制 但不是adt错误 * 我们对 栈所能做的也就是 push和pop操作 * 栈也叫后进先出表 * * 一般的栈模型是 存在某个元素位于栈顶而该元素是唯一的可见元素 *a+b*c+(d*e+f)*g * abc*+de*f+g*+ * * * 输出:abcdef+* * 栈:+*+(*+ * 输出:ab+c * 栈:* * */ public class MyStack<Anaytype> { private Object[] theArray; private Object[] emptyThearray; /** * 由于填写和删除都在栈顶操作 所以其实 现有的栈顶元素的索引 其实是 * 栈中元素个数 减一 * 然后如果要添加元素 那么 添加的位置的索引 其实就是 size */ private int elementCount; private int defaultLength = 10; public MyStack(){ theArray = new Object[defaultLength]; } public void push( Anaytype anaytype){ addLast(anaytype); } private void addLast(Anaytype anaytype){ ensureCapacityHelper(elementCount+1); theArray[elementCount++] = anaytype; } private void ensureCapacityHelper(int minCapacity){ if (minCapacity-theArray.length>0) grow(); } public boolean isEmpty(){ return elementCount==0; } public Anaytype pop(){ Anaytype anaytype = top(); removeEnd(); return anaytype; } public Anaytype top(){ int len = theArray.length; return returnElement(elementCount-1); } private Anaytype returnElement(int index){ if (index-elementCount>=0) throw new IndexOutOfBoundsException("大兄弟 你的索引值:"+index +"越界了,现在栈顶的索引才"+ (elementCount-1)); return (Anaytype) theArray[index]; } private void removeEnd(){ theArray[elementCount--] =null; } private void grow(){ int theArrayLength = theArray.length; int growLength = theArrayLength + theArrayLength>>1; Object[] a = new Object[growLength]; System.arraycopy(theArray,0,a,theArrayLength,elementCount); theArray = a; } public int size() { return elementCount; } }
package javabean.adt.List; import com.sun.istack.internal.NotNull; import com.sun.org.apache.xerces.internal.impl.xpath.regex.Match; import java.lang.reflect.Array; public class AdtUtil { private static MyStack<String> priority = new MyStack<>(); private static StringBuffer expresion = new StringBuffer(); private static MyStack<Integer> dataStack = new MyStack<>(); /** * 检测平衡符号、 * benan/end, * () * [] * {} */ public static void checkPinghengFuhao(String s) { char[] chars = s.toCharArray(); MyStack<Character> myStack = new MyStack<Character>(); MyStack<Integer> integerMyStack = new MyStack<>(); char[] openStrings = {'(', '[', '{'}; char[] closeStrings = {')', ']', '}'}; for (int i = 0; i < chars.length; i++) { for (int j = 0; j < openStrings.length; j++) { if (chars[i] == openStrings[j]) { myStack.push(openStrings[j]); integerMyStack.push(i); } } for (int g = 0; g < closeStrings.length; g++) { if (chars[i] == closeStrings[g]) { char top; try { top = myStack.top(); } catch (IndexOutOfBoundsException e) { throw new RuntimeException(e + " char数组中 第" + i + "位置上的 闭合符号没有对应的开符号"); } if (openStrings[g] == top) { myStack.pop(); integerMyStack.pop(); } else { if (myStack.isEmpty()) { throw new RuntimeException("char数组中 第" + i + "位置上的 闭合符号没有对应的开符号"); } else { throw new RuntimeException("char数组中 第" + integerMyStack.top() + "位置上开符号没有对应的闭合符号"); } } } } } if (!myStack.isEmpty()) { throw new RuntimeException("开符号没有对应的闭合符号"); } } /** * 实现后缀表达式 */ /** * 复制数组 */ @NotNull public static <T, U> T[] copy(@NotNull U[] original, int newLength, @NotNull Class<? extends T> newType) { /** * 说白了 在java中的数组 根据是不是Object 可以分为两种 是Object 或者不是, * 使用Array.newInstance的目的是在于 动态的开辟 创建不同类型的数组 * 数组 声明对象可以是任意 但是在赋值的时候 主要看的还是 该数组开辟的是什么类型的空间 */ T[] newCopy = newType == Object[].class ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType, newLength); System.arraycopy(original, 0, newCopy, 0, Math.min(original.length, newLength)); return newCopy; } public static Character[] getBaseCharToCharcter(@NotNull char[] chars) { Character[] characters = new Character[chars.length]; for (int i = 0; i < chars.length; i++) characters[i] = chars[i]; return characters; } /** * 更简单的传参数 * * @param original * @param newLength * @param <T> * @param <U> * @return */ public static <T, U> T[] copyOf(@NotNull U[] original, int newLength) { return (T[]) copy(original, newLength, original.getClass()); } public static int compareTo(String a, String b) { return 0; } /** * 判断 字符是不是一个 算数运算符 */ public static boolean isPority(@NotNull String s) { String[] poritys = {"(", ")", "*", "/", "+", "-"}; for (String p : poritys) { if (p.equals(s)) return true; } return false; } /** * 计算运算符优先级 * + - 优先级 最低 并且相同 * * / 优先级第二低 并且相同 * () 优先级最高 并且相同 这样 可以保证 在优先级相同时 弹出元素 也就是( 只有在遇到)才会被弹出 * * @param s * @return */ public static int priority(String s) { switch (s) { case "+": return 1; case "-": return 1; case "*": return 2; case "/": return 2; case "(": return 3; case ")": return 3; default: return 0; } } /** * 将中缀表达式转换成后缀表达式 * * @param scancer * @return */ public static String midleToEnd(@NotNull String scancer) { String[] strings = getCharToString(scancer.toCharArray()); /** * 开始遍历输入字符 * 若遇到操作符 将 操作符推进栈 * 若遇到非操作符以及 直接输出 * 若是 操作符 先与栈顶元素 比较优先级 若栈顶无元素直接推入栈顶, * 若栈顶有元素 若栈顶元素优先级 大 则将栈里元素弹出 直到 遇到优先级小的为止 * */ StringBuffer stringBuffer = new StringBuffer(); for (String s : strings) { if (isPority(s)) { stringBuffer = combalTo(s, stringBuffer); } else { stringBuffer.append(s); } } /** * 若当读取字符完成 但是栈中还是有数据的话 全部弹出并输出 */ if (!priority.isEmpty()) { for (priority.top(); !priority.isEmpty(); priority.pop()) stringBuffer.append(priority.top()); } return stringBuffer.toString(); } /** * 整个逻辑 在遇到运算符的时候 将遇到的运算符 送入 取优先级方法 取出 其对应优先级,然后判断栈是否为空, * 栈若为空时: * 直接将运算符送入栈中 * 栈若有值: * 判断遇到运算符 是否为), * 若为),栈中元素 弹出并输出知道栈顶元素为( ,并将(弹出 但是不输出, * 若不为),将栈顶元素送入取优先级方法 取出对应优先级,并与遇到运算符 进行优先级比较, * 若栈中元素的优先级大于等于,栈外元素优先级,则将栈顶元素弹出 并输出, * 是大于等于 * 然后 在判断 栈是否为空: * 为空:将栈外元素送进栈中, * 非空:再次取栈顶元素的优先级与栈外运算符优先级比较,重复上述步骤 * <p> * 判断 运算符 与栈顶运算符优先级 * 若运算符 为 )则将栈中元素全部 弹出直到遇到( * 若栈顶运算符 大于等于 运算符 * 在判断栈顶元素 是否是( 若判断正确 则 运算符进栈 * 若判断失败 栈顶元素 弹出 然后重复上面判断 * 若栈顶元素 小于 运算符 直接进栈 * "a+b*c+(d*e+f)*g"; * * @param s * @param stringBuffer * @return */ public static StringBuffer combalTo(String s, StringBuffer stringBuffer) { if (!priority.isEmpty()) { if (!priority.isEmpty()) { for (priority.top(); !priority.isEmpty() && priority(priority.top()) - priority(s) >= 0; priority.pop()) { if (!"(".equals(priority.top())) { stringBuffer.append(priority.top()); } if ("(".equals(priority.top()) && !")".equals(s)) break; } if (!s.equals(")")) { priority.push(s); } else { if (!priority.isEmpty()) { for (priority.top(); !priority.isEmpty() && !"(".equals(priority.top()); priority.pop()) { stringBuffer.append(priority.top()); } priority.pop(); } } if (!priority.isEmpty() && "(".equals(priority.top())) { return stringBuffer; } } return stringBuffer; } else { priority.push(s); return stringBuffer; } } public static String[] getCharToString(char[] characters) { String[] newS = new String[characters.length]; for (int i = 0; i < characters.length; i++) newS[i] = String.valueOf(characters[i]); return newS; } /** * 计算后缀表达式 */ public int getAnwser(String exp) throws Exception { String[] exps = getCharToString(exp.toCharArray()); for (String s : exps) { if (isPority(s)) { Integer a = getAB(s); dataStack.push(a); }else { dataStack.push(Integer.valueOf(s)); } } return dataStack.pop(); } /** * 取出栈顶的两个元素 进行运算 * @param s * @return * @throws Exception */ public int getAB(String s) throws Exception { if (!isPority(s)) throw new Exception(); if (dataStack.size() - 2 < 0) throw new Exception("栈中元素少于两个"); /** * 注意 这里 取出的两个数字 在运算时 后取出的数字一定要在运算符前面 * 因为 是被先读取进栈的 */ Integer integer1 = dataStack.pop(); Integer integer2 = dataStack.pop(); switch (s) { case "+": return integer2 + integer1; case "-": return integer2 - integer1; case "*": return integer2 * integer1; case "/": return integer2 / integer1; default: return 0; } } }