3.3_数据结构和算法复习-栈

本文用于本人技术学习和备忘。水平有限,如有错漏欢迎指正!

原创不易,如有转载请标注出处:https://blog.csdn.net/luyou0401/article/details/152208405

数据结构和算法复习-栈


基本定义

栈(Stack)是限定仅在表尾 进行插入和删除操作的线性表。表尾称为栈顶,表头称为栈底

栈的特性

  • 后进先出(LIFO - Last In First Out):最后入栈的元素最先出栈。
  • 操作受限:只能在栈顶进行插入(入栈)和删除(出栈)操作。
  • 动态大小:栈的大小可以动态变化(取决于实现方式)。

栈的ADT

text 复制代码
ADT Stack {
    数据对象:D = {a_i | a_i ∈ ElemSet, i=1,2,...,n, n≥0}
    数据关系:R = {<a_{i-1}, a_i> | a_{i-1}, a_i ∈ D, i=2,...,n}
    基本操作:
        Stack() ; // 初始化栈
        void push(E element); // 将元素压入栈顶
        E pop(); // 弹出并返回栈顶元素
        E peek(); // 查看栈顶元素但不弹出
        boolean isEmpty(); // 判断栈是否为空
        int size(); // 返回栈中元素个数
        void clear(); // 清空栈
}

栈的存储结构与实现

本文源码:https://gitcode.com/yaco/Yaco_CSDN_DSA.git


顺序栈(数组实现)

使用数组存储栈元素,需要维护栈顶指针。详见代码:

java 复制代码
package com.shuizd.linear_list.impl;

import com.shuizd.linear_list.Stack;

import java.util.EmptyStackException;

/**
 * 基于数组实现的栈
 * <p>
 * 该实现使用数组作为底层数据结构来存储栈元素。
 * 当数组容量不足时会自动扩容,当元素较少时会自动缩容以节省空间。
 * </p>
 *
 * @param <E> 栈中元素的类型
 * @author Yaco
 * @version 1.0.0
 */
public class ArrayStack<E> implements Stack<E> {
    private static final int DEFAULT_CAPACITY = 10;
    private E[] elements;
    private int top;

    /**
     * 创建默认容量的空栈
     */
    public ArrayStack() {
        this(DEFAULT_CAPACITY);
    }

    /**
     * 创建指定初始容量的空栈
     *
     * @param capacity 栈的初始容量
     */
    @SuppressWarnings("unchecked")
    public ArrayStack(int capacity) {
        elements = (E[]) new Object[capacity];
        top = -1;
    }

    /**
     * 将元素压入栈顶
     * <p>
     * 如果当前数组已满,则会自动扩容为原来大小的两倍。
     * </p>
     *
     * @param element 要压入栈的元素
     */
    @Override
    public void push(E element) {
        if (top == elements.length - 1) {
            resize(2 * elements.length);
        }
        elements[++top] = element;
    }

    /**
     * 弹出并返回栈顶元素
     * <p>
     * 如果弹出元素后数组使用率较低,则会自动缩容为原来大小的一半。
     * </p>
     *
     * @return 栈顶元素
     * @throws EmptyStackException 如果栈为空时调用此方法
     */
    @Override
    public E pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        E element = elements[top];
        elements[top--] = null; // 避免内存泄漏

        // 缩容
        if (top > 0 && top == elements.length / 4) {
            resize(elements.length / 2);
        }
        return element;
    }

    /**
     * 查看栈顶元素但不弹出
     *
     * @return 栈顶元素
     * @throws EmptyStackException 如果栈为空时调用此方法
     */
    @Override
    public E peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return elements[top];
    }

    /**
     * 判断栈是否为空
     *
     * @return 如果栈为空返回true,否则返回false
     */
    @Override
    public boolean isEmpty() {
        return top == -1;
    }

    /**
     * 返回栈中元素个数
     *
     * @return 栈中元素的个数
     */
    @Override
    public int size() {
        return top + 1;
    }

    /**
     * 清空栈中所有元素
     */
    @Override
    public void clear() {
        for (int i = 0; i <= top; i++) {
            elements[i] = null;
        }
        top = -1;
    }

    /**
     * 调整底层数组的大小
     *
     * @param newCapacity 新的数组容量
     */
    private void resize(int newCapacity) {
        @SuppressWarnings("unchecked")
        E[] newElements = (E[]) new Object[newCapacity];
        for (int i = 0; i <= top; i++) {
            newElements[i] = elements[i];
        }
        elements = newElements;
    }
    
    /**
     * 测试ArrayStack的基本功能
     *
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        // 创建一个整数栈
        ArrayStack<Integer> stack = new ArrayStack<>();
        
        // 测试栈是否为空
        System.out.println("栈是否为空: " + stack.isEmpty());
        System.out.println("栈的大小: " + stack.size());
        
        // 向栈中添加元素
        System.out.println("向栈中添加元素: 1, 2, 3, 4, 5");
        for (int i = 1; i <= 5; i++) {
            stack.push(i);
        }
        
        // 查看栈的状态
        System.out.println("栈是否为空: " + stack.isEmpty());
        System.out.println("栈的大小: " + stack.size());
        System.out.println("栈顶元素: " + stack.peek());
        
        // 弹出元素
        System.out.println("弹出元素:");
        while (!stack.isEmpty()) {
            System.out.println("弹出: " + stack.pop() + ", 剩余大小: " + stack.size());
        }
        
        // 测试栈空异常
        try {
            stack.pop();
        } catch (EmptyStackException e) {
            System.out.println("捕获到栈空异常: " + e.getClass().getSimpleName());
        }
        
        try {
            stack.peek();
        } catch (EmptyStackException e) {
            System.out.println("捕获到栈空异常: " + e.getClass().getSimpleName());
        }
        
        // 测试clear方法
        System.out.println("重新添加元素并测试clear方法");
        for (int i = 1; i <= 3; i++) {
            stack.push(i);
        }
        System.out.println("清空前栈的大小: " + stack.size());
        stack.clear();
        System.out.println("清空后栈的大小: " + stack.size());
        System.out.println("清空后栈是否为空: " + stack.isEmpty());
    }
}

链栈(链表实现)

使用单链表存储栈元素,头插法实现。详见代码:

java 复制代码
package com.shuizd.linear_list.impl;

import com.shuizd.linear_list.Stack;

import java.util.EmptyStackException;

/**
 * 基于链表实现的栈
 * <p>
 * 该实现使用单向链表作为底层数据结构来存储栈元素。
 * 链表头节点表示栈顶,这样可以保证入栈和出栈操作的时间复杂度都是O(1)。
 * </p>
 *
 * @param <E> 栈中元素的类型
 * @author Yaco
 * @version 1.0.0
 */
public class LinkedStack<E> implements Stack<E> {
    /**
     * 链表节点内部类
     *
     * @param <E> 节点中存储的元素类型
     */
    private static class Node<E> {
        E data;
        Node<E> next;

        /**
         * 创建一个新的节点
         *
         * @param data 节点存储的数据
         */
        Node(E data) {
            this.data = data;
        }
    }

    private Node<E> top;
    private int size;

    /**
     * 创建一个空栈
     */
    public LinkedStack() {
        top = null;
        size = 0;
    }

    /**
     * 将元素压入栈顶
     * <p>
     * 在链表头部插入新节点,时间复杂度为O(1)。
     * </p>
     *
     * @param element 要压入栈的元素
     */
    @Override
    public void push(E element) {
        Node<E> newNode = new Node<>(element);
        newNode.next = top;
        top = newNode;
        size++;
    }

    /**
     * 弹出并返回栈顶元素
     * <p>
     * 移除链表头节点并返回其数据,时间复杂度为O(1)。
     * </p>
     *
     * @return 栈顶元素
     * @throws EmptyStackException 如果栈为空时调用此方法
     */
    @Override
    public E pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        E element = top.data;
        top = top.next;
        size--;
        return element;
    }

    /**
     * 查看栈顶元素但不弹出
     *
     * @return 栈顶元素
     * @throws EmptyStackException 如果栈为空时调用此方法
     */
    @Override
    public E peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return top.data;
    }

    /**
     * 判断栈是否为空
     *
     * @return 如果栈为空返回true,否则返回false
     */
    @Override
    public boolean isEmpty() {
        return top == null;
    }

    /**
     * 返回栈中元素个数
     *
     * @return 栈中元素的个数
     */
    @Override
    public int size() {
        return size;
    }

    /**
     * 清空栈中所有元素
     */
    @Override
    public void clear() {
        top = null;
        size = 0;
    }
    
    /**
     * 测试LinkedStack的基本功能
     *
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        // 创建一个字符串栈
        LinkedStack<String> stack = new LinkedStack<>();
        
        // 测试栈是否为空
        System.out.println("栈是否为空: " + stack.isEmpty());
        System.out.println("栈的大小: " + stack.size());
        
        // 向栈中添加元素
        System.out.println("向栈中添加元素: A, B, C, D, E");
        stack.push("A");
        stack.push("B");
        stack.push("C");
        stack.push("D");
        stack.push("E");
        
        // 查看栈的状态
        System.out.println("栈是否为空: " + stack.isEmpty());
        System.out.println("栈的大小: " + stack.size());
        System.out.println("栈顶元素: " + stack.peek());
        
        // 弹出元素
        System.out.println("弹出元素:");
        while (!stack.isEmpty()) {
            System.out.println("弹出: " + stack.pop() + ", 剩余大小: " + stack.size());
        }
        
        // 测试栈空异常
        try {
            stack.pop();
        } catch (EmptyStackException e) {
            System.out.println("捕获到栈空异常: " + e.getClass().getSimpleName());
        }
        
        try {
            stack.peek();
        } catch (EmptyStackException e) {
            System.out.println("捕获到栈空异常: " + e.getClass().getSimpleName());
        }
        
        // 测试clear方法
        System.out.println("重新添加元素并测试clear方法");
        stack.push("X");
        stack.push("Y");
        stack.push("Z");
        System.out.println("清空前栈的大小: " + stack.size());
        stack.clear();
        System.out.println("清空后栈的大小: " + stack.size());
        System.out.println("清空后栈是否为空: " + stack.isEmpty());
    }
}

Java内置栈(Stack类)的使用

Java 内置的 ​​Stack 类​​ 是一种基于​​后进先出(LIFO)原则的数据结构,继承自 Vector类(属于 java.util包)。
核心特性

  • 继承关系
    • Stack 是 Vector的子类,因此继承了 Vector的所有方法(如 size()clear()),同时扩展了栈操作方法,如 push()pop()
  • 线程安全
    • 由于继承自 Vector,Stack 的所有方法均为同步方法,适合多线程环境,但单线程下可能带来性能开销。

核心方法​​:

方法 功能描述
push(E item) 将元素压入栈顶
pop() 移除并返回栈顶元素(若栈为空,抛出 EmptyStackException
peek() 返回栈顶元素但不移除(若栈为空,抛出 EmptyStackException
isEmpty() 检查栈是否为空
search(Object o) 返回对象在栈中的位置(基于1的索引,栈顶为1)

示例代码

java 复制代码
		/**
     * 测试Java内置Stack类的基本功能
     * <p>
     * Java内置的Stack类继承自Vector,是线程安全的栈实现。
     * 该方法演示了Java内置Stack类的基本操作。
     * </p>
     */
    public static void testJavaUtilStack() {
        // 创建Java内置Stack实例
        java.util.Stack<String> javaStack = new java.util.Stack<>();
        
        // 测试栈是否为空
        System.out.println("Java内置Stack测试:");
        System.out.println("栈是否为空: " + javaStack.isEmpty());
        System.out.println("栈的大小: " + javaStack.size());
        
        // 向栈中添加元素
        System.out.println("向栈中添加元素: A, B, C, D, E");
        javaStack.push("A");
        javaStack.push("B");
        javaStack.push("C");
        javaStack.push("D");
        javaStack.push("E");
        
        // 查看栈的状态
        System.out.println("栈是否为空: " + javaStack.isEmpty());
        System.out.println("栈的大小: " + javaStack.size());
        System.out.println("栈顶元素: " + javaStack.peek());
        
        // 弹出元素
        System.out.println("弹出元素:");
        while (!javaStack.isEmpty()) {
            System.out.println("弹出: " + javaStack.pop() + ", 剩余大小: " + javaStack.size());
        }
        
        // 测试栈空异常
        try {
            javaStack.pop();
        } catch (EmptyStackException e) {
            System.out.println("捕获到栈空异常: " + e.getClass().getSimpleName());
        }
        
        try {
            javaStack.peek();
        } catch (EmptyStackException e) {
            System.out.println("捕获到栈空异常: " + e.getClass().getSimpleName());
        }
        
        // 测试search方法(Java内置Stack特有)
        System.out.println("测试search方法:");
        javaStack.push("X");
        javaStack.push("Y");
        javaStack.push("Z");
        System.out.println("查找Y的位置: " + javaStack.search("Y"));  // 从栈顶开始计算位置
        System.out.println("查找A的位置: " + javaStack.search("A"));  // 不存在的元素返回-1
    }

测试结果

bash 复制代码
testJavaUtilStack();
==================================================
Java内置Stack测试:
栈是否为空: true
栈的大小: 0
向栈中添加元素: A, B, C, D, E
栈是否为空: false
栈的大小: 5
栈顶元素: E
弹出元素:
弹出: E, 剩余大小: 4
弹出: D, 剩余大小: 3
弹出: C, 剩余大小: 2
弹出: B, 剩余大小: 1
弹出: A, 剩余大小: 0
捕获到栈空异常: EmptyStackException
捕获到栈空异常: EmptyStackException
测试search方法:
查找Y的位置: 2
查找A的位置: -1

栈的实现对比

特性 顺序栈 链栈
存储结构 数组 链表
内存分配 连续内存 离散内存
扩容方式 需要重新分配 动态增长
空间效率 可能浪费 按需分配
时间效率 所有操作O(1) 所有操作O(1)

栈的操作复杂度分析

操作 时间复杂度 空间复杂度 说明
入栈(push) O(1) O(1) 直接在栈顶添加元素
出栈(pop) O(1) O(1) 直接移除栈顶元素
查看栈顶(peek) O(1) O(1) 读取栈顶元素值
判断空(isEmpty) O(1) O(1) 检查栈顶指针/引用
获取大小(size) O(1) O(1) 维护size变量
清空栈(clear) O(n) O(1) 需要遍历所有元素置空
查找(search) O(n) O(1) 需要遍历栈

栈的常用算法应用


逆波兰表达式

波兰表达式(Polish Notation)是由波兰数学家 ​​扬·武卡谢维奇在 1920 年提出的一种数学表达式表示方法。它的核心特点是操作符位于操作数之前​​(前缀表达式)或​​操作符位于操作数之后​​(后缀表达式 )。其中,​​后缀表达式​​(Reverse Polish Notation, RPN)更为常见,常用于计算机科学中的表达式求值和编译器设计。

波兰表达式的分类

  • 前缀表达式(Prefix Notation):操作符在操作数前面,例如:+ 3 4表示 3 + 4
  • 后缀表达式(Postfix Notation):操作符在操作数后面,例如:3 4 +表示 3 + 4
    实际应用中, 后缀表达式(逆波兰表达式 )更为常用,因此有时"波兰表达式"特指后缀表达式

例如

  • 中缀表达式:3 + 4 后缀表达式:3 4 +
  • 中缀表达式:(3 + 4) × 2 后缀表达式:3 4 + 2 ×

代码示例

java 复制代码
/**
     * 使用栈实现逆波兰表达式(后缀表达式)求值
     * <p>
     * 逆波兰表达式是一种后缀表达式,例如:"2 1 + 3 *" 等价于 ((2 + 1) * 3) = 9
     * 算法思路:
     * 1. 遍历表达式中的每个元素
     * 2. 如果是数字,则入栈
     * 3. 如果是运算符,则从栈中弹出两个数字进行运算,然后将结果入栈
     * 4. 最后栈中剩下的唯一数字就是表达式的结果
     * </p>
     *
     * @param tokens 逆波兰表达式的字符串数组形式
     * @return 表达式计算结果
     */
    public static int evalRPN(String[] tokens) {
        Stack<Integer> stack = new ArrayStack<>();

        for (String token : tokens) {
            switch (token) {
                case "+":
                    // 加法运算:弹出两个操作数,相加后将结果入栈
                    int b = stack.pop();
                    int a = stack.pop();
                    stack.push(a + b);
                    break;
                case "-":
                    // 减法运算:注意操作数顺序,是a-b而不是b-a
                    b = stack.pop();
                    a = stack.pop();
                    stack.push(a - b);
                    break;
                case "*":
                    // 乘法运算
                    b = stack.pop();
                    a = stack.pop();
                    stack.push(a * b);
                    break;
                case "/":
                    // 除法运算:注意操作数顺序,是a/b而不是b/a
                    b = stack.pop();
                    a = stack.pop();
                    stack.push(a / b);
                    break;
                default:
                    // 数字直接入栈
                    stack.push(Integer.parseInt(token));
                    break;
            }
        }

        // 最终栈中剩下的唯一元素就是表达式的结果
        return stack.pop();
    }
 public static void testEvalRPN() {
        // 测试用例1: ["2", "1", "+", "3", "*"] => ((2 + 1) * 3) = 9
        String[] tokens1 = {"2", "1", "+", "3", "*"};
        System.out.println("表达式: [\"2\", \"1\", \"+\", \"3\", \"*\"]");
        System.out.println("计算结果: " + evalRPN(tokens1));

        // 测试用例2: ["4", "13", "5", "/", "+"] => (4 + (13 / 5)) = 6
        String[] tokens2 = {"4", "13", "5", "/", "+"};
        System.out.println("\n表达式: [\"4\", \"13\", \"5\", \"/\", \"+\"]");
        System.out.println("计算结果: " + evalRPN(tokens2));

        // 测试用例3: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]
        // => ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 = 22
        String[] tokens3 = {"10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"};
        System.out.println("\n表达式: [\"10\", \"6\", \"9\", \"3\", \"+\", \"-11\", \"*\", \"/\", \"*\", \"17\", \"+\", \"5\", \"+\"]");
        System.out.println("计算结果: " + evalRPN(tokens3));
    }

测试结果

bash 复制代码
testEvalRPN();
==================================================
表达式: ["2", "1", "+", "3", "*"]
计算结果: 9

表达式: ["4", "13", "5", "/", "+"]
计算结果: 6

表达式: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]
计算结果: 22

括号匹配问题

问题分析

给定一个只包含括号字符的字符串(如 ()[]{}),判断字符串中的括号是否满足以下条件:

  • 所有括号正确闭合:每个左括号必须有对应的右括号。
  • 括号顺序合法:右括号必须与最近的未闭合左括号匹配。
  • 无冗余括号:字符串末尾不能有未闭合的左括号。
  • 示例:
    • 有效:(), ([]), {[()]}
    • 无效:)(, ([)], ((()

核心思路

使用​​栈(Stack)数据结构解决此问题,步骤如下:

  • 初始化一个空栈。
  • 遍历字符串的每个字符:
    • 遇到左括号(, [, {:将其压入栈。
    • 遇到右括号), ], }
      • 如果栈为空 → 直接返回 False(无左括号可匹配)。
      • 否则,弹出栈顶元素,检查是否与当前右括号匹配。
        • 若匹配 → 继续遍历。
        • 若不匹配 → 返回 False。
  • 遍历结束后:
    • 如果栈为空 → 所有括号闭合成功,返回 True。
    • 如果栈非空 → 存在未闭合的左括号,返回 False。

示例代码

java 复制代码
/**
     * 使用栈检查括号是否匹配
     * <p>
     * 括号匹配问题是栈的一个经典应用。给定一个包含括号的字符串,
     * 判断其中的括号是否正确匹配。
     * 算法思路:
     * 1. 遍历字符串中的每个字符
     * 2. 如果是左括号('(', '[', '{'),则将其压入栈中
     * 3. 如果是右括号(')', ']', '}'):
     *    - 如果栈为空,说明没有对应的左括号,返回false
     *    - 如果栈顶元素不是对应类型的左括号,返回false
     *    - 否则弹出栈顶元素,继续处理
     * 4. 字符串遍历完成后,如果栈为空,说明所有括号都正确匹配
     * </p>
     *
     * @param expression 包含括号的字符串表达式
     * @return 如果所有括号都正确匹配返回true,否则返回false
     */
    public static boolean isBalancedParentheses(String expression) {
        Stack<Character> stack = new ArrayStack<>();
        
        // 遍历表达式中的每个字符
        for (char ch : expression.toCharArray()) {
            // 如果是左括号,压入栈中
            if (ch == '(' || ch == '[' || ch == '{') {
                stack.push(ch);
            }
            // 如果是右括号,检查是否与栈顶的左括号匹配
            else if (ch == ')' || ch == ']' || ch == '}') {
                // 如果栈为空,说明没有对应的左括号
                if (stack.isEmpty()) {
                    return false;
                }
                
                char top = stack.pop();
                // 检查括号类型是否匹配
                if ((ch == ')' && top != '(') ||
                    (ch == ']' && top != '[') ||
                    (ch == '}' && top != '{')) {
                    return false;
                }
            }
            // 其他字符(字母、数字、运算符等)忽略
        }
        
        // 所有字符处理完后,如果栈为空说明所有括号都正确匹配
        return stack.isEmpty();
    }
/**
     * 测试括号匹配功能
     */
    public static void testBalancedParentheses() {
        System.out.println("括号匹配测试:");
        
        // 测试用例1: 简单匹配
        String expr1 = "()";
        System.out.println("\"" + expr1 + "\" 是否匹配: " + isBalancedParentheses(expr1));
        
        // 测试用例2: 多种括号匹配
        String expr2 = "()[]{}";
        System.out.println("\"" + expr2 + "\" 是否匹配: " + isBalancedParentheses(expr2));
        
        // 测试用例3: 嵌套匹配
        String expr3 = "{[()]}";
        System.out.println("\"" + expr3 + "\" 是否匹配: " + isBalancedParentheses(expr3));
        
        // 测试用例4: 复杂表达式匹配
        String expr4 = "((()))[]{[(()))]}";
        System.out.println("\"" + expr4 + "\" 是否匹配: " + isBalancedParentheses(expr4));
        
        // 测试用例5: 不匹配的情况1
        String expr5 = "(";
        System.out.println("\"" + expr5 + "\" 是否匹配: " + isBalancedParentheses(expr5));
        
        // 测试用例6: 不匹配的情况2
        String expr6 = ")(";
        System.out.println("\"" + expr6 + "\" 是否匹配: " + isBalancedParentheses(expr6));
        
        // 测试用例7: 不匹配的情况3
        String expr7 = "([)]";
        System.out.println("\"" + expr7 + "\" 是否匹配: " + isBalancedParentheses(expr7));
        
        // 测试用例8: 包含其他字符的表达式
        String expr8 = "a+(b*c)-{d+[e/f]}";
        System.out.println("\"" + expr8 + "\" 是否匹配: " + isBalancedParentheses(expr8));
    }

测试结果

bash 复制代码
testBalancedParentheses();
==================================================
括号匹配测试:
"()" 是否匹配: true
"()[]{}" 是否匹配: true
"{[()]}" 是否匹配: true
"((()))[]{[(()))]}" 是否匹配: false
"(" 是否匹配: false
")(" 是否匹配: false
"([)]" 是否匹配: false
"a+(b*c)-{d+[e/f]}" 是否匹配: true

单调栈应用

单调栈(Monotonic Stack)是一种特殊的数据结构,其核心特性是​​栈内元素保持严格递增或严格递减的顺序​​。它常用于解决一类与"最近更大/更小元素"相关的问题,在数组或序列中高效查找特定位置的极值或边界。

核心思想

单调栈通过维护栈的单调性,使得每个元素都能快速找到其左侧或右侧第一个比它大/小的元素。这种特性使其在以下场景中非常高效:

  • Next Greater Element (NGE)​​:找到每个元素右边第一个比它大的元素。
  • ​​Previous Greater Element (PGE)​​:找到每个元素左边第一个比它大的元素。
  • 最长递增/递减子序列:辅助寻找极值位置。
  • 括号匹配:优化某些特定情况的匹配效率。

单调栈的分类

根据栈的单调性方向,分为两种:

  • 递增单调栈:栈内元素从栈底到栈顶严格递增。
  • 递减单调栈​​:栈内元素从栈底到栈顶严格递减。

典型问题:Next Greater Element (NGE)

​​   问题描述​​:给定数组 nums,找到每个元素右边第一个比它大的元素。若不存在,输出 -1。

​​示例​​:

bash 复制代码
输入:nums = [2, 7, 3, 5, 4, 6, 8]
输出:[7, 8, 5, 6, 6, 8, -1]

单调栈解法​​:

  • 初始化一个空栈,用于存储数组下标。
  • 从右向左遍历数组(保证栈内元素是递增的)。
  • 对于当前元素 nums[i]:
    • 如果栈为空,说明右边没有更大元素,记录 -1。
    • 如果栈不为空且栈顶元素对应的值小于等于 nums[i],弹出栈顶元素(因为它们不可能是下一个更大元素)。
    • 重复上述步骤,直到栈为空或栈顶元素大于 nums[i]。
    • 将当前下标 i入栈,并记录结果。

代码示例

java 复制代码
		/**
     * 测试单调栈解决"下一个更大元素"问题(针对单个数组)
     */
    public static void testNextGreaterElements() {
        System.out.println("单调栈 - 下一个更大元素测试(单个数组):");

        // 测试用例
        int[] nums = {2, 7, 3, 5, 4, 6, 8};
        int[] result = nextGreaterElements(nums);
        System.out.println("输入: " + java.util.Arrays.toString(nums));
        System.out.println("输出: " + java.util.Arrays.toString(result));
    }
		/**
     * 使用单调栈解决"下一个更大元素"问题(针对单个数组)
     * <p>
     * 给定数组 nums,找到每个元素右边第一个比它大的元素。若不存在,输出 -1。
     * 算法思路:
     * 1. 从右向左遍历数组
     * 2. 维护一个单调递减栈
     * 3. 对于每个元素,弹出栈中所有小于等于当前元素的值
     * 4. 如果栈为空,表示没有更大的元素;否则栈顶元素就是下一个更大元素
     * 5. 将当前元素压入栈中
     * </p>
     *
     * @param nums 输入数组
     * @return 每个元素对应的下一个更大元素数组
     */
    public static int[] nextGreaterElements(int[] nums) {
        int[] result = new int[nums.length];
        Stack<Integer> stack = new ArrayStack<>();

        // 从右向左遍历数组
        for (int i = nums.length - 1; i >= 0; i--) {
            // 维护单调递减栈,弹出栈中所有小于等于当前元素的值
            while (!stack.isEmpty() && stack.peek() <= nums[i]) {
                stack.pop();
            }

            // 如果栈为空,表示没有更大的元素;否则栈顶元素就是下一个更大元素
            result[i] = stack.isEmpty() ? -1 : stack.peek();

            // 将当前元素压入栈中
            stack.push(nums[i]);
        }

        return result;
    }

测试结果

bash 复制代码
testNextGreaterElements();
==================================================
单调栈 - 下一个更大元素测试(单个数组):
输入: [2, 7, 3, 5, 4, 6, 8]
输出: [7, 8, 5, 6, 6, 8, -1]

栈的LeetCode高频面试题

题号 题目 难度 考点 LeetCode 链接
20 有效的括号 简单 栈的基本应用,括号匹配 https://leetcode.cn/problems/valid-parentheses/
42 接雨水 困难 单调栈或双指针,计算积水面积 https://leetcode.cn/problems/trapping-rain-water/
84 柱状图中最大的矩形 困难 单调栈经典应用,找左右边界 https://leetcode.cn/problems/largest-rectangle-in-histogram/
85 最大矩形 困难 将问题转化为柱状图最大矩形问题 https://leetcode.cn/problems/maximal-rectangle/
94 二叉树的中序遍历 简单 用栈模拟递归实现迭代遍历 https://leetcode.cn/problems/binary-tree-inorder-traversal/
150 逆波兰表达式求值 中等 栈用于后缀表达式求值 https://leetcode.cn/problems/evaluate-reverse-polish-notation/
155 最小栈 中等 设计支持常数时间获取最小元素的栈 https://leetcode.cn/problems/min-stack/
394 字符串解码 中等 栈处理嵌套结构,字符串处理 https://leetcode.cn/problems/decode-string/

栈的使用场景

  • 软件开发场景

    • 函数调用栈:程序执行时的函数调用关系。
    • 表达式求值:编译器中的算术表达式、逻辑表达式计算。
    • 撤销操作:编辑器、图形软件中的撤销/重做功能。
    • 浏览器历史:浏览器的前进后退功能。
    • 递归转非递归:用栈模拟递归调用过程。
  • 系统级应用

    • 内存管理:栈内存分配,局部变量存储。
    • 语法分析:编译器中的语法检查,XML/HTML标签匹配。
    • 路由算法:深度优先搜索(DFS)中的路径记录。
    • 回溯算法:迷宫问题、八皇后问题中的路径记录。

栈的选择策略

  • 解题思路模板

    • 识别栈特征:问题是否涉及嵌套结构、对称性、后进先出特性。
    • 选择栈类型:顺序栈还是链栈,是否需要特殊功能(如最小栈)。
    • 设计操作顺序:明确入栈、出栈的时机和条件。
    • 处理边界情况:空栈、栈满、异常输入等情况。
  • 建议重点练习:

    • 栈的基本操作实现。
    • 括号匹配类问题。
    • 单调栈应用。
    • 栈在树遍历中的应用。

本文用于本人技术学习和备忘。水平有限,如有错漏欢迎指正!

原创不易,如有转载请标注出处:https://blog.csdn.net/luyou0401/article/details/152208405

相关推荐
茉莉玫瑰花茶3 小时前
动态规划 - 两个数组的 dp 问题
算法·动态规划
微笑尅乐3 小时前
从暴力到滑动窗口全解析——力扣8. 字符串转换整数 (atoi)
算法·leetcode·职场和发展
火花怪怪3 小时前
LaMer结晶动力学模型
算法
legendary_bruce3 小时前
【22.2 增强决策树】
算法·决策树·机器学习
cookqq4 小时前
MongoDB源码delete分析oplog:从删除链路到核心函数实现
数据结构·数据库·sql·mongodb·nosql
ʚ希希ɞ ྀ4 小时前
用队列实现栈---超全详细解
java·开发语言·数据结构
老马啸西风4 小时前
力扣 LC27. 移除元素 remove-element
算法·面试·github
数智顾问4 小时前
中秋特别篇:使用QtOpenGL和着色器绘制星空与满月——从基础框架到光影渲染
算法
要一起看日出4 小时前
数据结构-----栈&队列
java·数据结构··队列