本文用于本人技术学习和备忘。水平有限,如有错漏欢迎指正!
原创不易,如有转载请标注出处: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(); // 清空栈
}
栈的存储结构与实现
顺序栈(数组实现)
使用数组存储栈元素,需要维护栈顶指针。详见代码:
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()
。
- Stack 是
- 线程安全
- 由于继承自
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