后入能先出,一文搞懂栈

目录

    • 什么是栈
    • 数组实现
    • 链表实现
    • 栈能这么玩
    • 总结

什么是栈

栈在我们日常编码中遇到的非常多,很多人对栈的接触可能仅仅局限在 递归使用的栈 和 StackOverflowException,栈是一种后进先出的数据结构(可以想象生化金字塔的牢房和生化角斗场的狗洞)。

image-20231102234445811

栈(stack)是一种运算受限的线性数据结构,它具有以下特点:

1. 运算受限: 栈限定仅在表尾进行插入和删除操作,这一端被称为栈顶,而另一端称为栈底。这限制了对栈的操作,只能按照后进先出(LIFO,Last-In-First-Out)的原则进行插入和删除操作。插入操作又称为进栈、入栈或压栈,它将新元素放到栈顶,使之成为新的栈顶元素;删除操作又称为出栈或退栈,它将栈顶元素删除,使其相邻的元素成为新的栈顶元素。

2. 线性表: 栈也是一种线性表,它表示数据元素之间的逻辑关系是线性的。虽然具体实现可以使用数组或链表等不同的物理存储结构,但逻辑上各个元素之间是相邻的,操作也是按照顺序进行的。

3. 栈顶和栈底: 栈的逻辑结构中有栈顶和栈底的概念。栈顶表示可以进行插入和删除操作的一端,通常与数组的末尾或链表的头部有关。栈底则是相对的另一端,用于限制操作的另一端。

4. 栈的应用: 栈在计算机科学和编程中有广泛的应用,例如程序执行调用堆栈、四则运算表达式求值、非递归算法实现、括号匹配问题、浏览器历史、内存分配、任务管理等的解决。掌握栈是非常重要的,它是必须了解的数据结构之一。

栈可以使用数组或链表来实现,选择合适的实现方式取决于具体的应用场景和性能需求。数组实现的栈通常更适合于需要固定大小的栈(当然也可以进行扩容),而链表实现的栈可以动态扩展,适用于不确定大小的栈。在栈的操作中,栈顶元素是非常关键的,因为它在插入和删除操作中起着重要作用。

总之,栈是一个非常有用的数据结构,它在计算机科学中扮演着重要的角色,了解它的特性和应用对于编程和算法设计至关重要。

对于一个栈的接口,我们简易定义如下:

public interface Stack<T> {
    void push(T item);      // 压栈
    T pop();               // 弹栈
    T peek();              // 获取栈顶元素
    boolean isEmpty();     // 判断栈是否为空
    int size();            // 返回栈的大小
}

数组实现

数组实现的栈用的比较多,我们经常刷题也会用数组去实现一个简单的栈去解决简单的问题。

结构设计

对于数组来说,我们模拟栈的过程很简单,因为栈是后进先出,我们很容易在数组的末尾进行插入和删除。所以我们选定末尾为栈顶。所以对于一个栈所需要的基础元素是 一个array[]数组和一个size表示大小,还需要一个负载因子表示数组的大小。

push入栈操作

  • 如果数组满了,需要扩容
  • size位置赋值, array[size++] = data;

image-20231105213018107

pop弹出栈并返回首位

  • 如果栈不为空,可以弹出。return array[--size];

如下图,当栈中还剩1,2,3,4执行pop操作,栈顶变为3的位置并且返回4

image-20231105213649409

peek返回栈顶

  • peek操作时返回栈顶不弹出,所以栈不为空时候return data[size-1]即可。

数组实现:

import java.util.EmptyStackException;

public class SeqStack<T> implements Stack<T> {

    private T array[];
    private int size;
    private static final int DEFAULT_CAPACITY = 10;

    public SeqStack() {
        this.size = 0;
        array = (T[]) new Object[DEFAULT_CAPACITY];
    }

    @Override
    public void push(T data) {
        if (size == array.length) {
            // 如果数组已满,扩展数组
            resizeArray();
        }
        array[size++] = data;
    }

    @Override
    public T pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        // 下面可以写成 return array[--size];
        T data = array[size - 1];
        size--;
        return data;
    }

    @Override
    public T peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return array[size - 1];
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int size() {
        return size;
    }

    private void resizeArray() {
        int newCapacity = (int) (array.length * 2);
        T[] newArray = (T[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newArray[i] = array[i];
        }
        array = newArray;
    }
}

链表实现

栈可以使用数组或链表来实现,两种思路如下:

  1. 链表尾部作为栈顶: 在数组实现中,栈的操作是在尾部进行插入和删除。链表中即使使用尾指针可以提高尾部插入效率,但删除操作仍然需要查找前驱节点。要实现高效的删除操作,需要使用双向链表,这增加了整个结构的复杂性。
  2. 链表头部作为栈顶: 在这种实现中,栈的设计不带头节点的单链表(不需要哑结点),所有操作都在链表的头部进行。头部插入删除都很方便效率比较高,编写代码也很简单。

基础结构

public class LinkedStack<T> implements Stack<T> {
    private Node<T> top;
    private int size;

    public LinkedStack() {
        top = null;
        size = 0;
    }

    private static class Node<T> {
        T data;
        Node<T> next;
        public Node(T data) {
            this.data = data;
            this.next = null;
        }
    }
  //其他方法
}

push入栈

与不带头结点单链表头插入一致

  • 创建新节点
  • 新节点的next指向栈顶节点top
  • 栈顶节点top指向新节点,表示这个节点为新的栈顶节点
  • size++

部分操作流程如下图

image-20231105221137828

pop弹出

与不带头结点单链表头插入一致

  • 判断是否为空
  • 记录头结点top的值data
  • 头结点top指向top.next
  • size–,返回前面记录的值data

部分操作流程如下图

image-20231105222129654

peek返回栈顶

不为空的时候返回 top.data即可

链表实现:

import java.util.EmptyStackException;

public class LinkedStack<T> implements Stack<T> {
    private Node<T> top;
    private int size;

    public LinkedStack() {
        top = null;
        size = 0;
    }

    private static class Node<T> {
        T data;
        Node<T> next;

        public Node(T data) {
            this.data = data;
            this.next = null;
        }
    }

    @Override
    public void push(T item) {
        Node<T> newNode = new Node<>(item);
        newNode.next = top;
        top = newNode;
        size++;
    }

    @Override
    public T pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        T data = top.data;
        top = top.next;
        size--;
        return data;
    }

    @Override
    public T peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return top.data;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int size() {
        return size;
    }
}

栈能这么玩

既然上面详细讲解设计栈,这里来两道栈非常经典非常经典的例题(非常高频,很容易忘,又很重要,普通问题就不放的)

力扣20有效的括号:

题意:给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。

示例 :

输入: "()[]{}"
输出: true

示例 :

输入: "([)]"
输出: false

分析:
括号类的问题是经典栈类问题,肯定要想到用栈处理。判断一个字符串满不满足一个有效的字符串,就要看它是不是都能组成对。

从单个括号对来说,((,))都是不满足的,只有()才可满足,即一左一右。

从多个括号对来说 {[(字符串还可接受任意无限([,{的括号。但是如果向左的括号只能先接收)括号(变成{[)。

从上面可以看作一种相消除的思想。例如(({[()()]}))字符串遍历时候可以这样处理:

  • (({[(下一个)消掉成(({[
  • (({[(下一个)消掉成(({[
  • (({[下一个]消掉成(({
  • (({下一个}消掉成((
  • ((下一个)消掉成(
  • (下一个)消掉成 这样就满足题意

每次操作的时候都判断剩余有效括号最顶部那个括号是否能够和遍历的相消除,这个过程利用栈判断当前是加入栈还是消除顶部,到最后如果栈为空说明满足,否则不满足,当然具体括号要对应,具体实现代码为:

public boolean isValid(String s) {
    Stack<Character> stack = new LinkedStack<Character>();
    for (int i = 0; i < s.length(); i++) {
        char te = s.charAt(i);
        if (te == ']') {
            if (!stack.isEmpty() && stack.pop() == '[')
                continue;
            else {
                return false;
            }
        } else if (te == '}') {
            if (!stack.isEmpty() && stack.pop() == '{')
                continue;
            else {
                return false;
            }
        } else if (te == ')') {
            if (!stack.isEmpty() && stack.pop() == '(') {
                continue;
            } else {
                return false;
            }
        } else {
            stack.push(te);
        }
    }
    return stack.isEmpty();
}

当然,JDK自带的栈用起来不快,可以用数组优化:

public boolean isValid(String s) {
    char a[] = new char[s.length()];
    int index = -1;
    for (int i = 0; i < s.length(); i++) {
        char te = s.charAt(i);
        if (te == ']') {
            if (index >= 0 && a[index] == '[')
                index--;
            else {
                return false;
            }
        } else if (te == '}') {
            if (index >= 0 && a[index] == '{')
                index--;
            else {
                return false;
            }
        } else if (te == ')') {
            if (index >= 0 && a[index] == '(')
                index--;
            else {
                return false;
            }
        } else {
            a[++index] = te;
        }
    }
    return index == -1;
}

力扣32最长有效括号(困难)

题目描述:给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。

示例 :

输入: “(()”
输出: 2
解释: 最长有效括号子串为 “()”

示例 :

输入: “)()())”
输出: 4
解释: 最长有效括号子串为 “()()”

方案一暴力

这种题核心思想就是使用栈模拟。本题的话更简单一点因为只有()两种括号,使用暴力的时候就可以循环每次找到最长的有效括号。而括号匹配的时候可以直接终止的情况是)右括号多出无法匹配。

例如())(到第三个不可能和前面相连。如果来(只需要期待后面能够来),一个)可以和一个(组成一对,消除栈中的一个(

当然,在具体的实现上,我们用数组模拟栈,实现代码为:

public int longestValidParentheses(String s) {
    char str[] = s.toCharArray();//字符数组
    int max = 0;
    for (int i = 0; i < str.length - 1; i++) {
        int index = -1;
        if (max >= str.length - i)
            break;
        for (int j = i; j < str.length; j++) {
            if (str[j] == '(') {
                index++;
            } else {
                if (index < 0) {
                    i = j;
                    break;
                } else {
                    index--;
                }
            }
            if (index == -1 && (j - i + 1 > max)) {
                max = j - i + 1;
            }
        }
    }
    return max;
}

这个复杂度太高,我们看看如何用栈优化。

方案二栈优化

如何将这道题从一个O(n^2)的时间复杂度优化到O(n)?这其实非常简单,只需要注意处理的过程。让我们首先考虑一些可能的最大情况。

  • ( ) ) ( ) ( ( ) ( ) ) 最大为后面部分(空格分开)
  • ( ) ( ) ( ( ( ) 最大为前面部分
  • ( ( ( ( ( ( ) ( ) ( ) ( ) 最大为后面部分

在处理这道题时,我们会注意到不同类型的括号可能会有一些区别:
(:左括号一旦出现那么他就期待一个)进行匹配,但它的后面可能有)并且在这中间有很多其他括号对。
):右扩号有两种情况:

  • 一种是当前已经超过左括号前面已经不可能连续了。例如( ) ) ( )第三个括号出现已经使得整个串串不可能连续,最大要么在其左面要么再其右面。 你可以理解其为一种清零初始机制。
  • 另一种情况)就是目标栈中存在(可与其进行匹配。匹配之后要叠加到消除后平级的数量上,并且判断是否是最大值。(下面会解释)

具体实现的思路上,就是使用一个int数组标记当前层级(栈深)有正确的括号数量。 模拟一次栈行为从左向右,遇到)太多(当前栈中不存在(进行匹配)就将数据清零重新开始。这样一直到最后。你可以把它看成台接,遇到(就上一个台阶并清零该新台阶,遇到)就下一个台阶并且把数量加到下降后的台阶上。具体可以看下面图片模拟的过程:
( ) ( ( ) ( ) ( ( ) ) )

image-20231105224429516

具体实现代码为:

public static int longestValidParentheses(String s) {
    int max = 0;
    int value[] = new int[s.length() + 1];
    int index = 0;
    for (int i = 0; i < s.length(); i++) {
        if (s.charAt(i) == '(') {
            index++;
            value[index] = 0;
        } else {//")"
            if (index == 0) {
                value[0] = 0;
            } else {
                value[index - 1] += value[index--] + 2;//叠加
                if (value[index] > max)//更新
                    max = value[index];
            }
        }
    }
    return max;
}

用栈也可以实现,但是效率比数组略低:

public int longestValidParentheses(String s) {
  int maxans = 0;
  Stack<Integer> stack = new Stack<>();
  stack.push(-1);
  for (int i = 0; i < s.length(); i++) {
    if (s.charAt(i) == '(') {//(将当前的 
      stack.push(i);
    } else {
      stack.pop();
      if (stack.empty()) {
        stack.push(i);
      } else {//i-stack.peek就是i是出现的总个数 peek是还没匹配的个数
        maxans = Math.max(maxans, i - stack.peek());
      }
    }
  }
  return maxans;
}

总结

到这里,本文对栈的介绍就结束了,相信你可以手写个栈并且可以小试牛刀解决括号匹配问题!当然栈能解决的问题还有很多比如接雨水问题、二叉树非递归遍历等等,有些重要的还会再总结。

系列仓库地址:https://github.com/javasmall/bigsai-algorithm
csdn专栏:数据结构与算法专栏

写一篇原创不易,还请点赞、收藏、关注三连支持一下!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/123758.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

欧科云链:成本与规模之辨——合规科技如何赋能香港Web3生态?

作为国际金融中心&#xff0c;香港近两年来在虚拟资产及Web3领域频频发力。秉持着“稳步创新”的基本逻辑&#xff0c;香港在虚拟资产与Web3领域已建立一定优势&#xff0c;但近期各类风险事件的发生则让业界的关注焦点再次转向“安全”与“合规”。 在香港FinTech Week前夕&a…

强力解决使用node版本管理工具 NVM 出现的问题(找不到 node,或者找不到 npm)

强力解决使用node版本管理工具 NVM 出现的问题&#xff08;找不到 node&#xff0c;或者找不到 npm&#xff09; node与npm版本对应关系 nvm是好用的Nodejs版本管理工具&#xff0c; 通过它可以方便地在本地调换Node版本。 2020-05-28 Node当前长期稳定版12.17.0&#xff0c;…

基于SSM+Vue的随心淘网管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

git and svn 行尾风格配置强制为lf

git CLI配置&#xff1a; // 提交时转换为LF&#xff0c;检出时转换为CRLF git config --global core.autocrlf true // 提交时转换为LF&#xff0c;检出时不转换 git config --global core.autocrlf input // 提交检出均不转换 git config --global core.autocrlf f…

线性代数(四)| 解方程 齐次性 非齐次性 扩充问题

文章目录 1 方程解的个数2 解方程步骤2.1 齐次性方程组2.2 非齐次方程组 3 一些扩充问题 系数矩阵 增广矩阵 A m n X B A_{mn}XB Amn​XB 1 方程解的个数 m 代表有m个方程 n代表有n个未知数 系数矩阵的秩与增广矩阵的秩不同 无解 若相同 &#xff0c;如系数矩阵的秩和未知…

HDFS系统权限详解

一&#xff0c;HDFS超级用户 启动namenode的用户就是HDFS中的超级用户 如图所示 HDFS中&#xff0c;也是有权限控制的&#xff0c;其控制逻辑和Linux文件系统的完全一致 但是不同的是&#xff0c;两个系统的Supergroup不同(超级用户不同) Linux的操作用户是root HDFS文件系统的…

压缩软件 7-Zip VS WinZips?

7-zip在联想应用商店给强烈推荐&#xff1f; 要说它好用还行&#xff0c;但每次压缩都显示网络连接失败等异常广告信息。 相反好用的7-ZIP必须鼠标点击右键点击更多才能够看到&#xff0c;这次更新体验也太差了吧&#xff1f; 用户放在第一位&#xff1f; 要不是更新后一直推…

【方法】如何取消PDF文件的“打开密码”?

我们知道&#xff0c;PDF文件可以设置“打开密码”&#xff0c;保护文件不被随意打开&#xff0c;那如果后续不需要了&#xff0c;要怎么取消“打开密码”呢&#xff1f;不清楚的小伙伴可以试试小编分享的3种方法&#xff01; 方法1&#xff1a;使用PDF编辑器 PDF编辑器不仅可…

Matlab使用cftool进行曲线拟合

第一步&#xff0c;导入要拟合的输入和输出数据 导入excel时&#xff0c;如果作为列矢量导入&#xff0c;则会将excel的数据按列导入&#xff0c;并且&#xff0c;默认将第一行的变量名作为每一列的矢量名。 第二步&#xff0c;打开插件curve fitting 在应用程序里打开&#…

【Kurbernetes资源管理】陈述式资源管理方式

陈述式 一、 理论部分1.1 管理K8s资源的基本方法1.1.1 陈述式资源管理方式1.1.2声明式资源管理方式1.1.3 GUI式资源管理方法 1.2 陈述式资源管理方式1.2.1 Kubelet工具简介1.2.2 kubectl 的基本语法1.2.3 Kubectl工具的自动补全功能 1.3 Kubernetes Service1.4 Service 的类型(…

最新Cocos Creator 3.x 如何动态修改3D物体的透明度

Cocos Creator 3.x 的2D UI有个组件UIOpacity组件可以动态修改UI的透明度,非常方便。很多同学想3D物体上也有一个这样的组件来动态的控制与修改3D物体的透明度。今天基于Cocos Creator 3.8 来实现一个可以动态修改3D物体透明度的组件Opacity3D。 对啦&#xff01;这里有个游戏…

机器学习股票大数据量化分析与预测系统 - python 计算机竞赛

文章目录 0 前言1 课题背景2 实现效果UI界面设计web预测界面RSRS选股界面 3 软件架构4 工具介绍Flask框架MySQL数据库LSTM 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 机器学习股票大数据量化分析与预测系统 该项目较为新颖&am…

MSSQL 配置ORACLE ​链接服务器

在有些场景&#xff0c;我们需要整合其他异构数据库的数据。我们可以使用代码去读取&#xff0c;经过处理后&#xff0c;再将数据保存到MSSQL数据库中。如果数据量比较大&#xff0c;但处理的逻辑并不复杂的情况下&#xff0c;这种方式就不是最好的办法。这时可以使用使用链接服…

CSDN每日一题学习训练——Java版(逆序输出、Z 字形变换、输出每天是应该学习还是休息还是锻炼)

版本说明 当前版本号[20231108]。 版本修改说明20231108初版 目录 文章目录 版本说明目录逆序输出题目解题思路代码思路参考代码 Z 字形变换题目解题思路代码思路参考代码 输出每天是应该学习还是休息还是锻炼题目代码思路参考代码 逆序输出 题目 如&#xff1a;abcd1234&…

【714. 买卖股票的最佳时机含手续费】

目录 一、题目解析二、算法原理三、代码实现 一、题目解析 二、算法原理 三、代码实现 class Solution { public:int maxProfit(vector<int>& prices, int fee) {int nprices.size();vector<vector<int>> dp(n,vector<int>(2));dp[0][0]-prices[0…

网络安全深入学习第八课——正向代理(工具:ReGeorg)

文章目录 一、环境配置二、开始模拟1、拿下跳板机的Webshell权限&#xff0c;并上传shell文件1.1、查看跳板机网络环境1.2、查看arp表 2、使用ReGeorg来建立连接2.1、生产ReGeorg隧道文件2.2、上传ReGeorg隧道的PHP脚本到跳板机2.3、连接隧道2.4、尝试浏览器连接 3、使用Proxif…

微服务使用指南

微服务使用指南 1.初识微服务 微服务可以认为是一种分布式架构的解决方案&#xff0c;提供服务的独立性和完整性&#xff0c;做到服务的高内聚、低耦合。 目前服务架构主要包含&#xff1a;单体架构和分布式架构。 1.1 单体架构 单体架构&#xff1a;把所有业务功能模块都…

【代码随想录】算法训练计划16

【代码随想录】算法训练计划04 1、111. 二叉树的最小深度 题目&#xff1a; 给定一个二叉树&#xff0c;找出其最小深度。 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 说明&#xff1a;叶子节点是指没有子节点的节点。 思路&#xff1a; 用递归&#xff0…

在已有的虚拟环境中升级python版本

对于现有的虚拟环境&#xff0c;想升级python版本方法&#xff0c;试了无数的方法终于找对了。 1.首先activate对应的虚拟环境&#xff0c;然后输入下面的命令&#xff1a; conda install python3.8 建议加上镜像源 ​conda install python3.8 -i https://pypi.tuna.tsingh…

css实现进度条

预期样式 方法一 <script setup> import { ref } from "vue"; // import ScreenLeft from "./ScreenLeft/index.vue"; const width ref("76.5%"); </script><template>Screen<div class"progress-contain">…