理论基础
栈
在Java中,“栈”这个术语通常指的是与方法调用相关的数据结构。在Java虚拟机(JVM)中,栈主要分为两种不同的类型:
- 虚拟机栈 (Virtual Machine Stack)
- 本地方法栈 (Native Method Stack)
虽然“方法栈”这个词不是官方术语,但我们可以理解为你所指的就是“虚拟机栈”,因为它确实用于支持Java方法的执行。接下来我将详细解释这两种栈的区别:
1. 虚拟机栈 (Virtual Machine Stack)
虚拟机栈是为每个线程创建的,用于存储线程的局部变量、部分结果以及方法调用的信息。每当一个线程开始执行时,JVM就会为该线程创建一个虚拟机栈。虚拟机栈中的每一个元素称为一个栈帧(Stack Frame),每个栈帧对应一个方法调用。
- 栈帧:每个方法调用都会生成一个栈帧,包含以下部分:
- 局部变量表 (Local Variable Table):用于存放方法参数和局部变量。
- 操作数栈 (Operand Stack):用于存放中间计算结果。
- 动态链接 (Dynamic Link):用于支持方法调用过程中将常量池中的符号引用转换为直接引用。
- 返回地址 (Return Address):记录方法返回时需要跳转的位置。
2. 本地方法栈 (Native Method Stack)
本地方法栈与虚拟机栈类似,但它用于支持本地方法(即使用本地代码实现的方法,通常是C/C++实现的JNI方法)的执行。本地方法栈和虚拟机栈的主要区别在于它们支持的代码类型不同。虚拟机栈支持的是Java字节码,而本地方法栈支持的是本地平台的机器代码。
- 栈帧:本地方法栈中的栈帧也包含与方法调用相关的信息,但由于执行的是本地代码,因此具体的栈帧布局可能与虚拟机栈有所不同。
区别总结
-
用途:
- 虚拟机栈:用于支持Java方法的执行。
- 本地方法栈:用于支持本地方法的执行。
-
数据类型:
- 虚拟机栈:处理的是Java字节码。
- 本地方法栈:处理的是本地代码(例如C/C++)。
-
实现方式:
- 虚拟机栈:由JVM实现。
- 本地方法栈:通常由操作系统提供的栈实现。
实例说明
假设有一个Java程序,其中包含了一个本地方法调用:
public class StackExample {
public static void main(String[] args) {
System.out.println("In main method");
methodA();
}
public static void methodA() {
System.out.println("In methodA");
nativeMethod();
}
public static native void nativeMethod();
}
在这个例子中,当执行 methodA()
时,程序的执行流程如下:
- 主方法 (
main
) 的栈帧被压入虚拟机栈。 methodA
的栈帧被压入虚拟机栈的栈顶。nativeMethod
被调用,此时,一个栈帧会被压入本地方法栈。nativeMethod
执行完毕后,其栈帧从本地方法栈中弹出。methodA
继续执行直至完成,其栈帧从虚拟机栈中弹出。main
方法继续执行直至完成。
虚拟机栈和本地方法栈的主要区别在于它们支持的方法类型和实现机制。虚拟机栈用于Java方法的执行,而本地方法栈用于本地方法的执行。两者都在方法调用时创建栈帧来存储相关信息。
队列
- 栈:在Java中,栈是一种由JVM自动管理的数据结构,用于支持线程和方法调用。
- 队列:队列是一种程序员可以显式创建和使用的数据结构,通过Java标准库中的
Queue
接口和其实现类来实现。
232.用栈实现队列
思路
栈只能实现元素的一端进出,而队列是要求一端进一端出,但进到栈底的元素无法出来,这时就需要另一个栈来辅助,如下图
关键点在于要判断栈2是否为空,否则会影响到下次操作,同时要把全部栈1push到栈2中
class MyQueue {
Stack<Integer> stackIn;
Stack<Integer> stackOut;
/** Initialize your data structure here. */
public MyQueue() {
stackIn = new Stack<>(); // 负责进栈
stackOut = new Stack<>(); // 负责出栈
}
/** Push element x to the back of queue. */
public void push(int x) {// 加入元素,即将元素 x 推到队列的末尾
stackIn.push(x);
}
/** Removes the element from in front of queue and returns that element. */
public int pop() { //删除元素
dumpstackIn();
return stackOut.pop();
}
/** Get the front element. */
public int peek() {//返回队列开头的元素
dumpstackIn();
return stackOut.peek();
}
/** Returns whether the queue is empty. */
public boolean empty() {
return stackIn.isEmpty() && stackOut.isEmpty();
}
// 如果stackOut为空,那么将stackIn中的元素全部放到stackOut中
private void dumpstackIn(){
if (!stackOut.isEmpty()) return;
while (!stackIn.isEmpty()){
stackOut.push(stackIn.pop());
}
}
}
225. 用队列实现栈
思路
同时也可以用两个队列来模拟栈,比较容易,这里还是用以一个队列来模拟栈
import java.util.LinkedList;
import java.util.Queue;
class MyStack {
// 使用LinkedList作为队列的底层实现
private Queue<Integer> queue;
// 构造函数:初始化一个空的队列
public MyStack() {
queue = new LinkedList<>();
}
// push方法:将一个元素添加到队列中
// 由于队列遵循先进先出的原则,这里的操作实际上模拟了栈的后进先出行为
public void push(int x) {
queue.add(x); // 将元素添加到队列尾部
}
// pop方法:从队列中移除并返回队列头部的元素
// 为了模拟栈的行为,我们需要先将队列中的所有元素除了最后一个以外都重新定位到队列尾部
public int pop() {
rePosition(); // 重新定位队列中的元素
return queue.poll(); // 从队列头部移除并返回元素
}
// top方法:返回队列头部的元素,但不移除它
// 为了模拟栈的行为,我们同样需要先重新定位队列中的元素
public int top() {
rePosition(); // 重新定位队列中的元素
int result = queue.poll(); // 从队列头部获取元素
queue.add(result); // 将元素重新添加到队列尾部,以保持队列的状态不变
return result; // 返回头部元素
}
// empty方法:检查队列是否为空
public boolean empty() {
return queue.isEmpty(); // 检查队列是否为空
}
// rePosition方法:重新定位队列中的元素,除了最后一个以外的所有元素都要移动到队列尾部
// 这样做的目的是为了让队列的头部始终是栈顶元素
public void rePosition(){
int size = queue.size(); // 获取队列的大小
size--; // 减去1,因为我们最终要保留最后一个元素在队列头部
while(size-->0) { // 循环直到只剩下最后一个元素
queue.add(queue.poll()); // 从队列头部移除元素并添加到队列尾部
}
}
}
20. 有效的括号
思路
考虑不匹配的情况:
- 数量不匹配:左括号多余、右括号多余
- 括号数量匹配但类型不匹配
遇到左括号在栈弹入右括号(方便直接判断),当遇到右括号直接进行比较
class Solution {
public boolean isValid(String s) {
Deque<Character> deque = new LinkedList<>();
char ch;
for (int i = 0; i < s.length(); i++) {
ch = s.charAt(i);
//碰到左括号,就把相应的右括号入栈
if (ch == '(') {
deque.push(')');
}else if (ch == '{') {
deque.push('}');
}else if (ch == '[') {
deque.push(']');
} else if (deque.isEmpty() || deque.peek() != ch) {
return false;
}else {//如果是右括号判断是否和栈顶元素匹配
deque.pop();
}
}
//最后判断栈中元素是否匹配
return deque.isEmpty();
}
}
1047. 删除字符串中的所有相邻重复项
栈的作用与上一题相似,存储遍历过的元素,比较下一个元素是否和上一个存储的元素相同
运行速度:双指针<字符串<栈
栈方法
class Solution {
public String removeDuplicates(String S) {
//ArrayDeque会比LinkedList在除了删除元素这一点外会快一点
//参考:https://stackoverflow.com/questions/6163166/why-is-arraydeque-better-than-linkedlist
ArrayDeque<Character> deque = new ArrayDeque<>();
char ch;
for (int i = 0; i < S.length(); i++) {
ch = S.charAt(i);
if (deque.isEmpty() || deque.peek() != ch) {
deque.push(ch);
} else {
deque.pop();
}
}
String str = "";
//剩余的元素即为不重复的元素
while (!deque.isEmpty()) {
str = deque.pop() + str;
}
return str;
}
}
拓展方法1-字符串
class Solution {
public String removeDuplicates(String s) {
// 将 res 当做栈
// 也可以用 StringBuilder 来修改字符串,速度更快
// StringBuilder res = new StringBuilder();
StringBuffer res = new StringBuffer();
// top为 res 的长度
int top = -1;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top--
if (top >= 0 && res.charAt(top) == c) {
res.deleteCharAt(top);
top--;
// 否则,将该字符 入栈,同时top++
} else {
res.append(c);
top++;
}
}
return res.toString();
}
}
拓展方法2-双指针
class Solution {
public String removeDuplicates(String s) {
char[] ch = s.toCharArray();
int fast = 0;
int slow = 0;
while(fast < s.length()){
// 直接用fast指针覆盖slow指针的值
ch[slow] = ch[fast];
// 遇到前后相同值的,就跳过,即slow指针后退一步,下次循环就可以直接被覆盖掉了
if(slow > 0 && ch[slow] == ch[slow - 1]){
slow--;
}else{
slow++;
}
fast++;
}
return new String(ch,0,slow);
}
}