《Java数据结构》栈原来如此简单有趣

简介: 《Java数据结构》栈原来如此简单有趣

一、栈的基本认识

同链表和顺序表一样,栈也是用来存储逻辑关系为 "一对一" 数据线性数据结构,如图所示。

61819b9c498ed1f1570f8fec62f7b890.gif

从图 中我们看到,栈存储结构与之前所学的链表和顺序表有所差异,这缘于栈对数据 "存" 和 "取" 的过程有特殊的要求:


1. 栈只能从表的一端存取数据,另一端是封闭的;

2.在栈中,无论是存数据还是取数据,都必须遵循"先进后出"的原则,即最先进栈的元素最后出栈。拿图中的栈来说,从图中数据的存储状态可判断出,元素 1 是最先进的栈。因此,当需要从栈中取出元素 1 时,根据"先进后出"的原则,需提前将元素 3 和元素 2 从栈中取出,然后才能成功取出元素 1。


因此,我们可以给栈下一个定义,即栈是一种只能从表的一端存取数据且遵循 "先进后出" 原则的线性存储结构。


通常,栈的开口端被称为栈顶;相应地,封口端被称为栈底。因此,栈顶元素指的就是距离栈顶最近的元素,同理,栈底元素指的是位于栈最底部的元素

9ef772305d892de68d9cc40180829bda.gif


那么了解了栈的基本结构,在栈中我们如何实现元素的增加和删除呢?


从上面的图我们不难发现,在栈中我们无论是放元素还是取元素,顺序都是从上到下,即都是从栈顶开始、到栈底结束,所以增加和删除的也是通过栈顶来实现的,即我们删除一个元素就是把当前栈顶的元素给挪开,让该元素下面的元素变成新的栈顶;我们增加一个元素就是让新增加的元素变成我们新的栈顶


  1. 1.我们存放数据的过程就叫做入栈
  2. 2.我们取出数据的过程就叫做出栈


二、栈模拟实现:

别看说了这么多其实栈的实现很简单,不信你看

public class MyStack {
    public int[] elem;
    public int usedSize;
    MyStack() { // 栈的初始化
        elem = new int[]{1, 2, 3, 4};
        usedSize = elem.length;
    }
    public void push(int val) { // 默认相当于是尾插, 来入栈
        elem[usedSize] = val;
        ++usedSize;
    }
    public void pop() { // 出栈
        if (empty()) {
            System.out.println("当前栈为空,你的操作不合法!");
            return;
        }
        --usedSize;
    }
    // 判断栈是否为空, 空则返回true,非空则返回false
    public boolean empty() {
        if (usedSize == 0) return true;
        return false;
    }
    // 获取栈中元素的个数
    public int size() {
        return usedSize;
    }
    // 顺序打印当前栈中的所有元素
    public void printMyStack(){
        for (int i = 0; i < usedSize; i++) {
            System.out.print(elem[i] + " ");
        }
        System.out.println();
    }
}

上面我们就自己手动实现了一个栈,下面我们来用一下

public class test2 {
    public static void main(String[] args) {
        MyStack myStack = new MyStack(); // 初始化一个栈
        myStack.printMyStack();
        myStack.pop(); // 出栈
        System.out.println("====================");
        myStack.printMyStack();
        myStack.push(77); // 入栈
        System.out.println("====================");
        myStack.printMyStack();
        System.out.println("栈中的元素个数是:" + myStack.size());
    }
}

运行结果如图所示 

d6f8c8d1d6f4455da062bb93f1cc996a.png

三、栈的实战演练

🍉有效的括号

题目链接

有效的括号——力扣 

0790e68c7fc944b28cd59c5ab489da15.png

🌰本题的思路


只要所给字符串的左右两边是对称的——就合法

即字符串中左右括号的个数是相当的,注意这里所说的左右括号各有三种类型——"{}、[]、()"

我们不妨用栈来储存左括号,借助栈来看看左右括号的个数是否相等

再对该字符串进行遍历的过程,如果我们的右边的元素和储存在栈中的左括号相对于,则把当前栈中的所匹配的元素出栈,继续对字符串遍历

🍑当字符串遍历完后,如果栈中的元素刚好为空,则说明该字符串有效

🍑如果字符串遍历完后,栈中依然有元素,说明左括号多

🍑如果字符串还没遍历完,栈就为空了,说明右括号多


class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for(int i = 0; i < s.length(); ++i) {
            char ch = s.charAt(i);
            if (ch == '{' || ch == '[' || ch == '(') {
                stack.push(ch);  // 如果是左括号则入栈
            }
            else { // 如果是右括号则与栈中左括号相比,看是否相匹配
                if (stack.empty()) return false; // 当然如果右括号所对应的右边元素为空,自然是不匹配
                // 注意此时我们并没有取出栈中的元素(因为还不确定这对括号是否匹配),只是显示当前的堆栈元素
                char top = stack.peek();
                if ( (top == '{' && ch == '}') || (top == '[' && ch == ']') || (top == '(' && ch == ')') ) {
                    stack.pop();
                }
                else{
                    return false; // 说明此时右边的元素和栈中所存放的左边的元素不匹配
                }
            }
        }
        if (!stack.empty()) {
            return false;
        }
        return true;
    }
}

🍉逆波兰表达式

原题链接

76ea20ddf4bb4710b3bef5f019f6d218.png


对于这个题目,首先我们需要先补充一点知识

🌰中缀表达式转后缀表达式

比如(1 +(2 * 3))+ ((4 * 5) + 6)*  7)这样的中缀表达式要变成像123*+45*6+7*+这样的后缀表达式。

🍑步骤如下:

1、首先将中缀表达式的各个括号对都用不同的颜色清晰的标出来,像这样:

2e9ae88f700947baa11eb9407e676bb3.png

2、然后把运算符符移动到相邻的最近的括号对的后边,(先移动里面的运算符,再移动外面的运算符,然后从前往后的进行移动)


比如对于(1 +(2 * 3))就是先把*号移动——》(1 +(23)*)

然后再是+号——》(1(23)*)+  

3、 最后再把括号去掉才变成了123*+45*6+7*+这种形式


🌰总结一下就是:

1)先按照运算符的优先级对中缀表达式加括号,变成( ( a+(b*c) ) + ( ((d*e)+f) *g ) )

2)将运算符移到括号的后面,变成((a(bc)*)+(((de)*f)+g)*)+

3)去掉括号,得到abc*+de*f+g*+


🌰后缀表达式求值


那么如何像这种后缀表达式123*+45*6+7*+如何进行求值你呢?这就要借助我们的栈了。


1.建立一个空栈 stack。

2.遍历后缀表达式(此后将遍历到的字符称为 s)。

3.当 s 为数字时,将其压入 stack 栈。

4.当 s 为运算符时,弹出 stack 栈顶和次栈顶的两个数字,并将两个数字按照运算符 s 进行运算,然后将运算结果压入stack 栈 。

5.当遍历结束时,存留在栈 stack 中还剩下唯一一个数字,该数字便是运算结果


🍑下面用例子来解释一下:

假定待求值的后缀表达式为:6  5  2  3  + 8 * + 3  +  *,则其求值过程如下:

 

1)遍历表达式,遇到的数字首先放入栈中,此时栈如下所示:


55bb0b524c8c7bf330f032ca295bbbc7.png

2)然后遇到了  +  这个运算符,那么就把当前的栈顶和次栈顶取出来,进行加法运算,但要注意运算时候次栈顶再前面,栈顶再后面 ,即次栈顶+栈顶的顺序来进行运算(其他的减乘除也都是按照次栈顶、栈顶这样的前后位置来进行运算的)

3)再把刚才的运算结果5放到栈中,成为新的栈顶


0804df9c127aedcc3cf9eba7e310bf0b.png

4)然后又继续向后走,把接下来的数字8入栈,成为栈顶


ecc138253045494ff89a678f431fc396.png

5)遇到  * 号后,取出栈顶和次栈顶进行 * 的运算——》5,把新的运算结果作为新的栈顶放到栈中

6)这样当遍历结束时,存留在栈 stack 中还剩下唯一一个数字,该数字便是运算结果

class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack();
        for (int i = 0; i < tokens.length; ++i) {
            String s = tokens[i]; 
            if (charge(s)) { // 如果是运算符,则进行运算操作
                int num1 = stack.pop();
                int num2 = stack.pop();
                switch(s) { // 对从栈顶和次栈顶取出的元素进行运算
                    case "+":
                        stack.push(num2 + num1); // 把运算的结果放回栈中
                        break;
                    case "-":
                        stack.push(num2 - num1);
                        break;
                    case "*":
                        stack.push(num2 * num1);
                        break;
                    case "/":
                        stack.push(num2 / num1);
                        break;        
                }
            }
            else { // 不是运算符,就存在栈中
                stack.push(Integer.parseInt(s)); // 因为我们的栈中储存的是字符,所以要用Integer中的parseInt进行类型转换
            }
        }
        return(stack.pop()); // 栈中最后一个元素给出栈
    }
    public boolean charge(String str) { // 判断当前字符串是否是有效的运算符
        if (str.equals("+") || str.equals("-") || str.equals("*") || str.equals("/")) {
            return true;
        }
        return false;
    }
}

🍉栈的压入、弹出序列

原题链接栈的压入和弹出——牛客


ec2e654f8fb54b83ad75d13264577e73.png

 

详细的题解和思路可以看看这篇文章

栈的压入和弹出题解——牛客

import java.util.*;
public class Solution {
    public static boolean IsPopOrder(int [] pushA,int [] popA) {
        int j = 0;
        // 用到了一个辅助栈
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < pushA.length; i++) {
            stack.push(pushA[i]); // 把
            while (!stack.empty() && j < popA.length && stack.peek() == popA[j]) {
                stack.pop();
                ++j;
            }
        }
        if (stack.empty()) {
            return true;
        }
        return false;
    }
}

3bdd3cd96f1948969809a1679ba63d40.gif

相关文章
|
8天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
75 9
|
27天前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
62 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
17天前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
31 1
|
19天前
|
存储 Java
告别混乱!用Java Map优雅管理你的数据结构
【10月更文挑战第17天】在软件开发中,随着项目复杂度增加,数据结构的组织和管理至关重要。Java中的Map接口提供了一种优雅的解决方案,帮助我们高效、清晰地管理数据。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,有效提升了代码质量和维护性。
68 2
|
19天前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
50 2
|
2天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
2天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
15 6
|
8天前
|
存储 Java 索引
Java中的数据结构:ArrayList和LinkedList的比较
【10月更文挑战第28天】在Java编程世界中,数据结构是构建复杂程序的基石。本文将深入探讨两种常用的数据结构:ArrayList和LinkedList,通过直观的比喻和实例分析,揭示它们各自的优势与局限,帮助你在面对不同的编程挑战时做出明智的选择。
|
5天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
7天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
29 4
下一篇
无影云桌面