代码随想录刷题day28|(栈与队列篇:栈)232.用栈实现队列
目录
一、栈的基本知识
二、栈在Java中的实现
太长不看简易总结版
1.使用组合模式
2.通过Deque接口
3.基于底层数据结构的实现
使用数组:
使用链表:
4.使用Java的Stack类
三、相关算法题目
四、其他细节
1.组合模式如何通过封装隐藏其他方法?
2.为什么 Stack 类不能隐藏这些破坏栈特性的方法?
3.组合模式中创建对象的具体实现:
4.接口作为类的成员变量
一、栈的基本知识
栈是一种特殊的数据结构,所有的数据操作只能在一端(栈顶)进行操作,特点是先进后出,栈中除了栈顶元素其他都是未知的,栈无法随机访问;比如桶装羽毛球;
入栈:往栈中添加元素的操作,一般叫做push;
出栈:从栈中移除元素的操作,并返回栈顶元素,一般叫做pop,出栈(弹出栈顶元素);
peek:获取栈顶元素,并不出栈;
注意:这里说的"栈"与内存中的"栈空间"是两个不同的概念,具体不同见⬇️
Java 【数据结构】 栈(Stack)和队列(Queue)【神装】_java stack queue-CSDN博客
二、栈在Java中的实现
四种实现方法:
太长不看简易总结版
实现方式 | 优点 | 缺点 |
使用Stack类 | 简单易用,线程安全 | 性能较差、设计缺陷 |
使用Deque接口的实现类 | 性能高、代码简洁 | 需自觉遵守栈的规则 |
组合模式实现栈 | 隐藏底层实现、灵活性高 | 需要手动实现栈的逻辑 |
直接通过数组、链表实现栈 | 性能高、实现方式更底层 | 灵活性较低、需要手动实现 |
1.使用组合模式
组合模式(Composition)是一种设计模式,它通过在一个类中持有另一个类的实例来实现功能,而不是通过继承。组合模式的核心思想是 “优先使用组合,而不是继承” 即:持有实例而不是通过继承。
组合模式的体现:
-
在栈的实现中,
MyStack
类持有一个List接口
的实例(如ArrayList
),并通过实例来实现栈的功能,而不是继承List;
-
这样,
MyStack
类可以通过封装隐藏List
的其他方法(防止破坏栈的特性),只暴露栈的标准接口; -
需要手动编写代码实现栈的操作逻辑(如push、pop、peek等);可以 完全控制栈的行为,隐藏底层数据结构的方法,确保栈的特性不被破坏。
示例代码:
public class MyStack<E> {
private List<E> list; // 组合 List 的实例
public MyStack() {
this.list = new ArrayList<>(); //使用ArrayList实现
//将list成员变量初始化为一个 ArrayList对象
//创建了一个 ArrayList 的实例,并将其赋值给 list
}
// 栈的标准方法
public void push(E element) {
list.add(element);
}
public E pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return list.remove(list.size() - 1);
}
public E peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return list.get(list.size() - 1);
}
public boolean isEmpty() {
return list.isEmpty();
}
}
2.通过Deque接口
Deque接口(双端队列),支持在队列的两端进行插入和删除操作,继承了对队列Queue接口;
Deque有两个主要的实现类:ArrayDeque(底层使用数组),LinkedList(底层使用双向链表);
具体实现:
- Deque接口的实现类(如 ArrayDeque)提供了栈的标准方法(如 push、pop、peek),直接调用即可;
- 同时也提供了其他方法(如 addFirst、addLast、removeFirst、removeLast 等),误用可能会破坏栈的特性;
- deque的设计目标是灵活,如果像组合模式一样封装,会影响其灵活性,增加代码复杂性,一般不进行封装;
示例代码:
Deque<Integer> stack = new ArrayDeque<>();//创建一个ArrayDeque类的对象stack
stack.push(1);//入栈
stack.push(2);
stack.push(3);
System.out.println(stack.pop()); // 输出 3
System.out.println(stack.peek()); // 输出 2
3.基于底层数据结构的实现
直接通过数组或者链表实现栈,不涉及另一个类;
使用数组:
public class ArrayStack<E> {
private Object[] elements; // 用数组存储栈元素
private int size; // 栈的大小
使用数组来实现栈
public ArrayStack() {
elements = new Object[10]; // 初始容量
size = 0;
}
// 入栈
public void push(E element) {
if (size == elements.length) {
resize(); // 扩容
}
elements[size++] = element; // 将元素添加到数组末尾
}
// 出栈
public E pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
E element = (E) elements[--size]; // 返回数组的最后一个元素
elements[size] = null; // 帮助垃圾回收
return element;
}
// 查看栈顶元素
public E peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return (E) elements[size - 1]; // 返回数组的最后一个元素
}
// 判断栈是否为空
public boolean isEmpty() {
return size == 0;
}
// 扩容
private void resize() {
Object[] newElements = new Object[elements.length * 2];
System.arraycopy(elements, 0, newElements, 0, size);
elements = newElements;
}
}
使用链表:
public class LinkedStack<E> {
private static class Node<E> {
E element;
Node<E> next;
Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
private Node<E> top;
private int size;
public LinkedStack() {
top = null;
size = 0;
}
// 入栈
public void push(E element) {
top = new Node<>(element, top);
size++;
}
// 出栈
public E pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
E element = top.element;
top = top.next;
size--;
return element;
}
// 查看栈顶元素
public E peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return top.element;
}
// 判断栈是否为空
public boolean isEmpty() {
return size == 0;
}
}
4.使用Java的Stack类
Java 的Stack类是基于 Vector 实现的,而 Vector 是一个过时的类,存在以下问题:
-
Stack继承自 Vector ,而Vector 是一个动态数组,提供了许多栈不需要的方法(如随机访问、插入、删除等),破坏了栈先进后出的特性;
-
Vector 的所有方法都加了 synchronized关键字,虽保证了线程安全,但在单线程下导致性能下降;
三、相关算法题目
232.用栈实现队列(使用Deque接口的ArrayDeque实现类)
232. 用栈实现队列 - 力扣(LeetCode)
class MyQueue {
//成员变量 接口也可以作为成员变量
private Deque<Integer> stack1;//用于入栈
private Deque<Integer> stack2;//用于出栈
public MyQueue() {//构造函数
this.stack1 = new ArrayDeque<>(); //初始化stack1
this.stack2 = new ArrayDeque<>();//初始化stack2
}
//成员方法
public void push(int x) {
stack1.push(x);
}
public int pop() {
//先判断stack2是否为空
if(stack2.isEmpty()){
//为空 将stack1中的元素全部压入stack2
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
//全部压入以后 再弹出stack2的栈顶元素
}
//不为空 直接弹出stack2中的元素
if(stack2.isEmpty()){
return -1;
}
return stack2.pop();
}
public int peek() {
if(stack2.isEmpty()){
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
四、其他细节
1.组合模式如何通过封装隐藏其他方法?
-
封装将类的内部状态(成员变量)和方法实现隐藏起来,只通过公共方法(如
push
、pop
、peek
)来操作类的内部状态,而不是直接访问成员变量。 -
list
是MyStack
类的私有成员变量,外部无法直接访问。 -
MyStack
类只暴露了push
、pop
、peek
等栈的标准方法,而没有暴露list
的其他方法(如add(int index, E element)
、remove(int index)
等)。 -
这样,外部调用者只能通过栈的标准接口来操作栈,而无法直接操作底层
List
,从而确保栈的特性不被破坏。
2.为什么Stack
类不能隐藏这些破坏栈特性的方法?
Stack
类继承自 Vector
,而 Vector
提供了许多不适合栈操作的方法(如 add(int index, E element)
、remove(int index)
等)。由于继承关系,Stack
类会继承这些方法,导致栈的特性被破坏。
3.组合模式中创建对象的具体实现:
- 创建
MyStack
对象:当调用MyStack<Integer> stack = new MyStack<>();
时,会执行MyStack
类的构造函数,创建一个MyStack
类的对象stack; - 初始化
list
:在构造函数中,this.list = new ArrayList<>();
将list
初始化为一个ArrayList
对象,new ArrayList<>()
创建了一个ArrayList
的实例,并将其赋值给list;
- 结果:
stack
对象的list
成员变量指向一个ArrayList
对象,可以通过list
调用ArrayList
的方法(如add
、remove
等); - stack.push(1) 实际执行的是list.add(1);
4.接口作为类的成员变量
List是接口,但接口可以作为类的成员变量类型。接口类型的变量可以指向任何实现了该接口的类的对象。
因为在 Java 中,接口是一种引用类型,它可以指向任何实现了该接口的类的对象。例如:list是List接口类型的变量,但它指向的是ArrayList类型的对象。