栈是一种先进后出(LIFO, Last In First Out) 的线性数据结构,它只允许在一端(称为栈顶)进行插入和删除操作。栈的特性使其在很多场景中发挥重要作用,如表达式求值、函数调用、括号匹配、深度优先搜索等。本文将详细讲解栈的概念、实现及应用。

1.栈的基本概念
核心术语
- 栈顶(Top):允许插入和删除操作的一端
- 栈底(Bottom):固定的,不允许操作的一端
- 压栈(Push):在栈顶插入元素的操作
- 弹栈(Pop):从栈顶删除元素的操作
- 栈空(Empty):栈中没有任何元素的状态
- 栈满(Full):栈中元素达到最大容量的状态
栈的特性
- 元素的插入和删除只能在栈顶进行;
- 元素的访问顺序是 "先进后出",最后进入的元素最先被访问;
- 栈是一种操作受限的线性表。
栈的基本操作
push(element):向栈顶插入元素pop():移除并返回栈顶元素peek():返回栈顶元素但不移除isEmpty():判断栈是否为空isFull():判断栈是否已满(仅数组实现)size():返回栈中元素个数clear():清空栈中所有元素
2.栈的两种实现方式
一、基于数组实现
数组实现的栈使用固定大小的数组存储元素,实现简单但容量固定。
java
import java.util.EmptyStackException;
/**
* 基于数组的栈实现
* @param <T> 栈中元素的类型
*/
public class ArrayStack<T> {
private T[] stack; // 存储栈元素的数组
private int top; // 栈顶指针,指向栈顶元素的索引
private int capacity; // 栈的容量
// 构造指定容量的栈
@SuppressWarnings("unchecked")
public ArrayStack(int capacity) {
this.capacity = capacity;
stack = (T[]) new Object[capacity];
top = -1; // 栈空时,栈顶指针为-1
}
// 构造默认容量为10的栈
public ArrayStack() {
this(10);
}
/**
* 压栈操作:向栈顶添加元素
* @param element 要添加的元素
* @throws StackOverflowError 如果栈已满
*/
public void push(T element) {
if (isFull()) {
throw new StackOverflowError("栈已满,无法添加元素");
}
stack[++top] = element; // 先移动栈顶指针,再添加元素
}
/**
* 弹栈操作:移除并返回栈顶元素
* @return 栈顶元素
* @throws EmptyStackException 如果栈为空
*/
public T pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
T element = stack[top];
stack[top--] = null; // 帮助垃圾回收
return element;
}
/**
* 获取栈顶元素但不移除
* @return 栈顶元素
* @throws EmptyStackException 如果栈为空
*/
public T peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return stack[top];
}
/**
* 判断栈是否为空
* @return 如果栈为空返回true,否则返回false
*/
public boolean isEmpty() {
return top == -1;
}
/**
* 判断栈是否已满
* @return 如果栈已满返回true,否则返回false
*/
public boolean isFull() {
return top == capacity - 1;
}
/**
* 获取栈中元素的个数
* @return 栈中元素的个数
*/
public int size() {
return top + 1;
}
/**
* 清空栈
*/
public void clear() {
// 清空元素,帮助垃圾回收
for (int i = 0; i <= top; i++) {
stack[i] = null;
}
top = -1;
}
/**
* 打印栈中的元素
*/
public void printStack() {
if (isEmpty()) {
System.out.println("栈为空");
return;
}
System.out.print("栈元素(从栈底到栈顶):");
for (int i = 0; i <= top; i++) {
System.out.print(stack[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
ArrayStack<Integer> stack = new ArrayStack<>(5);
// 压栈
stack.push(1);
stack.push(2);
stack.push(3);
stack.printStack(); // 输出: 栈元素(从栈底到栈顶):1 2 3
// 查看栈顶元素
System.out.println("栈顶元素: " + stack.peek()); // 输出: 3
// 弹栈
System.out.println("弹出元素: " + stack.pop()); // 输出: 3
stack.printStack(); // 输出: 栈元素(从栈底到栈顶):1 2
// 栈大小
System.out.println("栈大小: " + stack.size()); // 输出: 2
// 继续压栈
stack.push(4);
stack.push(5);
stack.push(6);
stack.printStack(); // 输出: 栈元素(从栈底到栈顶):1 2 4 5 6
// 测试栈满
try {
stack.push(7);
} catch (StackOverflowError e) {
System.out.println(e.getMessage()); // 输出: 栈已满,无法添加元素
}
// 清空栈
stack.clear();
System.out.println("栈是否为空: " + stack.isEmpty()); // 输出: true
}
}
二、基于链表的栈实现
链栈是一种基于链表实现的栈,其特点是无需事先分配固定长度的存储空间,栈的长度可以动态增长或缩小,避免了顺序栈可能存在的空间浪费和存储溢出问题。
链栈中的每个元素称为"节点",每个节点包括两个部分:数据域和指针域。数据域用来存储栈中的元素值,指针域用来指向栈顶元素所在的节点。
链栈的基本操作包括入栈、出栈、获取栈顶元素和遍历等,相比顺序栈而言,链栈的实现难度稍高,但其在某些情况下有着更好的灵活性和效率,特别适用于在动态存储空间较为紧缺的场合。
注意:如果栈的使用过程中元素变化不可预料,那么最好使用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈。
链栈可以分为单链栈和双链栈:
单链栈使用单链表实现,每个节点只含有一个指向下一个节点的指针。因此,单链栈只能从栈顶进行插入和删除操作。
双链栈使用双向链表实现,每个节点同时包含指向前一个节点和后一个节点的指针。因此,双链栈既可以从栈顶进行插入和删除操作,也可以从栈底进行插入和删除操作,使得操作更加灵活。
java
import java.util.EmptyStackException;
/**
* 基于链表的栈实现
* @param <T> 栈中元素的类型
*/
public class LinkedStack<T> {
// 节点类
private static class Node<T> {
T data; // 节点数据
Node<T> next; // 指向下一个节点的引用
public Node(T data) {
this.data = data;
this.next = null;
}
}
private Node<T> top; // 栈顶节点
private int size; // 栈中元素的个数
// 构造空栈
public LinkedStack() {
top = null;
size = 0;
}
/**
* 压栈操作:向栈顶添加元素
* @param element 要添加的元素
*/
public void push(T element) {
Node<T> newNode = new Node<>(element);
newNode.next = top; // 新节点指向当前栈顶
top = newNode; // 更新栈顶为新节点
size++;
}
/**
* 弹栈操作:移除并返回栈顶元素
* @return 栈顶元素
* @throws EmptyStackException 如果栈为空
*/
public T pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
T data = top.data;
top = top.next; // 更新栈顶为下一个节点
size--;
return data;
}
/**
* 获取栈顶元素但不移除
* @return 栈顶元素
* @throws EmptyStackException 如果栈为空
*/
public T peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return top.data;
}
/**
* 判断栈是否为空
* @return 如果栈为空返回true,否则返回false
*/
public boolean isEmpty() {
return top == null;
}
/**
* 获取栈中元素的个数
* @return 栈中元素的个数
*/
public int size() {
return size;
}
/**
* 清空栈
*/
public void clear() {
top = null;
size = 0;
}
/**
* 打印栈中的元素
*/
public void printStack() {
if (isEmpty()) {
System.out.println("栈为空");
return;
}
System.out.print("栈元素(从栈顶到栈底):");
Node<T> current = top;
while (current != null) {
System.out.print(current.data + " ");
current = current.next;
}
System.out.println();
}
public static void main(String[] args) {
LinkedStack<String> stack = new LinkedStack<>();
// 压栈
stack.push("A");
stack.push("B");
stack.push("C");
stack.printStack(); // 输出: 栈元素(从栈顶到栈底):C B A
// 查看栈顶元素
System.out.println("栈顶元素: " + stack.peek()); // 输出: C
// 弹栈
System.out.println("弹出元素: " + stack.pop()); // 输出: C
stack.printStack(); // 输出: 栈元素(从栈顶到栈底):B A
// 栈大小
System.out.println("栈大小: " + stack.size()); // 输出: 2
// 继续压栈
stack.push("D");
stack.push("E");
stack.printStack(); // 输出: 栈元素(从栈顶到栈底):E D B A
// 清空栈
stack.clear();
System.out.println("栈是否为空: " + stack.isEmpty()); // 输出: true
}
}
三、两种实现方式的对比
|----------|---------------|-----------------|--------------|--------------|
| 实现方式 | 优点 | 缺点 | 时间复杂度 | 适用场景 |
| 数组实现 | 实现简单访问速度快内存连续 | 容量固定,可能溢出扩容成本高 | 所有操作都是 O (1) | 已知最大容量对性能要求高 |
| 链表实现 | 容量动态扩展没有溢出问题 | 实现较复杂需要额外空间存储指针 | 所有操作都是 O (1) | 未知最大容量需要灵活扩展 |
3.java 中栈的实现
Java 标准库中提供了java.util.Stack类,但该类是遗留类,继承自Vector,存在一些设计缺陷,不推荐使用。
推荐使用java.util.Deque接口的实现类作为栈使用,如ArrayDeque:
java
import java.util.Deque;
import java.util.ArrayDeque;
public class StackExample {
public static void main(String[] args) {
// 使用ArrayDeque作为栈
Deque<Integer> stack = new ArrayDeque<>();
// 压栈
stack.push(1);
stack.push(2);
stack.push(3);
// 查看栈顶元素
System.out.println("栈顶元素: " + stack.peek()); // 输出: 3
// 弹栈
System.out.println("弹出元素: " + stack.pop()); // 输出: 3
// 遍历栈
System.out.print("栈元素: ");
while (!stack.isEmpty()) {
System.out.print(stack.pop() + " "); // 输出: 2 1
}
}
}
ArrayDeque作为栈使用的优势:
- 性能优于
Stack类 - 接口设计更合理
- 没有同步开销(
Stack是线程安全的,带来额外开销)
4.栈的应用
1.函数递归调用
函数递归调用时,计算机会把函数调用时需要的参数和返回地址等信息放入栈中,函数执行完毕后再从栈中取回这些信息。
以汉诺塔为例:
java
public static void main(String[] args) {
Hanoi(6,'A','B','C');
}
private static int count = 0; //统计步数
//汉诺方法
public static void Hanoi(int n,char A,char B,char C){
//当n为1时,直接移动
if(n == 1){
System.out.println("第"+ ++count + "步:"+A +"-->"+C);
} else {
//当n不为1时
//首先 为了将第n个盘子从A移到C 可先将第n-1个盘子借助C从A移到B
Hanoi(n - 1, A, C, B);
System.out.println("第" + ++count + "步:" + A + "-->" + C);
//然后再将第n-1个盘子借助A从B移到C
Hanoi(n - 1, B, A, C);
}
}
2.括号匹配
java
public static void main(String[] args) {
String expr1 = "((a + b) * (c - d))";
String expr2 = "((a + b) * [c - d})";
String expr3 = "a + b) * (c - d";
System.out.println(expr1 + " 括号匹配: " + isBalanced(expr1)); // true
System.out.println(expr2 + " 括号匹配: " + isBalanced(expr2)); // false
System.out.println(expr3 + " 括号匹配: " + isBalanced(expr3)); // false
}
//检查括号匹配
public static boolean isBalanced(String expression){
Deque<Character> stack = new ArrayDeque<>();
for(char c : expression.toCharArray()){
//如果是左括号,入栈
if(c == '(' || c == '{' || c == '['){
stack.push(c);
}
//如果是右括号
else if(c == ')' || c == '}' || c == ']'){
if(stack.isEmpty()){
return false;
}
//弹出栈顶元素进行匹配
char top = stack.pop();
if(top == '(' && c != ')' || top == '{' && c != '}' || top == '[' && c!= ']'){
return false;
}
}
}
return stack.isEmpty();
}
3.表达式求值
java
/**
* 使用栈求后缀表达式(逆波兰表达式)的值
*/
public class ExpressionEvaluation {
public static int evaluatePostfix(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String token : tokens) {
// 如果是运算符,弹出两个元素进行运算
if (token.equals("+") || token.equals("-") ||
token.equals("*") || token.equals("/")) {
int b = stack.pop(); // 注意弹出顺序,后弹出的是第一个操作数
int a = stack.pop();
int result = 0;
switch (token) {
case "+":
result = a + b;
break;
case "-":
result = a - b;
break;
case "*":
result = a * b;
break;
case "/":
result = a / b; // 假设除数不为0
break;
}
stack.push(result);
}
// 如果是数字,直接入栈
else {
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}
public static void main(String[] args) {
// 表达式: 3 + 4 * 2 / (1 - 5)
// 后缀表达式: 3 4 2 * 1 5 - / +
String[] tokens = {"3", "4", "2", "*", "1", "5", "-", "/", "+"};
System.out.println("表达式结果: " + evaluatePostfix(tokens)); // 输出: 1
}
}
4.浏览器历史记录
栈可以用于实现浏览器的前进和后退功能:
- 访问新页面时,将当前页面压入历史栈
- 点击后退按钮时,从历史栈弹出页面
- 可以使用两个栈实现前进功能
5.栈性能的分析
栈的所有基本操作(push、pop、peek、isEmpty)的时间复杂度都是O(1),因为这些操作只涉及栈顶元素,不需要遍历整个栈。
栈的空间复杂度是O(n),其中 n 是栈中元素的数量,因为需要存储所有元素。