文章目录
栈与队列基础理论
了解java栈(Stack)
在Java编程语言中,栈(Stack)是一种非常重要的数据结构,它在方法调用和变量存储中扮演着关键的角色。了解Java中的栈对于程序员来说至关重要.
栈(Stack)是一种常见的数据结构,具有后进先出(LIFO,Last In First Out)的特性,即最后入栈的元素最先出栈。栈通常用于存储临时性的数据,如方法调用过程中的局部变量、操作数栈等。在计算机科学中,栈的应用非常广泛,包括编程语言中的函数调用、内存分配以及表达式求值等领域。在Java编程语言中,栈也被广泛应用于方法调用和内存管理的过程中。
java中的栈帧
在Java虚拟机(JVM)中,每个方法在运行时都会创建一个对应的栈帧(Stack Frame),栈帧用于存储方法的局部变量、操作数栈、动态链接、返回地址等信息。
栈帧的结构如下:
局部变量表(Local Variable Table):局部变量表用于存储方法参数和方法内部定义的局部变量。局部变量表中的每个槽位可以存储一个基本类型的值或对象引用。在方法调用时,参数和本地变量的值会被压入局部变量表;在方法执行期间,可以通过索引来访问局部变量表中的值。
操作数栈(Operand Stack):操作数栈是用于执行方法时进行计算的临时数据存储区域。操作数栈的元素可以是任意的Java数据类型,包括基本类型和对象引用。在方法执行过程中,操作数栈用于存储方法执行过程中的计算结果、方法参数以及临时变量等数据。
动态链接(Dynamic Linking):动态链接指向运行时常量池中该方法的符号引用的指针。在Java中,动态链接主要用于解析方法调用的目标地址,以便在运行时能够正确调用方法。
方法返回地址:方法返回地址是指向方法调用者的指令地址。当方法执行完毕后,JVM会使用返回地址恢复执行方法调用者的指令。
栈帧的创建和销毁是在方法调用和返回过程中自动进行的。每当发生方法调用时,JVM会为该方法创建一个新的栈帧并将其推入调用栈(Call Stack),当方法返回时,对应的栈帧会被销毁,栈顶指针会回到前一个方法的栈帧。栈帧的动态创建和销毁确保了方法的独立性和互相调用的正确性。
java栈的基本方法
- push(Object item):将元素item压入栈顶。=
- pop():弹出栈顶元素,并将其从栈中删除。
- peek():返回栈顶元素,但不删除它。
- isEmpty():判断栈是否为空,返回布尔值。
- search(Object item):搜索元素item在栈中的位置(从栈顶开始),如果找到则返回其距离栈顶的位置(栈顶为1),如果未找到则返回-1。
- clear():对当前栈进行清空。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
了解java队列(Queue)
Queue接口是Java集合框架中定义的一个接口,它代表了一个先进先出(FIFO)的队列。Queue接口继承自Collection接口,它定义了一组方法来操作队列中的元素。下面是Queue接口的一些主要方法和特性的详细解释:
Queue接口是Java集合框架中定义的一个接口,它代表了一个先进先出(FIFO)的队列。Queue接口继承自Collection接口,它定义了一组方法来操作队列中的元素。下面是Queue接口的一些主要方法和特性的详细解释:
- 添加元素:
-
boolean add(E element): 将指定的元素添加到队列的末尾,如果成功则返回true,如果队列已满则抛出异常。
-
boolean offer(E element): 将指定的元素添加到队列的末尾,如果成功则返回true,如果队列已满则返回false。
- 移除元素:
- E remove(): 移除并返回队列头部的元素,如果队列为空则抛出异常。
- E poll(): 移除并返回队列头部的元素,如果队列为空则返回null。
- 获取头部元素:
- E element(): 获取队列头部的元素,但不移除它,如果队列为空则抛出异常。
- E peek(): 获取队列头部的元素,但不移除它,如果队列为空则返回null。
- 队列大小:
- int size(): 返回队列中的元素个数。
- boolean isEmpty(): 判断队列是否为空。
Queue接口还有一些其他方法,如clear()用于清空队列中的所有元素,contains(Object o)用于判断队列是否包含指定元素等。
Queue接口的常见实现类包括LinkedList、ArrayDeque和PriorityQueue等。LinkedList实现了Queue接口,并且还可以作为栈使用,它是一个双向链表。ArrayDeque是一个双端队列,它同时实现了Queue和Deque接口。PriorityQueue是一个基于优先级的队列,它允许元素按照优先级顺序被插入和删除。
通过使用Queue接口,我们可以方便地进行队列操作,如入队、出队、查看队列头部元素等。它在处理任务调度、消息传递等场景中非常有用。
233.用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
说明:
- 你 只能 使用标准的栈操作 —— 也就是只有
push to top
,peek/pop from top
,size
, 和is empty
操作是合法的。 - 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
示例 1:
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、peek
和empty
- 假设所有操作都是有效的 (例如,一个空的队列不会调用
pop
或者peek
操作)
进阶:
- 你能否实现每个操作均摊时间复杂度为
O(1)
的队列?换句话说,执行n
个操作的总时间复杂度为O(n)
,即使其中一个操作可能花费较长时间。
Related Topics
-
栈
-
设计
-
队列
思路
这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。
使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。
下面动画模拟以下队列的执行过程:
执行语句:
queue.push(1);
queue.push(2);
queue.pop(); 注意此时的输出栈的操作
queue.push(3);
queue.push(4);
queue.pop();
queue.pop();注意此时的输出栈的操作
queue.pop();
queue.empty();
在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入出栈(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
在代码实现的时候,会发现pop() 和 peek()两个函数功能类似,代码实现上也是类似的,可以思考一下如何把代码抽象一下。
java实现
class MyQueue {
//定义一个进栈
Stack<Integer> stackIn;
//定义一个出栈
Stack<Integer> stackOut;
public MyQueue() {
stackIn = new Stack<>(); // 负责进栈
stackOut = new Stack<>(); // 负责出栈
}
public void push(int x) {
//数据进栈无特殊操作,只要数据放进输入栈就好
stackIn.push(x);
}
public int pop() {
//数据出栈时,完成数据导入导出工作
dumpstackIn();
return stackOut.pop();
}
public int peek() {
dumpstackIn();
return stackOut.peek();
}
public boolean empty() {
return stackIn.isEmpty() && stackOut.isEmpty();
}
/**
* @Description 在数据出栈时, 完成数据导出导入操作
* @Param null
* @Return {@link null}
* @Author 君君
* @Date 2024/7/2 0:25
*/
private void dumpstackIn() {
//判断输出栈是否为空,如果不为空直接返回就可.
if (!stackOut.isEmpty()) {
return;
}
//将stackIn中的元素全部放到stackOut中
while (!stackIn.isEmpty()) {
stackOut.push(stackIn.pop());
}
}
}
225.用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
注意:
- 你只能使用队列的标准操作 —— 也就是
push to back
、peek/pop from front
、size
和is empty
这些操作。 - 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例:
输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]
解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、top
和empty
- 每次调用
pop
和top
都保证栈不为空
**进阶:**你能否仅用一个队列来实现栈。
Related Topics
-
栈
-
设计
-
队列
思路
(这里要强调是单向队列)
有的同学可能疑惑这种题目有什么实际工程意义,其实很多算法题目主要是对知识点的考察和教学意义远大于其工程实践的意义,所以面试题也是这样!
队列模拟栈,其实一个队列就够了,那么我们先说一说两个队列来实现栈的思路。
队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。
所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。
但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用来备份的!
如下面动画所示,用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
模拟的队列执行语句如下:
queue.push(1);
queue.push(2);
queue.pop(); // 注意弹出的操作
queue.push(3);
queue.push(4);
queue.pop(); // 注意弹出的操作
queue.pop();
queue.pop();
queue.empty();
java实现
class MyStack {
Queue<Integer> queue1; // 和栈中保持一样元素的队列
Queue<Integer> queue2; // 辅助队列
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
//添加元素时,每执行一次添加元素,都会交换当前x与队的位置,
//所以,相当于一直在向退列的头部添加元素,也就是栈顶
public void push(int x) {
//先将这个元素放到辅助队列中
queue2.offer(x);
//再将queue1的元素挨个放入queue2中
while (!queue1.isEmpty()) {
queue2.offer(queue1.poll());
}
Queue<Integer> queueTemp;
queueTemp = queue1;
queue1 = queue2;
// 最后交换queue1和queue2,将元素都放到queue1中
queue2 = queueTemp;
}
public int pop() {
// 因为queue1中的元素和栈中的保持一致,所以这个和下面两个的操作只看queue1即可
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
20.有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
提示:
1 <= s.length <= 104
s
仅由括号'()[]{}'
组成
Related Topics
-
栈
-
字符串
思路
题外话
括号匹配是使用栈解决的经典问题。
题意其实就像我们在写代码的过程中,要求括号的顺序是一样的,有左括号,相应的位置必须要有右括号。
如果还记得编译原理的话,编译器在 词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。
再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。
cd a/b/c/../../
这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用(其实可以出一道相应的面试题了)
所以栈在计算机领域中应用是非常广泛的。
有的同学经常会想学的这些数据结构有什么用,也开发不了什么软件,大多数同学说的软件应该都是可视化的软件例如APP、网站之类的,那都是非常上层的应用了,底层很多功能的实现都是基础的数据结构和算法。
所以数据结构与算法的应用往往隐藏在我们看不到的地方!
这里我就不过多展开了,先来看题。
正题
由于栈结构的特殊性,非常适合做对称匹配类的题目。
首先要弄清楚,字符串里的括号不匹配有几种情况。
一些同学,在面试中看到这种题目上来就开始写代码,然后就越写越乱。
建议在写代码之前要分析好有哪几种不匹配的情况,如果不在动手之前分析好,写出的代码也会有很多问题。
先来分析一下 这里有三种不匹配的情况,
-
第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
-
第二种情况,括号没有多余,但是 括号的类型没有匹配上。
-
第三种情况,字符串里右方向的括号多余了,所以不匹配。
我们的代码只要覆盖了这三种不匹配的情况,就不会出问题,可以看出 动手之前分析好题目的重要性。
动画如下:
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。
分析完之后,代码其实就比较好写了,
但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!
/**
* @Description 有效的括号
* @Param s
* @Return {@link boolean}
* @Author 君君
* @Date 2024/7/2 1:31
*/
public boolean isValid(String s) {
//使用双端队列来实现栈的功能
Deque<Character> deque = new LinkedList<>();
char temp;
for (int i = 0; i < s.length(); i++) {
temp = s.charAt(i);
//如果碰到左括号,进行入栈操作
if (temp=='(') {
deque.push(')');
}else if(temp=='{'){
deque.push('}');
}else if(temp=='['){
deque.push(']');
}
//如果为右括号,并且栈内已经没有元素了,或者栈顶的元素和当前元素不匹配
//则该字符串不符合有效的括号
else if (deque.isEmpty()||deque.peek()!=temp) {
return false;
}
//当前字符与栈顶元素匹配
else{
deque.pop();
}
}
//最后判断栈内元素是否匹配
return deque.isEmpty();
}
时间复杂度O(N)
空间复杂度O(N) 开辟了一个长度为n的双端队列,所以是O(N)
1047. 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
提示:
1 <= S.length <= 20000
S
仅由小写英文字母组成。
Related Topics
-
栈
-
字符串
思路
本题要删除相邻相同元素,相对于20. 有效的括号 (opens new window)来说其实也是匹配问题,20. 有效的括号 是匹配左右括号,本题是匹配相邻元素,最后都是做消除的操作。
本题也是用栈来解决的经典题目。
那么栈里应该放的是什么元素呢?
我们在删除相邻重复项的时候,其实就是要知道当前遍历的这个元素,我们在前一位是不是遍历过一样数值的元素,那么如何记录前面遍历过的元素呢?
所以就是用栈来存放,那么栈的目的,就是存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素。
然后再去做对应的消除操作。 如动画所示:
从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转一下,就得到了最终的结果。
使用Deque实现
public String removeDuplicates(String s) {
ArrayDeque<Character> deque = new ArrayDeque<>();
char temp;
for (int i = 0; i < s.length(); i++) {
temp = s.charAt(i);
if (deque.isEmpty() || deque.peek() != temp) {
deque.push(temp);
} else {
deque.pop();
}
}
1. 可以用普通字符串操作
String str= "";
while (!deque.isEmpty()) {
//添加字符串,并且进行翻转字符串操作
str = deque.pop() + str;
}
return str;
2.也可以使用stream流来操作
//AtomicReference<String> str = new AtomicReference<>("");
//deque.stream().forEach((Character item)->{
// str.set(item.toString() + str);
//});
//return str.get();
}
时间复杂度O(N)
空间复杂度O(N) 开辟了一个长度为n的双端队列,所以是O(N)
时间复杂度O(N)
空间复杂度O(N) 开辟了一个长度为n的双端队列,所以是O(N)
使用java自带的可扩展字符串StringBuilder或者StringBuffer实现
/**
* @Description
* @Param s
* @Return {@link String}
* @Author 君君
* @Date 2024/7/2 2:02
*/
public String removeDuplicates(String s) {
// 将 res 当做栈
// 也可以用 StringBuilder 来修改字符串,速度更快
StringBuffer res = new StringBuffer();
//当前字符串res的长度
int top = -1;
char temp;
for (int i = 0; i < s.length(); i++) {
temp = s.charAt(i);
// 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,
// 弹出栈顶字符,同时 top--
if (top >= 0 && res.charAt(top) == temp) {
res.deleteCharAt(top);
top--;
}
//否则将该字符入字符串,同时top++;
else {
res.append(temp);
top++;
}
}
return res.toString();
}
时间复杂度O(N)
空间复杂度O(N) ,由于是可扩展的字符串,所以空间复杂度为O(N)
题外话
这道题目就像是我们玩过的游戏对对碰,如果相同的元素挨在一起就要消除。
可能我们在玩游戏的时候感觉理所当然应该消除,但程序又怎么知道该如何消除呢,特别是消除之后又有新的元素可能挨在一起。
此时游戏的后端逻辑就可以用一个栈来实现(我没有实际考察对对碰或者爱消除游戏的代码实现,仅从原理上进行推断)。
游戏开发可能使用栈结构,编程语言的一些功能实现也会使用栈结构,实现函数递归调用就需要栈,但不是每种编程语言都支持递归,例如:
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
相信大家应该遇到过一种错误就是栈溢出,系统输出的异常是Segmentation fault
(当然不是所有的Segmentation fault
都是栈溢出导致的) ,如果你使用了递归,就要想一想是不是无限递归了,那么系统调用栈就会溢出。
而且在企业项目开发中,尽量不要使用递归!在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),**造成栈溢出错误(这种问题还不好排查!)