数据结构——栈

简介: 数据结构——栈

一、理解栈



1. 栈的定义


栈是限定仅在表尾进行插入和删除操作的线性表。

栈中的数据的特点:先进后出,后进先出。

在下图我们可以看到,我们只能对栈顶进行插入和操作,通俗的说,栈只能实现尾插尾删。


79085bd155a8492c8f504bf05016d3b3.png


2. 什么是 Java 虚拟机栈


Java 虚拟机栈是 JVM 中的一块内存,该内存一般用来存放,局部变量等等…


8eb9ca025f84449797b3760950b35ac3.png


3. 什么是栈帧


调用函数的时候,我们会为这个函数开辟一块内存,叫做栈帧,而开辟内存的场所就在 Java 虚拟机栈 之中。


二、测试 Java 实现栈的方法



测试一


程序清单1:


//测试 push、pop、peek、isEmpty 方法
public class Test1 {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
        stack.push(1);
        stack.push(3);
        stack.push(5);
        stack.push(7);
        stack.push(9);
        System.out.println(stack);
        System.out.println(stack.pop()); //弹出栈顶的元素,并删除此元素
        System.out.println(stack);
        System.out.println(stack.peek()); //查看栈顶的元素,不删除此元素
        System.out.println(stack.peek());
        System.out.println(stack);
        System.out.println(stack.isEmpty());
    }
}


输出结果:


7294edb0749c4fd0976055b4271b9fb0.png



测试二


程序清单2:


//测试 search 方法
public class Test2 {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
        stack.push(1);
        stack.push(3);
        stack.push(9);
        stack.push(7);
        stack.push(9);
        System.out.println(stack.search(9));
        System.out.println(stack.search(3));
        System.out.println(stack.search(2));
        //search() 方法从栈顶开始找元素,
        //如果找到栈顶第一个元素就返回1,找到第二个元素就返回2...
        //找不到就返回-1
    }
}


输出结果:


c52dd9b673574b4683d66efd25d786a5.png


三、通过顺序表实现自己的栈



创建一个类 Stack 表示栈中的情况


程序清单3:


import java.util.Arrays;
public class Stack{
    public int[] elem;
    public int usedSize;
    public Stack() {
        elem = new int[10];
    }
    //入栈
    public void push(int data){
        if(isFull() == true){
            expansion();
        }
        elem[usedSize] = data;
        usedSize++;
    }
    //判断栈是否已满
    public boolean isFull(){
        if(usedSize == elem.length){
            return true;
        }else {
            return false;
        }
    }
    //扩容
    public void expansion(){
        elem = Arrays.copyOf(elem,elem.length*2);
    }
    //移除栈顶的一个元素
    public int pop(){
        if(isEmpty() == true){
            throw new RuntimeException("栈为空!");
        }
        int ret = elem[usedSize-1];
        elem[usedSize-1] = 0;
        usedSize--;
        return ret;
    }
    //访问栈顶的一个元素
    public int peek(){
        if(isEmpty() == true){
            throw new RuntimeException("栈为空!");
        }
        return elem[usedSize-1];
    }
    //判断栈是否为空
    public boolean isEmpty(){
        if(usedSize == 0){
            return true;
        }else {
            return false;
        }
    }
    //寻找栈中的某个元素
    public int search(int findData){
        int size = usedSize;
        for (int i = usedSize - 1; i >= 0 ; i--) {
            if(elem[i] == findData){
                return size - i ;
            }
        }
        return -1;
    }
    //打印栈中的所有元素
    public void print(){
        for (int i = 0; i < usedSize; i++) {
            System.out.print(elem[i] + " ");
        }
        System.out.println();
    }
}


创建一个类 Test 用来测试对栈的操作



测试一


程序清单4:


public class Test4 {
    public static void main(String[] args) {
        Stack stack = new Stack();
        stack.push(1);
        stack.push(3);
        stack.push(5);
        stack.push(7);
        stack.push(9);
        stack.print();
        System.out.println(stack.pop());
        stack.print();
        System.out.println(stack.peek());
        System.out.println(stack.peek());
        stack.print();
        System.out.println(stack.isEmpty());
    }
}


输出结果:


5205bade4bb245848c31ab7e78e14b9f.png


测试二


程序清单5:


public class Test5 {
    public static void main(String[] args) {
        Stack stack = new Stack();
        stack.push(1);
        stack.push(3);
        stack.push(9);
        stack.push(7);
        stack.push(9);
        System.out.println(stack.search(9));
        System.out.println(stack.search(3));
        System.out.println(stack.search(2));
    }
}


输出结果:


702a678c84344b6a867e7fd62bb3e363.png


思考用什么结构实现栈最优?


刚刚我们自己通过顺序表实现了栈的结构,那么,我们可以使用单链表实现栈的结构吗?


答:可以。但是,有区别。


通过顺序表(数组实现栈时),入栈和出栈的复杂度都为 O(1),因为我们只要考虑尾插、尾删即可,很方便就能对栈顶的元素进行直接操作。


但是,通过单链表实现栈时,当我们头插头删的时候,入栈和出栈的复杂度都为 O(1),而我们进行尾插尾删的时候,我们都要进行遍历链表,找到链表的最后一个节点,此时入栈和出栈的复杂度都为 O(n) 。


而我们通过双向链表实现栈时,不论是头插头删、尾插尾删,入栈和出栈的复杂度都为 O(1),因为双向链表某个节点总有前驱节点的地址。


四、通过OJ题深入理解栈



题目一 出栈的输出序列判断


  1. 一个栈的入栈顺序是 a b c d e,则栈不可能的输出序列是:( C )


A. e d c b a

B. d e c b a

C. d c e a b

D. a b c d e


一个数据可以入栈,也同时可以出栈,先入栈的数据可以事先出来,事先入栈的元素也可以最后出来。并且遵循先进后出原则。


题目二 栈的压入、弹出序列


牛客网链接


程序清单6:


import java.util.*;
public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        Stack<Integer> stack = new Stack<>();
        int j = 0;
        for(int i=0; i<pushA.length; i++){
            stack.push( pushA[i] );
            while( !stack.isEmpty() && stack.peek() == popA[j] ){
                stack.pop();
                j++;
            }
        }
        return stack.isEmpty();
    }
}


思路:


① 理解 pushA 数组是压栈数组,popA 数组是出栈数组。

② 通过 for 循环遍历 pushA 数组,不断地往 stack 栈中放入数据,数据通过数组下标 i 记录。

③ 与此同时,通过 while 循环判断栈最顶部的数据是否与 popA 数组的 j 下相等,若相等,则执行出栈操作。

④ 返回的时候,若栈为空,那么就符合出栈的逻辑,反之,不符合数据结构的逻辑。


图解分析:


下图为两种情况,只要考虑到这两种情况,那么就能将此题做出来了。


7b83922935434bb797d857302d77bf91.png


这里,我们需要注意一下程序清单1中,while 循环的条件:


while( !stack.isEmpty() && stack.peek() == popA[j] )


其中一条就是栈中的数据不能为空,否则我们使用 stack.peek( ) 访问栈中数据时会抛出异常,因为我们猜想:栈在源代码中,就是通过数组实现的,如果数组长度为0,我们再去访问元素,这就会造成数组越界!下图是我通过 IDEA 验证出来的结果:


3136fad44e984e54aac22ab5e414e3c4.png


题目三 中缀表达式 与 后缀表达式


先说明一下中缀表达式和后缀表达式之间的转换。

如下图:中缀表达式:(1 + 4) * 3 - 2


1. 中缀表达式 转 后缀表达式


① 在每一层运算加上圆括号(本身已有圆括号就不用重复加了)

② 将每个操作符移至每一层圆括号的后面

③ 把所有圆括号去掉


314ba8b80542473ba4f91698e14df1d5.png


2. 通过后缀表达式求出中缀表达式的值


(1 + 4) * 3 - 2  //中缀表达式
14 + 3 * 2 - //后缀表达式


利用栈:


① 将后缀表达式放入一个字符串数组中,开始遍历这个数组

② 创建一个栈

③ 遇到数字就入栈

④ 遇到操作符就出栈,出栈就进行运算,先出栈的放在操作符的右边,后出栈放在操作符的左边。将出栈的两个元素计算后,继续入栈,并重复以上操作。


cce427b527bb4d86a6ae29b6d3c67106.png


3. OJ 题

其中后缀表达式又称为逆波兰表达式


leetcode 150


程序清单7:


class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack<>();
        for(int i =0;i<tokens.length; i++){
            String val = tokens[i];
            if(judge(val) == false){
                int n = Integer.parseInt(val);
                stack.push(n);
            }else{
                int num2 = stack.pop();
                int num1 = stack.pop();
                switch(val){
                    case "+":
                        stack.push(num1 + num2);
                        break;
                    case "-":
                        stack.push(num1 - num2);
                        break;
                    case "*":
                        stack.push(num1 * num2);
                        break;
                    case "/":
                        stack.push(num1 / num2);
                }
            }
        }
        return stack.pop();
    }
    public boolean judge(String val){
        if(val.equals("+") || val.equals("-") || val.equals("*") || val.equals("/")){
            return true;
        }else{
            return false;
        }
    }
}


思路:


① 创建一个数组 tokens,里面都是字符串类型,表示我们需要操作的数据。

② 模拟一个栈 stack,准备用来存储整数。

③ 通过 for 循环遍历数组 tokens,并用 judge( ) 方法来判断每次拿到的是整数还是操作数 [ 加减乘除 ]。

④ 如果拿到的是整数,我们通过 stack.push( ) 方法进行入栈操作。

⑤ 如果拿到的是操作数,我们通过 stack.pop( ) 方法出栈操作,并使用栈顶的前两个整数进行数据运算。

⑥ 最终,我们 return 的值,也就是栈中最后一个值。


图解分析:


a7e547f10bb54759946a6c4733e31060.png



题目四 有效的括号


leetcode 20


程序清单8:


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.isEmpty()){
                  //如果还未遍历完字符串,栈中已为空
                  //也就是右括号多于左括号的情况,直接返回 false
                    return false;
                }
                char top = stack.peek();
                //判断左右括号是否匹配
                if(top == '(' && ch == ')' || top == '{' && ch == '}' || top == '[' && ch == ']'){
                    stack.pop();
                }else{
                    return false; //如果左右括号不匹配,直接返回 false
                }
            }
        }
        //当遍历完字符串时,栈中还有元素,也就是左括号多于右括号的情况,直接返回 false
        if(!stack.isEmpty()){
            return false;
        }
        return true;
    }
}


思路:


正常情况:

((( )))

([ ])


考虑三种特殊情况

① ((( ))

当遍历完字符串时,栈中还有元素,也就是左括号多于右括号的情况,直接返回 false

② (( )))

如果还未遍历完字符串,栈中已为空,也就是右括号多于左括号的情况,直接返回 false

③ ([ )]

如果左右括号不匹配,直接返回 false


分析图解:


32eca8cb9d0f428c9a4e93b59ae69660.png


题目五 最小栈


方法一:利用最小栈


leetcode 155


程序清单9:


class MinStack {
    Stack<Integer> stack = new Stack<>();//普通栈
    Stack<Integer> minStack = new Stack<>();//最小栈
    public MinStack() {
    }
    public void push(int val) {
        int topVal = stack.push(val);
        //若最小栈为空时,往最小栈放元素
        //若最小栈不为空,就要判断情况,
        //如果普通栈的栈顶元素值 > 最小栈的栈顶元素值,那么就不往最小栈放元素
        if(minStack.isEmpty() || topVal <= minStack.peek()){
            minStack.push(topVal);   
        }
    }
    public void pop() {
        int topVal = stack.pop();
        //如果两个栈栈顶的元素是相同的,那么就删除最小栈的栈顶元素
        if(topVal == minStack.peek()){
          minStack.pop();
        }
    }
    public int top() {
        return stack.peek();
    }
    public int getMin() {
      return minStack.peek();
    }
}


思路:


(1)入栈时


① 创建两个栈,一个是普通栈 stack ,一个是最小栈 minStack。

② 遇到的每个元素都要往普通栈中放,与此同时,若最小栈为空时,往最小栈放元素;若最小栈不为空,且普通栈的栈顶元素值 <= 最小栈的栈顶元素值,那么就往最小栈放元素。因为我们要保证栈顶为最小元素,方便我们拿出元素。


(2)出栈时


① 首先肯定要将普通栈的栈顶元素弹出,与此同时,若普通栈栈顶与最小栈栈顶的元素是相同的,那么就删除最小栈的栈顶元素。

试想,假设普通栈移除的是最小的元素1,如果不把最小栈的元素1移除,那么最后 getMin() 方法返回的就很可能是大于1的元素,这样就不符合逻辑了。因此我们可以把最小栈想象成一个虚拟的容器,它的存在只是用来记录普通栈的元素。


不管我们实现入栈操作,还是出栈操作,目的只有一个:最小栈的栈顶永远是最小的元素。因为在 getMin( ) 方法中,我们必须实现【 在常数时间内检索到最小元素的栈 】。


图解分析:


4d11c5e6c1814a68961fb51c992780d3.png


方法二:直接利用顺序表实现栈


import java.util.*;
class MinStack {
    ArrayList<Integer> list = new ArrayList<>();
    public MinStack() {
    }
    public void push(int val) {
        list.add(val);
    }
    public void pop() {
        list.remove( list.size()-1 );
    }
    public int top() {
        return list.get( list.size()-1 );
    }
    //遍历顺序表,找到最小值
    public int getMin() {
        int minVal = list.get(0);
        for (Integer x:list) {
            if(minVal > x){
                minVal = x;
            }
        }
        return minVal;
    }
}


目录
相关文章
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
137 9
|
25天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
24 1
|
12天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
30 5
|
28天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
1月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
1月前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
50 4
|
2月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
39 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
1月前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
2月前
初步认识栈和队列
初步认识栈和队列
61 10
|
2月前
数据结构(栈与列队)
数据结构(栈与列队)
22 1