目录
前言
一键助你了解并认识栈和队列,栈和队列的概念为何?如何使用栈和队列?栈和队列的应用场景?栈、虚拟机栈、栈帧有什么区别?什么是循环队列、双端队列?如何用栈和队列实现彼此?本篇文章统统会为你解答。
注:笔者代码水平与笔力有限,如有错误欢迎大家在评论区指出,我会及时回复改正的。
一、栈是什么?
栈(Stack)是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
生活中常见的例子有:羽毛球在羽毛球筒中的进出、子弹在弹夹中的进出等。
一言蔽之,栈是一种先进后出的数据结构。我们可以用顺序表实现栈也可以用链表实现栈,不过一般都使用顺序表,两者分别叫做顺序栈与链式栈。如果你是第一次接触栈这个数据结构,了解这些内容即可,对整体有个大概后在回过头来看这些内容便一目了然。
再具体些,顺序栈就是底层是一个数组的栈,对应Stack;链式栈则对应利用LinkedList或者Deque引用。
实现链式栈时,又分为单向链表(入栈出栈从链表头部开始)与双向链表,进行入栈出栈操作时的时间复杂度都是O(1)。
1. 栈的方法与功能
Java提供了Stack类,下面我们使用它来了解栈的使用方式。
// 实例化一个栈
Stack<Integer> stack = new Stack<>();
// 入栈
// push方法返回值的意思是:入栈的元素
stack.push(12);
stack.push(23);
stack.push(34);
stack.push(45);
stack.push(56);
// pop出栈 返回值的意思是:出栈的元素
Integer x = stack.pop();
System.out.println(x);
// peek方法:获取栈顶元素 但是 不删除
Integer y = stack.peek();
System.out.println(y);
Integer y2 = stack.peek();
System.out.println(y2);
boolean flg = stack.isEmpty();
System.out.println(flg);
flg = stack.empty();
System.out.println(flg);
如上述代码块,我们可以了解到,Stack常用的方法与功能有:
方法 | 功能 |
---|---|
Stack() | 构造一个空的栈 |
E push(E e) | 将e入栈,并返回e |
E pop() | 将栈顶元素出栈并返回 |
E peek() | 获取栈顶元素 |
int size() | 获取栈中有效元素个数 |
boolean empty() | 检测栈是否为空 |
2. 栈的模拟实现
使用整型数组模拟实现栈:
public class MyStack {
// usedSize 可以表示当前存放数据的个数,也可以表示当前存放数据的下标
public int[] elem;
public int usedSize;
public MyStack() {
this.elem = new int[10];
}
public void push(int val) {
if (isFull()) {
// 扩容
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
usedSize++;
}
public boolean isFull() {
return usedSize == elem.length;
}
public int pop() {
if (empty()) {
throw new RuntimeException("栈为空,无法出栈。");
}
int oldVal = elem[usedSize - 1];
// 当栈中的元素为引用数据类型时,出栈后需要置空
//elem[usedSize] = null;
usedSize--;
return oldVal;
}
public int peek() {
if (empty()) {
throw new RuntimeException("栈为空,无法获取栈顶元素。");
}
return elem[usedSize - 1];
}
public boolean empty() {
return usedSize == 0;
}
}
使用对象数组模拟实现栈:
public class MyStack2<E> {
public Object[] elem;
public int usedSize;
public MyStack2() {
this.elem = new Object[10];
}
public void push(E val) {
if (isFull()) {
// 扩容
elem = Arrays.copyOf(elem, 2 * elem.length);
}
elem[usedSize] = val;
usedSize++;
}
public boolean isFull() {
return usedSize == elem.length;
}
public E pop() {
if (empty()) {
return null;
}
E oldVal = (E) elem[usedSize - 1];
// elem[usedSize] = null;
usedSize--;
return oldVal;
}
public E peek() {
if (empty()) {
return null;
}
return (E) elem[usedSize - 1];
}
public boolean empty() {
return usedSize == 0;
}
}
3. 栈的应用场景
(1) 改变元素的序列
若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的出栈序列是()
A: 1,4,3,2
B: 2,3,4,1
C: 3,1,4,2
D: 3,4,2,1
一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( )
A: 12345ABCDE
B: EDCBA54321
C: ABCDE12345
D: 54321EDCBA
这类题目需要注意题干中是否说明入栈时是否可以出栈。
两题答案:C、B
(2) 将递归转化为循环
逆序打印链表
// 递归方式
void printList (Node head) {
if (null != head) {
printList(head.next);
System.out.print(head.val + " ");
}
}
// 循环方式
void printList(Node head) {
if (null == head) {
return;
}
Stack<Node> s = new Stack<>();
// 将链表中的结点保存在栈中
Node cur = head;
while (null != cur) {
s.push(cur);
cur = cur.next;
}
// 将栈中的元素出栈
while (!s.empty()) {
System.out.print(s.pop() + " ");
}
}
(3) 括号匹配
原题链接:力扣-有效的括号
参考代码:
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
// 1.左括号入栈
if (ch == '(' || ch == '[' || ch == '{') {
stack.push(ch);
} else {
// 2.遇到了右括号
if (stack.empty()) {
return false;
} else {
// 3.取栈顶元素的左括号,看和当前右括号是否匹配
char chL = stack.peek();
if (chL == '(' && ch == ')' ||
chL == '[' && ch == ']' || chL == '{' && ch == '}') {
// 4.证明当前这一对括号是匹配的
stack.pop();
} else {
// 5.当前括号不匹配
return false;
}
}
}
}
return stack.empty();
}
}
(4) 逆波兰表达式求值
原题链接:力扣-逆波兰表达式求值
举例:5*(2+3)转换为逆波兰表示式则为:523+*。具体解释力扣里有百科链接。
参考代码:
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < tokens.length; i++) {
String tmp = tokens[i];
if (!isOperation(tmp)) {
// 说明该字符代表数字
Integer val = Integer.valueOf(tmp);
stack.push(val);
} else {
// 说明该字符代表 + - * / 操作
Integer val2 = stack.pop();
Integer val1 = stack.pop();
switch (tmp) {
case "+":
stack.push(val1 + val2);
break;
case "-":
stack.push(val1 - val2);
break;
case "*":
stack.push(val1 * val2);
break;
case "/":
stack.push(val1 / val2);
break;
default:
}
}
}
return stack.pop();
}
public boolean isOperation(String s) {
if ("+".equals(s) || "-".equals(s) || "*".equals(s) || "/".equals(s)) {
return true;
}
return false;
}
}
代码解析:本质上就是遇到数字就入栈,遇到操作符就连出两栈分别作为操作符右边和左边的数字(顺序不能错),然后将计算结果继续入栈,最后将唯一的结果出栈。
(5) 出栈入栈次序匹配
原题链接:牛客-栈的压入、弹出序列
参考代码:
public boolean IsPopOrder(int[] pushV, int[] popV) {
Stack<Integer> stack = new Stack<>();
int j = 0;
for (int i = 0; i < pushV.length; i++) {
stack.push(pushV[i]);
for (; stack.peek() == popV[j] && j < popV.length && !stack.empty(); j++) {
stack.pop();
}
}
return stack.empty();
}
代码解析:用 i 和 j 分别遍历入栈序列、出栈序列,入栈的同时通过条件限制判断出栈的时机,那么本题的难点就在于需要哪些条件。
- stack.peek() == popV[j]:这个最好理解,当栈顶元素与当前出栈序列对应的元素相等时才可出栈。
- j < popV.lengeh:当出栈序列遍历至序列尾后便要结束遍历。
- !stack.empty():不写这个条件运行这段代码时可能会报空栈异常:java.util.EmptyStackException
具体来说就是当我们用一个空栈来进行出栈(pop)或者取顶(peek)操作时,由于此时栈是空的,无法完成这些操作,所以抛出这个异常。我们来看看为什么会这样,比如说我们将前两个条件保留,去掉第三个栈非空的条件,然后给出 入栈序列(pushV) 和 出栈序列(popV) 分别为:[2,1,0],[1,2,0]
入栈序列的2入栈,此时栈中只有一个2,于是开始遍历出栈序列
1:栈中的2与出栈序列的1不相等,继续遍历;
2:栈中的2与出栈序列的2相等,出栈序列也没有遍历完,进行出栈操作(此时栈已经空了),继续遍历;
0:这里我们便遇到了栈空时进行取顶(peek)操作,异常的来源便是这里。
综上,一旦栈空就没必要继续遍历出栈序列了,直接跳出循环继续遍历入栈序列→入栈→遍历出栈序列→出栈即可。
(6) 最小栈
原题链接:力扣-最小栈
参考代码:
class MinStack {
Stack<Integer> stack;
Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
stack.push(val);
if (minStack.empty()) {
minStack.push(val);
} else {
int peekVal = minStack.peek();
if (val <= peekVal) {
minStack.push(val);
}
}
}
public void pop() {
int popVal = stack.pop();
if (popVal == minStack.peek()) {
minStack.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
代码解析:用两个栈实现能在常数时间内检索到最小元素的一个栈。
一个栈(stack)正常存放栈中元素,另一个栈(minStack)只存最小的那个元素。
3. 栈、虚拟机栈、栈帧的区别
虚拟机栈:定义局部变量时,这些局部变量会存到虚拟机栈中。
栈帧:调用函数时,要给函数开辟一块内存,这块内存就可以理解为栈帧。
二、队列是什么?
队列(Queue)是只允许在一段进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)的原则。
入队列:进行插入操作的一端称为队尾(Tail/Rear)。
出队列:进行删除操作的一端称为队头(Head/Front)。
生活中的例子:排队打饭
注意读音:是Queue不是Queen,即就读作q
队列既可以用双向链表实现,也可以用数组实现,一般用链表实现
1. 队列的方法与功能
Java提供了Queue接口,下面我们使用它来了解队列的使用方式
Queue<Integer> queue = new LinkedList<>();
// 尾插法
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
// 头删
System.out.println(queue.poll());
System.out.println(queue.peek());
System.out.println(queue.peek());
System.out.println(queue.isEmpty());
如上述代码块,我们可以了解到,Queue常用的方法与功能有:
方法 | 功能 |
---|---|
boolean offer(E e) | 入队列 |
E poll() | 出队列 |
peek() | 获取队头元素 |
int size() | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
2. 队列的模拟实现
使用链表实现队列:
public class MyQueue {
static class ListNode {
public int val;
public ListNode prev;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
public ListNode last;
public void offer(int val) {
ListNode node = new ListNode(val);
if (head == null) {
head = last = node;
} else {
last.next = node;
node.prev = last;
last = last.next;
}
}
public int poll() {
if (head == null) {
return -1;
}
int ret = head.val;
if (head.next == null) {
head = null;
last = null;
} else {
head = head.next;
head.prev = null;
}
return ret;
}
public int peek() {
if (head == null) {
return -1;
}
return head.val;
}
public boolean isEmpty() {
return head == null;
}
}
3. 循环队列
在上文我们实现队列时,使用了链表。那么假如我们要使用数组实现队列时,又该怎么办?
队列包含队头(head)和队尾(last),用一般的思路来构建队列时,会出现空间浪费的现象,如:
队头为0,队尾为7。若此时出队三次,则队头为3,队尾为7,继续添加元素也只会增加队尾坐标,队头前面的三个空间就被浪费掉了。
循环队列就可以很好的解决这个问题,为了不出现空间浪费的情况,在队满且last需要增加时,我们让last从队尾移至队头(如何让last这样移动这里按下不表,后文有详解)。形象点说,就是让队列卷起来,头尾相接,last超出队尾不会消失,会继续在队头“重生”。
举个例子,仍然队头为0,队尾为7,假如数组大小就是8,此时队内放了7个元素,再次让元素入队时,last会直接跑到0位置,这样就算元素出队时head向“后”跑,元素入队时last也会再次利用起head空出的位置,这样就解决了空间浪费问题。
但是,这样的循环队列引出了一个新的问题:我们无法判断队列空或满!
还是上面的例子,队满后head、last都在0位置,而初始状态的空队也是这样的,这就导致我们无法判断队列空或满。
针对这个问题,有以下三种方式解决:(first视为head)
- 使用usedSize计数来表示队内元素数量,即 空:us == 0 满:us == 数组长度
- 只需要表示空满两个状态的话,不妨定义一个布尔类型flg来表示空满:boolean flg = false;
即last移动过程中与first相遇就为true,first移动过程中与last相遇就为false。- 浪费一个空间:当last的下一个是first就认为此时队列已满,这样first与last相遇时就是空。
好,这样我们解决完判断队列空或满的问题后,下面便解决如何让last从队尾移动到队头:
如果我们不做处理,正常入队后的操作是last++,但要让last重新回到队头,我们不妨让last = (last + 1) % len,len就是数组长度。
这样让last++后对数组长度取余,就可以让last重新回到队头。
下面我们尝试设计一个浪费一个空间形式的循环队列:
原题链接:力扣-设计循环队列
参考代码:
class MyCircularQueue {
public int[] elem;
public int first;
public int last;
public MyCircularQueue(int k) {
elem = new int[k + 1];
}
public boolean enQueue(int value) {
if (isFull()) {
return false;
}
elem[last] = value;
last = (last + 1) % elem.length;
return true;
}
public boolean deQueue() {
if (isEmpty()) {
return false;
}
first = (first + 1) % elem.length;
return true;
}
public int Front() {
if (isEmpty()) {
return -1;
}
return elem[first];
}
public int Rear() {
if (isEmpty()) {
return - 1;
}
int index = last == 0 ? elem.length - 1 : last - 1;
return elem[index];
}
public boolean isEmpty() {
return first == last;
}
public boolean isFull() {
return first == (last + 1) % elem.length;
}
}
4. 双端队列
双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。
双端队列的元素可以从队头出队和入队,也可以从队尾出队和入队。
同样的,双端队列也既有链式实现又有线性实现
// 双端队列
// 链式实现
Deque<Integer> queue = new LinkedList<>();
queue.offerFirst(1);
queue.pollFirst();
// 线性实现
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
既然LinkedList和ArrayDeque这两个类可以实现双端队列,那么进一步,它们俩就可以既当做队列使用,也可以当做栈使用
三、栈和队列的相互实现
1. 用队列实现栈
原题链接:力扣-用队列实现栈
参考代码:
class MyStack {
public Queue<Integer> queue1;
public Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
public void push(int x) {
if (empty()) {
queue1.offer(x);
return;
}
if (queue1.isEmpty()) {
queue1.offer(x);
} else {
queue2.offer(x);
}
}
public int pop() {
int size;
if (!queue2.isEmpty()) {
size = queue2.size();
for (int i = 0; i < size - 1; i++) {
queue1.offer(queue2.poll());
}
return queue2.poll();
} else {
size = queue1.size();
for (int i = 0; i < size - 1; i++) {
queue2.offer(queue1.poll());
}
return queue1.poll();
}
}
public int top() {
int size;
int ret = -1;
if (!queue2.isEmpty()) {
size = queue2.size();
for (int i = 0; i < size; i++) {
ret = queue2.poll();
queue1.offer(ret);
}
} else {
size = queue1.size();
for (int i = 0; i < size; i++) {
ret = queue1.poll();
queue2.offer(ret);
}
}
return ret;
}
public boolean empty() {
return queue1.isEmpty() && queue2.isEmpty();
}
}
代码解析:我们看到题目:用队列实现栈。因此我们要知道一个前提:能否用一个队列实现栈?
显然是不行的,因为队列先进先出,栈先进后出,所以一个队列不能实现栈。
接下来我们考虑用两个队列实现栈,我们不妨将问题拆解为两个过程:出栈、入栈。
- 出栈:把其中一个非空队列的N-1个元素放到另一个队列当中,当前队列剩下的一个元素就是需要“出栈”的元素。
- 入栈:把数据放到非空队列当中。
第一次“入栈”时两个队列都是空,我们直接规定放到第一个队列中即可。
2. 用栈实现队列
原题链接:力扣-用栈实现队列
参考代码:
class MyQueue {
public Stack<Integer> stack1;
public Stack<Integer> stack2;
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
public void push(int x) {
stack1.push(x);
}
public int pop() {
if (stack2.empty()) {
while (!stack1.empty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
public int peek() {
if (stack2.empty()) {
while (!stack1.empty()) {
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
public boolean empty() {
return stack1.empty() && stack2.empty();
}
}
代码解析:与上题类似,首先一个栈无法实现队列。
所以用两个栈,记作入队栈(stack1)和出队栈(stack2):
- 入队:把数据放到s1当中。
- 出队:s2为空时,需要将s1中的全部元素出栈,再入栈到s2中,这样s2出栈的元素就是要“出队”的元素;s2不为空时,此时s2的出栈元素就是要“出队”的元素。
码字不易,点个赞吧(〃‘▽’〃)。