【DSaAA】栈和队列 水深无声 2022-05-25 03:57 197阅读 0赞 # 栈和队列 # * 栈和队列 * 栈 * ADT * 顺序栈 * 链式栈 * 顺序栈和链式栈的比较 * 栈的应用举例 * 检查符号对是否匹配 * 计算器(四则运算) * 方法调用和递归 * 队列 * ADT * 顺序队列 * 链式队列 * 顺序队列与链式队列 * 队列的应用 ## 栈 ## ### ADT ### 栈也是一种表,只不过这种表的插入和删除操作只能在一个同一个位置,即表的末尾处(栈顶)。对栈的操作最普遍的是入栈和出栈,当然还包括判断栈空、判断栈满、取栈顶等操作。 栈是后进先出的(`LIFO`),栈的可见元素只存在于栈顶。 ### 顺序栈 ### 顺序栈可以看做是顺序表的简单实现,但是需要确定把顺序表的表头还是表尾作为栈顶。 如果把表头作为栈顶,入栈和出栈操作都需要在第`0`个位置上做插入和删除操作,这就导致每次出入栈都需要移动顺序表中剩余的所有元素,时间复杂度为`O(n)`;如果把顺序表的最后一个元素作为栈顶,每次出入栈操作都是在顺序表末尾做删除或添加操作,那么很明显时间复杂度为`O(1)`。 顺序栈的实现可以参考:[ArrayStack.java][] ### 链式栈 ### 链式栈使用链表实现的,和顺序实现其实没什么特别大的区别,只是用了不同的数据结构,并且链式栈的栈顶不再需要规定是尾节点,因为链表的头尾插入和删除的时间复杂度都是`O(1)`。 ### 顺序栈和链式栈的比较 ### 简单的顺序栈需要预先分配空间(简单数组实现),如果栈中数据太多就可能导致溢出,而依附于`ArrayList`实现的话没有溢出的可能,但是在扩容和收缩的时候有复制操作,特别是数据量大的时候,而且扩展的一半没有完全使用会造成空间浪费,不过存储的都是数据本身,数据占用空间会少一些。 链式栈没有顺序栈的大部分烦恼,除了会占用额外的空间,然而实际应用中的栈基本都是顺序表实现的(包括`Java`),因为栈不适合存储大量的数据,一般都是用栈实现一种上下文相关的算法,因此不会有太多的数据,使用顺序栈已经足够。 ### 栈的应用举例 ### #### 检查符号对是否匹配 #### 比如检查一个表达式`{(a+b)/c-d+[(e-f)/(g+h)+i]}/100`中的`{[()]}`是否成对且是否匹配: public class SymbolPairChecker { public static boolean check(String pattern) { if (StringUtils.isNoneBlank(pattern)) { ArrayStack<Character> stack = new ArrayStack<>(); for (int i = 0; i < pattern.length(); i++) { Character character = pattern.charAt(i); switch (character) { case '{': case '[': case '(': if (!stack.isEmpty()) { Character preCharacter = stack.peek(); if (!order(character, preCharacter)) { throw new RuntimeException(formatError(pattern, i)); } } stack.push(character); break; case ')': case ']': case '}': if (stack.isEmpty()) { throw new RuntimeException(formatError(pattern, i)); } Character preCharacter = stack.pop(); if (!match(character, preCharacter)) { throw new RuntimeException(formatError(pattern, i)); } } } if(!stack.isEmpty()){ throw new RuntimeException(formatError(pattern, pattern.length() - 1)); } } return true; } private static boolean match(Character character, Character preCharacter) { switch (character) { case '}': if(preCharacter != '{'){ return false; } break; case ']': if(preCharacter != '['){ return false; } break; case ')': if(preCharacter != '('){ return false; } break; } return true; } private static boolean order(Character character, Character preCharacter) { return character <= preCharacter; } private static String formatError(String pattern, int i) { return "Symbol not match!\n" + pattern + "\n" + pointer(i); } private static String pointer(int i) { StringBuilder result = new StringBuilder(); for (int j = 0; j < i; j++) { result.append(" "); } return result + "^"; } public static void main(String[] args) { check("{(a+b)/c-d+[(e-f)/(g+h)+i]}/100"); //check("(a+b)/c-d+[(e-f)/(g+h)+i]}/100"); //check("((a+b)/c-d+[(e-f)/(g+h)+i]}/100"); //check("([])"); //check("({})"); check("["); } } #### 计算器(四则运算) #### 简单计算器直接边读边解释,不遵守四则运算法则,这肯定是不对的;如果要遵守四则运算,就必须使用后缀表达(逆波兰表达式)式压栈的方式;也可以用栈实现中缀表达式到逆波兰表达式的转换: public class StackCalculator { private final static Map<Character, Integer> CHARACTER_WEIGHT_MAP = new HashMap<>(); static { CHARACTER_WEIGHT_MAP.put('+', 1); CHARACTER_WEIGHT_MAP.put('-', 1); CHARACTER_WEIGHT_MAP.put('*', 2); CHARACTER_WEIGHT_MAP.put('/', 2); CHARACTER_WEIGHT_MAP.put('(', 3); CHARACTER_WEIGHT_MAP.put(')', 4); } public static Double eval(String exp) { exp = exp.replaceAll(" ", "")//干掉空格 .replaceAll("\\(-", "(0-");//转换负数(-2) -> (0-2),其中负数必须用括号括起来 SymbolPairChecker.check(exp); List<String> reversePolishExp = reversePolishExp(exp); ArrayStack<Double> numStack = new ArrayStack<>(); for (String s : reversePolishExp) { try { Double d = Double.parseDouble(s); numStack.push(d); } catch (NumberFormatException e){ Double b = numStack.pop(); Double a = numStack.pop(); switch (s){ case "+": numStack.push(a + b); break; case "-": numStack.push(a - b); break; case "*": numStack.push(a * b); break; case "/": numStack.push(a / b); break; } } } return numStack.pop(); } private static List<String> reversePolishExp(String exp) { List<String> list = new ArrayList<>(); ArrayStack<Character> stack = new ArrayStack<>(); StringBuilder num = new StringBuilder(); for (int i = 0; i < exp.length(); i++) { Character character = exp.charAt(i); String type = typeOfCharacter(character); if (type.equals("num")) { num.append(character); } else if (type.equals("op")) { if(num.length() > 0){ list.add(num.toString()); num = new StringBuilder(); } if (stack.isEmpty()) { //空栈则直接将操作符入栈 stack.push(character); } else { if (character == ')') { //如果是),依次打印栈中所有操作符,直到出现(,其中(需要弹出但不输出 Character toAppend = stack.pop(); while (toAppend != '(') { list.add(String.valueOf(toAppend)); toAppend = stack.pop(); } } else { //其他情况时,如果此次操作符权重高于或等于栈中操作符,本次操作符压栈;否则输出栈顶操作符 Character preCharacter = stack.peek(); while (preCharacter != '(' && CHARACTER_WEIGHT_MAP.get(character) <= CHARACTER_WEIGHT_MAP.get(preCharacter)) { list.add(String.valueOf(stack.pop())); if(stack.isEmpty()){ break; } preCharacter = stack.peek(); } } //本次操作符入栈 if(character != ')'){ stack.push(character); } } } } if(num.length() > 0){ list.add(num.toString()); } if(!stack.isEmpty()){ while (!stack.isEmpty()){ list.add(String.valueOf(stack.pop())); } } return list; } private static String typeOfCharacter(Character character) { switch (character) { case '+': case '-': case '*': case '/': case '(': case ')': return "op"; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': return "num"; default: throw new RuntimeException("Character not allowed here : " + character); } } public static void main(String[] args) { System.out.println(eval("((12+40)*2-4+((5-6/2)/(1+2)+100))/100")); System.out.println(eval("11+22*33+(44*55+66)*77")); System.out.println(eval("1/2-3*(4+5)")); } } 上面的方法有点笨,其实可以直接使用两个栈:一个操作数栈,一个操作符栈,一边解析一边计算,不过先转换成逆波兰表达式再计算,思路是非常清晰的,两个栈的方式效率更高。 #### 方法调用和递归 #### `Java`中任何一个程序片段几乎都是方法的嵌套调用:进入一个方法`A()`时,这个方法有各种参数、变量名称和对应的值;然后开始调用另外一个方法`A1()`,此时`A()`方法的现场推入一个栈中(`push()`),然后开始进入`A1()`方法,`A1()`方法结束后,再把`A()`方法的现场恢复出来(`pop()`),继续执行;如果`A1()`方法中还嵌套有其他方法,可以一样推到同一个栈中。 递归调的实现也很类似,即 f(x)=xf(x−1) f ( x ) = x f ( x − 1 ) 一个问题可以被划分成很多个相同的子问题,问题的难度是递减的,最简单的问题是一个常量,并且问题的个数是有限的。也就是说想要解决`f(x)`,只需要解决`f(x-1)`即可,依次类推直到`f(1)`为固定解,最后回退回去依次解决`f(2) f(3) ... f(x)`。一个典型的递归——阶乘的非递归实现: public static Long factorialStack(int n){ Stack<Integer> stack = new ArrayStack<>(); while(n > 0){ stack.push(n--); } Long result = 1L; while(!stack.isEmpty()){ result *= stack.pop(); } return result; } ## 队列 ## ### ADT ### 队列也是一种简单的表实现,只能在头部删除、尾部插入,先进先出。队列用于缓冲,都有容量,当队列满载是,新的入队请求将被阻止,直到有元素出队。 队列的操作一般有:入队、出队、判空、置空、判满、获取队首元素等。 ### 顺序队列 ### 队列可以用顺序表实现,维护两个指针`front`和`rear`,前者指向队首,后者指向队尾,如果每次入队出队操作只是简单地在尾节点天加或者头节点删除,只需要移动`front`和`rear`,时间复杂度为`O(1)`并不需要复制元素,但是`front`指针一直在向后移动,如果某个时刻`front`指针移动到了队尾,这是再入队肯定会得到一个数组越界的异常(假溢出);但是如果让`front`指针都保持在数组第一个元素,但每次出队都要复制元素,这并不是非常完美的实现方法([ArrayQueue.java][])。 有一种解决假溢出办法是将数组的最后一个节点和第一个节点在逻辑上相连,`front`和`rear`的维护不再是简单的`++`,而是如果两个指针任何一个到达数组末尾,直接跳到数组起始位置,每次入队时`rear = (rear + 1) / capacity`,每次出队时`front = (fron + 1) / capacity`,这样就可以保证是在逻辑上首尾相连的([SimpleArrayQueue.java][])。 ### 链式队列 ### 一个解决假溢出非常有效的方法是使用链表来实现队列,因为本身链表就有首尾节点,删除和添加元素的时间复杂度都是`O(1)`,不需要指针来维护,但是需要维护队列当前容量,以保证队列不溢出([LinkedQueue.java][])。 ## 顺序队列与链式队列 ## 因为队列是有固定容量的,基于`ADT`定义,没有其他查询操作,因此直接使用顺序栈可以节省空间,`Java`中的`ArrayBlokingQueue`就是顺序队列。 ## 队列的应用 ## 队列一般都用来做缓存,用来匹配生产和消费速度的不匹配。 比如打印机作业,很多人使用同一台打印机,如果有`10`个人同时提交打印请求,和明显打印机是忙不过来的,但是打印请求却可以提交成功的,这些请求被放在一个队列里,按照先到先服务的原则,一个个打印。 再比如`redis`的队列,消息的生产速度是很快的,比如批量发送优惠券,但是消息的消费速度却是很慢的,因为一条信息的发送取决于用户的状态、网络等,这时就可以把消息放到一个队列,让发送服务器慢慢消费。 [ArrayStack.java]: https://github.com/clyoudu/dsaaa/blob/master/src/main/java/github/clyoudu/stack/ArrayStack.java [ArrayQueue.java]: https://github.com/clyoudu/dsaaa/blob/master/src/main/java/github/clyoudu/quene/ArrayQueue.java [SimpleArrayQueue.java]: https://github.com/clyoudu/dsaaa/blob/master/src/main/java/github/clyoudu/quene/SimpleArrayQueue.java [LinkedQueue.java]: https://github.com/clyoudu/dsaaa/blob/master/src/main/java/github/clyoudu/quene/LinkedQueue.java
相关 js:数组实现队列和栈、栈实现队列、队列实现栈 *目录** 一、利用数组结构实现大小固定的队列和栈 二、仅用队列结构实现栈结构 三、仅用栈结构实现队列结构 四、总结 ------------------... 悠悠/ 2024年04月17日 05:55/ 0 赞/ 108 阅读
相关 栈和队列 20.[\[LeetCode\] Valid Parentheses 验证括号][LeetCode_ Valid Parentheses] 给定一个只包括 `'('`,`') Dear 丶/ 2024年02月19日 13:28/ 0 赞/ 79 阅读
相关 栈和队列 > 栈 是限定 仅在表尾 进行插入和删除操作的线性表 > > 队列 是只允许 在一端 进行 插入 操作、而在 另一端 进行 删除 操作的线性表 第一部分 相关定义 秒速五厘米/ 2023年07月14日 15:57/ 0 赞/ 149 阅读
相关 栈和队列 物理结构与逻辑结构 把物质层面的人体比作数据存储的物理结构,那么精神层面的人格则是数据存储的逻辑结构。逻辑结构是抽象的概念,它依赖于物理结构而存在。 ![在这里插入图 柔光的暖阳◎/ 2023年07月02日 03:24/ 0 赞/ 25 阅读
相关 实现栈和队列、用栈实现队列,用队列实现栈。 一、实现一个栈 就是一个指针下标,入栈加,出栈减。 / 我的栈 / public class MySt 一时失言乱红尘/ 2023年02月16日 12:15/ 0 赞/ 24 阅读
相关 栈和队列 栈: package Suan; public class Stack { private int size; 迈不过友情╰/ 2022年06月18日 07:54/ 0 赞/ 202 阅读
相关 队列组合(栈和队列) 题目描述 设计算法以求解从集合\{1…n\}中选取k(k<=n)个元素的所有组合。例如,从集合\{1…4\}中选取2个元素的所有组合的输出结果为:1 2,1 3,1 ╰半橙微兮°/ 2022年03月31日 07:42/ 0 赞/ 317 阅读
还没有评论,来说两句吧...