思维导图
栈(Stack)是一种数据结构,遵循后进先出(LIFO)原则。在java中Stack在java.util.Stack中。
一.常用方法的使用
1. push(E item):把元素压入栈顶。
代码示例:
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
}
}
2. pop():弹出并返回栈顶元素。
代码示例:
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
int topElement = stack.pop();
System.out.println("弹出的元素:" + topElement);
}
}
注意:如果栈为空,会抛出EmptyStackException异常。
3. peek():查看栈顶元素,但不弹出。
代码示例:
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
int top = stack.peek();
System.out.println("栈顶元素:" + top);
}
}
注意:如果栈为空,会抛出EmptyStackException异常。
4. empty():判断栈是否为空,返回true或false。
代码示例:
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
boolean isEmpty = stack.empty();
System.out.println("栈是否为空:" + isEmpty);
}
}
二.细节问题
1.空栈操作
在调用pop和peek方法前,一定要先检查栈是否为空。如果对空栈执行pop或peek操作,会抛出EmptyStackException异常。
正确写法:
Stack<Integer> stack = new Stack<>();
if (!stack.isEmpty()) {
Integer topElement = stack.peek();
// 进一步处理栈顶元素
}
2.同步问题
java.util.Stack是同步的,这在多线程环境下可能会带来性能开销。如果不需要线程安全,可以考虑使用非同步的数据结构或者采取其他同步策略以提高性能。但如果错误地假设它是非同步的,在多线程环境下可能会导致数据不一致等问题。
代码示例:
// 错误示例:在多线程环境下未正确同步对栈的操作
Stack<Integer> stack = new Stack<>();
Thread t1 = new Thread(() -> {
stack.push(1);
});
Thread t2 = new Thread(() -> {
stack.pop();
});
t1.start();
t2.start();
3.为什么Stack是同步的?
三.栈的模拟实现
1.顺序栈(源码实现也是顺序栈)
代码示例:
//实现类
public class ArrayStack<T> {
private T[] stack;
private int top;
private int capacity;
public ArrayStack(int capacity) {
this.capacity = capacity;
stack = (T[]) new Object[capacity];
top = -1;
}
public boolean isEmpty() {
return top == -1;
}
public boolean isFull() {
return top == capacity - 1;
}
public void push(T item) {
if (isFull()) {
throw new RuntimeException("Stack is full");
}
stack[++top] = item;
}
public T pop() {
if (isEmpty()) {
return null;
}
return stack[top--];
}
public T peek() {
if (isEmpty()) {
return null;
}
return stack[top];
}
}
//测试类
public class Test {
public static void main(String[] args) {
ArrayStack<Integer> stack = new ArrayStack<>(10);
stack.push(1);
stack.push(2);
System.out.println(stack.isFull());
System.out.println(stack.isEmpty());
System.out.println(stack.pop());
System.out.println(stack.peek());
}
}
运行结果:
2.链式栈
代码示例:
//实现类
class StackNode<T> {
T data;
StackNode<T> next;
public StackNode(T data) {
this.data = data;
this.next = null;
}
}
class LinkedStack<T> {
private StackNode<T> top;
public LinkedStack() {
top = null;
}
public boolean isEmpty() {
return top == null;
}
public void push(T item) {
StackNode<T> newNode = new StackNode<>(item);
newNode.next = top;
top = newNode;
}
public T pop() {
if (isEmpty()) {
return null;
}
T item = top.data;
top = top.next;
return item;
}
public T peek() {
if (isEmpty()) {
return null;
}
return top.data;
}
}
//测试类
public class Main {
public static void main(String[] args) {
LinkedStack<Integer> stack = new LinkedStack<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("栈顶元素:" + stack.peek());
System.out.println("弹出元素:" + stack.pop());
System.out.println("弹出元素:" + stack.pop());
stack.push(4);
while (!stack.isEmpty()) {
System.out.println("弹出元素:" + stack.pop());
}
}
}
使用链式栈需要注意内存管理
如果使用自定义的链式栈,要注意内存泄漏问题。当从栈中弹出节点时,如果没有正确地处理节点的引用,可能会导致被弹出的节点无法被垃圾回收器回收,从而占用不必要的内存。
class LinkedStack<T> {
// 需要做的事
public T pop() {
if (isEmpty()) {
return null;
}
T item = top.data;
StackNode<T> oldTop = top;
top = top.next;
// 解除对被弹出节点的引用,帮助垃圾回收
oldTop.next = null;
return item;
}
//需要做的事
}
四.顺序栈链式栈的优缺点
1.顺序栈
优点:
1. 简单直观:实现相对容易,操作也比较直接,易于理解和使用。
2. 随机访问:如果是基于数组实现的顺序栈,可以通过索引快速访问特定位置的元素,例如获取栈顶元素的操作非常高效。
3. 内存连续:存储在连续的内存空间中,有利于 CPU 缓存的利用,可能会提高访问速度。
缺点:
1. 固定容量:通常需要预先指定栈的大小,如果栈的大小设置不当,可能会导致空间浪费或者栈溢出。当栈的容量不够时,需要进行扩容操作,这可能涉及到创建新的数组并复制元素,比较耗时。
2. 灵活性差:难以动态地调整大小,对于一些不确定大小的应用场景可能不太适用。
二、链式栈
优点:
1. 动态大小:可以根据需要动态地增加或减少栈的大小,无需预先指定固定的容量,具有很高的灵活性。
2. 内存分配灵活:节点可以在内存中的任意位置分配,不会受到连续内存空间的限制。
缺点:
1. 额外开销:每个节点需要额外的空间来存储指向下一个节点的引用,相比顺序栈会占用更多的内存。
2. 访问效率:由于节点在内存中不一定连续存储,无法像顺序栈那样通过索引快速访问元素,访问特定位置的元素需要遍历链表,效率较低。
五.顺序栈链式栈的区别
1. 数据存储方式
顺序栈通常使用数组来存储元素,元素在内存中是连续存储的。 链式栈使用节点(通常是自定义的类)通过链接的方式存储元素,节点在内存中可以是分散存储的。
2. 内存管理:
顺序栈在创建时需要指定容量,可能会存在空间浪费或栈溢出的情况。如果栈满了需要扩容,可能涉及到创建新数组和复制元素的操作,比较耗时。
链式栈可以根据需要动态地分配和释放内存,更加灵活,但每个节点需要额外的空间来存储指向下一个节点的引用。
3. 操作效率:
顺序栈可以通过索引快速访问特定位置的元素,例如获取栈顶元素很高效。但是在插入和删除元素时,如果涉及到扩容或缩容操作,可能会比较耗时。
链式栈在插入和删除元素(即push和pop操作)时,只需要改变指针的指向,速度较快。但是访问特定位置的元素需要遍历链表,效率较低。
4. 实现复杂度:
顺序栈的实现相对简单,代码量较少。
链式栈的实现需要定义节点类,代码相对复杂一些。
六.顺序栈和链式栈的应用场景
顺序栈适用场景
1. 空间效率要求高
当内存资源有限且需要尽可能节省内存空间时,顺序栈是一个较好的选择。因为它不需要为每个元素存储额外的指针,相比链式栈在存储大量小元素时可能占用更少的内存。
例如:在嵌入式系统或者资源受限的环境中,顺序栈可以更有效地利用有限的内存资源。
2. 频繁随机访问
如果需要经常随机访问栈中的元素,顺序栈可以通过索引快速定位到特定位置的元素。
比如在某些算法中,需要反复查看栈顶以下的特定位置的元素,顺序栈的这种特性就很有优势。
3. 已知最大容量
当能够预先确定栈的最大容量时,顺序栈可以更高效地分配内存。因为可以在创建栈时一次性分配足够的连续内存空间,避免了后续动态分配的开销。
如:在处理固定规模的数据集合或者具有明确边界条件的问题时,顺序栈可以提供更稳定的性能。
链式栈适用场景
1. 不确定容量
当无法预先确定栈的大小,或者栈的大小可能动态变化很大时,链式栈更加灵活。它可以根据需要动态地分配和释放内存,不会出现栈满导致的溢出问题。
如:在处理用户输入的动态数据或者需要处理不同规模的任务时,链式栈可以适应不确定的容量需求。
2. 频繁插入和删除
对于频繁进行插入和删除操作的场景,链式栈的性能更好。因为在链式栈中进行插入和删除操作只需要改变指针的指向,时间复杂度为 O(1),而顺序栈在插入和删除元素时可能需要移动大量元素,尤其是在栈接近满或空的情况下。
例如:在实现一些需要频繁调整的算法或者数据结构时,链式栈可以提供更高效的操作。
3. 多线程环境
在多线程环境下,如果需要避免同步带来的性能开销,可以考虑使用链式栈。因为链式栈的各个节点是独立分配的,不同线程可以独立地对不同的节点进行操作,减少了线程之间的竞争。 当然在多线程环境下使用链式栈时需要注意正确的同步机制以确保数据的一致性。
在 Java 源码中某些地方使用顺序栈而不是链式栈可能有以下原因:
一、性能方面
1. 局部性原理:顺序栈的元素存储在连续的内存空间中,更符合 CPU 的局部性原理,能够提高缓存命中率,从而在一定程度上提高访问速度。相比之下,链式栈的节点在内存中可能是分散存储的,不利于缓存的利用。
2. 快速随机访问:如果需要随机访问栈中的元素,顺序栈可以通过索引快速定位到特定位置的元素,而链式栈需要遍历链表才能找到特定位置的元素,效率较低。
二、空间利用率
1. 没有额外指针开销:顺序栈不需要像链式栈那样为每个节点存储指向下一个节点的指针,因此在存储相同数量的元素时,顺序栈可能占用更少的内存空间,尤其是在存储小对象时,这种优势可能更加明显。
2. 可预测的内存占用:在某些情况下,需要对内存的使用进行更精确的控制和预测。顺序栈在创建时可以指定固定的容量,这使得内存的占用更加可预测,而链式栈的内存占用取决于动态的插入和删除操作,难以准确预估。
三、实现复杂度和稳定性
1. 简单的实现:顺序栈的实现相对简单,代码逻辑更加清晰,容易理解和维护。对于一些对代码稳定性要求较高的场景,简单的实现可能更可靠,减少出现错误的可能性。
2. 边界情况处理:在处理一些边界情况时,顺序栈可能更容易处理。例如,判断栈是否为空或满可以通过简单的条件判断来实现,而链式栈可能需要更多的逻辑来处理各种特殊情况。
需要注意的是,这并不意味着链式栈在 Java 中没有用武之地。在一些需要动态调整大小、对内存分配灵活性要求较高或者需要频繁进行插入和删除操作的场景中,链式栈可能更加合适。选择使用顺序栈还是链式栈取决于具体的应用需求和场景。