栈(Stack)和队列(queue)
目录
栈(Stack)
概念
栈的使用
栈的模拟实现
栈的应用场景
1. 改变元素的序列
编辑
2. 将递归转化为循环
3. 括号匹配
4. 逆波兰表达式求值
编辑
5. 出栈入栈次序匹配
6. 最小栈
概念区分
队列(Queue)
概念
队列的使用
队列模拟实现
循环队列
双端队列 (Deque)
面试题
1. 用队列实现栈
2. 用栈实现队列
栈(Stack)
概念
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
注:上述两幅图示是为了理解栈的机制的。
栈的使用
public class Test {
/**
* 这是使用栈底层代码生成的测试文件
* @param args
*/
public static void main(String[] args) {
/**
* 栈的一些方法的实现
*/
Stack<Integer> stack = new Stack<>(); // 构造一个栈对象出来
// 往栈里面放元素【原则:先进的后出】
stack.push(12);
stack.push(23);
stack.push(34);
stack.push(45); // 通过这个往栈里面添加元素,其中栈底为最先放的那个[12],栈顶为最后放的那个[45]
// 从栈顶弹出元素[删除]
int number1 = stack.pop();
System.out.println(number1); // 结果就是45
// 获取栈顶元素【只是获取,但并不删除】
System.out.println(stack); // 先打印看一下栈里面有哪些元素
int number2 = stack.peek();
System.out.println(number2);
System.out.println(stack); // 再打印一下这个栈,验证一下是否被删除
// 看一下栈是否为空
boolean result1 = stack.empty();
System.out.println(result1);
// 再把栈中的元素全部删完
stack.pop();
stack.pop();
stack.pop();
// 这个时候再看一下栈是否为空
boolean result2 = stack.empty();
System.out.println(result2);
// 获取栈的长度【因为刚刚已经把栈中的元素删完了,所以这里需要往栈里面添加一些元素】
stack.push(99);
stack.push(101);
stack.push(103);
stack.push(105);
// 一共是添加了4个元素,接下来验证一样是不是4个元素
int ret = stack.size();
System.out.println(ret);
}
上述代码就是对栈的一些方法进行一些简单的测试
栈的模拟实现
下述代码是我们自己模拟实现的一个栈的方法:
package stack_queue;
import java.util.Arrays;
public class Mystack {
private int[] elem;
private int usedSize;
private static final int DEFAULT_CAPACIPY = 10; // 默认数组的长度是10
// 再来提供一个构造方法
public Mystack(){
this.elem = new int[DEFAULT_CAPACIPY];
}
/**
* 入栈方法
* @param data
*/
public void push(int data){
// 如果满的话要进行扩容
if (isFull()){
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
this.elem[this.usedSize] = data;
this.usedSize++;
}
/**
* 删除栈中的元素【从栈顶开始删除】
* @return
*/
public int pop(){
// 栈为空能删除元素吗?
if (empty()){
System.out.println("当前栈为空!!");
return -1;
}
// return this.elem[usedSize--];
int oldval = elem[usedSize-1];
usedSize--;
return oldval;
}
/**
* 判断栈是否为空
* @return
*/
public boolean empty(){
if (usedSize == 0){
return true;
}
return false;
}
/**
* 获取栈顶元素,但是不删除栈顶元素
* @return
*/
public int peek(){
// 首先要判断一下是否为空
if (empty()){
System.out.println("当前栈为空!!");
return -1;
}
return elem[usedSize-1];
}
/**
* 获取栈中元素的个数
* @return
*/
public int size(){
return usedSize;
}
/**
* 判断一下这个栈有没有满,如果满的话,我们要做一些扩容【扩容的方法要记得】
* @return
*/
public boolean isFull(){
return usedSize == elem.length;
}
}
栈的应用场景
1. 改变元素的序列
大家可以做一下上面两道题,答案分别是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().val + " ");
}
}
3. 括号匹配
20. 有效的括号 - 力扣(LeetCode)
核心要点:字符串遍历完成 && 栈为空
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); // 肯定是括号
if(ch == '(' || ch == '{' || ch == '['){
//左括号就入栈
stack.push(ch);
}else{
//不是左括号,肯定是右括号了
if(stack.empty()){
return false;
}
// 喵一眼栈顶的元素
char top = stack.peek();
if(ch == '}' && top == '{' || ch == ')' && top == '(' || ch == ']' && top == '['){
stack.pop();
}else{
return false;
}
}
}
if(!stack.empty()){
return false;
}
return true;
}
}
4. 逆波兰表达式求值
150. 逆波兰表达式求值 - 力扣(LeetCode)
首先我们要了解什么是中缀表达式?什么是后缀表达式?中缀表达式怎么转成后缀表达式?后缀表达式怎么转成中缀表达式?
小编以图解的但是对此 123*+45*6+7*+ 这个后缀表达式用栈的方式进行计算;
如上图所示是利用栈进行后缀表达式计算的所有图示过程。可利用上述进行代码思路的搭建。
方法1:是一种比较低效的方法
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i];
if (token.equals("/") || token.equals("*") || token.equals("+") || token.equals("-")) {
int right = stack.pop();
int left = stack.pop();
int result = 0;
if (token.equals("/")) {
result = left / right;
} else if (token.equals("*")) {
result = left * right;
} else if (token.equals("+")) {
result = left + right;
} else if (token.equals("-")) {
result = left - right;
}
stack.push(result);
} else {
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}
}
方法2:相比之前的方法,效率有所提高
5. 出栈入栈次序匹配
栈的压入、弹出序列_牛客题霸_牛客网
这个题就是我们在做“不可能的出栈顺序”体型的代码实现。【入栈的同时是可以出栈的!】
上述是小编关于这个题目要求写的 一个图解,希望能够帮助同学们搭建代码的结构。
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pushV int整型一维数组
* @param popV int整型一维数组
* @return bool布尔型
*/
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]);
while(!stack.empty()&&stack.peek() == popV[j] && j < popV.length){
stack.pop();
j++;
}
}
if(stack.empty()){
return true;
}
return false;
}
}
6. 最小栈
155. 最小栈 - 力扣(LeetCode)
上述是小编关于这个题目要求写的 一个图解,希望能够帮助同学们搭建代码的结构。【上述两图,图一是入栈,图一是出栈】
代码示例:小编的代码只供参考
class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minstack;
public MinStack() {
stack = new Stack<>();
minstack = new Stack<>(); //初始化对象
}
public void push(int val) {
stack.push(val);
if(minstack.empty()|| minstack.peek() >= val){
minstack.push(val);
}
}
public void pop() {
int delete = stack.pop();
if(!minstack.empty()&& minstack.peek() == delete){
minstack.pop();
}
}
// 获取栈顶元素【注意不是删除】
public int top() {
return stack.peek();
}
// 获取堆栈中的最小元素【注意不是删除】
public int getMin() {
return minstack.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(val);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
概念区分
队列(Queue)
概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾(Tail/Rear) 出队列:进行删除操作的一端称为队头(Head/Front.
队列的使用
在Java中,Queue是个接口,底层是通过链表实现的。
下面是关于队列的一些方法:
下面是关于上图中代码的测试:
/**
* 先来一个queue已有方法的测试
*/
public static void main(String[] args) {
Deque<Integer> deque = new LinkedList<>(); // 这是一个双端队列
Queue<Integer> queue = new LinkedList<>(); // 这是一个单端队列
// 往队列里面放元素
queue.offer(11);
queue.offer(22);
queue.offer(33);
System.out.println(queue);
System.out.println("========================");
// 从队列里面拿元素【先进的先出】(删除)
int ret1 = queue.poll();
System.out.println(ret1);
System.out.println(queue);
System.out.println("=========================");
// 获取队头元素【这是回去不是删除】
int ret2 = queue.peek();
System.out.println(ret2);
System.out.println(queue);
System.out.println("=========================");
// 获取队列的长度
queue.offer(99);
queue.offer(520);
queue.offer(1314);
System.out.println(queue); // 先打印一下这个列表
int size = queue.size();
System.out.println("列表的长度为:" + size);
// 检测队列是否为空
boolean result1 = queue.isEmpty();
System.out.println(result1);
queue.poll();
queue.poll();
queue.poll();
queue.poll();
queue.poll();
boolean result2 = queue.isEmpty();
System.out.println(result2);
}
如上图所示,在队列里面,其中某一个功能有很多方法可以实现,但是每个方法都有其区别,大家可以私下在查阅资料进行比对。下面是他们的一些适用场景:
队列模拟实现
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,通过前面线性表的学习了解到常见的空间类型有两种:顺序结构 和 链式结构。同学们思考下:队列的实现使用顺序结构还是链式结构好?
方法实现代码:
package stack_queue;
/**
* 这个模拟队列的实现基于 双链表 去写
*/
public class Myqueue {
static class Listnode{
public int val;
public Listnode next;
public Listnode pre;
// 再来一个构造方法
public Listnode(int val) {
this.val = val;
}
}
public Listnode head; //头节点【队头】
public Listnode last; //尾巴节点【队尾】
/**
* 1.模拟实现往队列里面放元素
*/
public void offer(int val){
Listnode node = new Listnode(val);
// 该链表为空【即该队列为空】
if (head == null){
head = node;
last = node;
return;
}
// 如果链表不为空【进行尾插法】
last.next = node;
node.pre = last;
last = node;
}
/**
* 2.模拟实现从队列中取出元素
*/
public int poll(){
// 如果链表为空
if (head == null){
return -1;
}
Listnode cur = head;
// 如果链表只有一个元素
if (head.next == null){
head = null;
last = null;
return cur.val;
}
// 链表有两个或两个以上的元素
head = head.next;
head.pre = null; //把head的前驱置空
return cur.val;
}
/**
* 3.获取队头元素
*/
public int peek(){
// 如果链表为空
if (head == null){
return -1; // 或者选择抛一个异常【这样更好】
}
// 如果链表不为空
return head.val;
}
/**
* 4.获取队列长度
*/
public int size(){
int usedSize = 0;
Listnode cur = head;
while (cur != null){
usedSize++;
cur = cur.next;
}
return usedSize;
}
/**
* 检测队列是否为空
*/
public boolean isEmpty(){
if (size() == 0){
return true;
}
return false;
}
/**
* 重写toString,为了方便测试的时候看测试结果
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("["); // 以数组的格式展示
Listnode cur = head;
while (cur != null) {
sb.append(cur.val);
if (cur.next != null) { // 不是最后一个节点,就添加逗号
sb.append(", ");
}
cur = cur.next;
}
sb.append("]");
return sb.toString();
}
}
测试代码:
/**
* 这个是测试模拟队列的实现
*/
public static void main(String[] args) {
Myqueue myqueue = new Myqueue();
/**
* 1.往队列里面放元素
*/
myqueue.offer(11);
myqueue.offer(22);
myqueue.offer(33);
myqueue.offer(44);
System.out.println(myqueue.toString());
System.out.println("======================");
/**
* 2.从队列中取出元素
*/
int ret1 = myqueue.poll();
System.out.println(myqueue.toString());
System.out.println("取出的元素是:"+ret1);
System.out.println(myqueue.toString());
System.out.println("======================");
/**
* 3.获取队头元素
*/
int ret2 = myqueue.peek();
System.out.println(myqueue.toString());
System.out.println("队头元素是:" + ret2);
System.out.println(myqueue.toString());
System.out.println("======================");
/**
* 4.获取队列长度
*/
System.out.println(myqueue.toString()); // 打印一下列表来验证长度
int size = myqueue.size();
System.out.println("队列的长度为:" + size);
System.out.println("======================");
/**
* 5.判断队列是否为空
*/
boolean result1 = myqueue.isEmpty();
System.out.println(result1);
// 然后把队列中的元素都删除
myqueue.poll();
myqueue.poll();
myqueue.poll();
// 再来测试一下
boolean result2 = myqueue.isEmpty();
System.out.println(result2);
System.out.println("=======================");
}
注:小编再这里要做一个必要的提醒,小编的代码引导,是其中的一个思路。并不是唯一的实现方法,也不一定是最优的方法。
循环队列
622. 设计循环队列 - 力扣(LeetCode)
提出一个问题:根据前面关于队列的知识,我们在入队的同时,也可以出队。但是对于数组来讲,如果一个数字只能存放10个元素,然后当我们在入队的同时出队,实际上数组还有空位,但是此时按照队列的思路,我们无法往数组里面添加元素了,那数组的存储空间不是白白浪费掉了吗?因此,我们提出了一个循环队列。
实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列通常使用数组实现。
那到底什么时候才满呢?提供以下集中思路:
判断是否满了:
1.定义一个usedSize == len;
2.做一个标记;
3.浪费一个空间来区分;
下标的变化:
实现代码示例:
/**
下面这个代码是浪费一个数组空间来实现环形队列的
*/
class MyCircularQueue {
private int[] elem; // 创建出一个数组
private int front;
private int rear;
public MyCircularQueue(int k) {
// 给这个数组分配一个内存空间
this.elem = new int[k+1]; // 分配一个k+1长度的数组空间【因为需要浪费一个空间】
}
public boolean enQueue(int value) {
if(isFull()){
return false;
}
elem[rear] = value;
rear = (rear+1)%elem.length;
return true;
}
public boolean deQueue() {
if(isEmpty()){
return false;
}
front = (front+1)%elem.length;
return true;
}
public int Front() {
if(isEmpty()){
return -1;
}
return elem[front];
}
// public int Rear() {
// if(isEmpty()){
// return -1;
// }
// return elem[(rear - 1 + elem.length) % elem.length];
// }
public int Rear() {
if(isEmpty()){
return -1;
}
return elem[(rear - 1 + elem.length) % elem.length];
}
public boolean isEmpty() {
return front == rear;
}
public boolean isFull() {
return (rear + 1) % elem.length == front;
}
}
/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue obj = new MyCircularQueue(k);
* boolean param_1 = obj.enQueue(value);
* boolean param_2 = obj.deQueue();
* int param_3 = obj.Front();
* int param_4 = obj.Rear();
* boolean param_5 = obj.isEmpty();
* boolean param_6 = obj.isFull();
*/
双端队列 (Deque)
双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
Deque是一个接口,使用时必须创建LinkedList的对象。
在实际工程中,使用Deque接口是比较多的,栈和队列均可以使用该接口。
Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现
在 Java 集合框架(Collection Framework) 中:
Deque
是 接口,表示一个双端队列(Double-ended Queue),即可以在头部和尾部插入或删除元素。ArrayDeque
和LinkedList
都是Deque
的实现类,提供了两种不同的数据结构方式:
-
Deque<E>
(接口)Deque
继承自Queue
接口,提供 双端插入、删除 功能。- 既可以用作队列(FIFO)也可以用作栈(LIFO)。
-
ArrayDeque<E>
(基于数组的双端队列实现)- 底层是 可扩容的数组,类似
ArrayList
。 - 优点:
- 随机访问快,比
LinkedList
速度更快。 - 无锁,比
LinkedBlockingDeque
更高效。
- 随机访问快,比
- 缺点:
- 内存占用大,因为需要提前分配数组空间,并且扩容会有一定的开销。
- 底层是 可扩容的数组,类似
-
LinkedList<E>
(基于双向链表的双端队列实现)- 底层是 双向链表,类似
LinkedList
结构。 - 优点:
- 动态扩容,不需要像
ArrayDeque
那样预留数组空间。 - 插入和删除快,因为不需要移动大量元素(相比
ArrayDeque
)。
- 动态扩容,不需要像
- 缺点:
- 访问慢,因为链表结构没有连续内存,访问需要遍历。
- 底层是 双向链表,类似
面试题
1. 用队列实现栈
225. 用队列实现栈 - 力扣(LeetCode)
思路:
1.当两个队列都为空的时候,说明我模拟实现的栈是空的
2."出栈"的时候,出并不为空的队列,size-1个【size--】.最后那个元素就是我出栈的元素
3."入栈"的时候入到不为空的队列
代码示例:
class MyStack {
/**
先创建两个队列
*/
Queue<Integer> queue1 = null;
Queue<Integer> queue2 = null;
public MyStack() {
queue1 = new LinkedList();
queue2 = new LinkedList();
}
public void push(int x) {
if(!queue1.isEmpty()){
queue1.offer(x); // 那个队列不为空就入那个队列
}else{
queue2.offer(x);
}
}
public int pop() {
if (empty()) {
return -1;
}
Queue<Integer> active = queue1.isEmpty() ? queue2 : queue1;
Queue<Integer> backup = queue1.isEmpty() ? queue1 : queue2;
while (active.size() > 1) {
backup.offer(active.poll());
}
return active.poll();
}
public int top() {
if (empty()) {
return -1;
}
Queue<Integer> active = queue1.isEmpty() ? queue2 : queue1;
Queue<Integer> backup = queue1.isEmpty() ? queue1 : queue2;
int res = -1;
while (!active.isEmpty()) {
res = active.poll();
backup.offer(res);
}
return res;
}
public boolean empty() {
// 当两个队列都为空的时候,说明我当前模拟实现的栈是空的
return (queue1.isEmpty() && queue2.isEmpty());
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
2. 用栈实现队列
232. 用栈实现队列 - 力扣(LeetCode)
核心思路:
1.入队的时候,把所有元素全部放在第一个栈中
2.在出队之前要判断一下两个栈空不空,如果两个栈都为空,说明该队列中已经没有元素了。假设栈没有空,判断一下第二个栈中的元素为不为空,如果不为空则出第二个栈中元素;如果为空则把第一个栈中元素全部倒回第二个栈中,然后紧接着出第二个栈的元素。
上述是小编关于这个题目要求写的 一个图解,希望能够帮助同学们搭建代码的结构。
代码示例:
class MyQueue {
private Stack<Integer> stack1;
private Stack<Integer> stack2;
// 进行栈的实例化
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
public void push(int x) {
// 往stack1里面放
stack1.push(x);
}
public int pop() {
// 首先判断一下队列是否为空
if(empty()){
return -1;
}
// 如果队列不为空
if(!stack2.empty()){
// 如果栈2不为空就从栈2中取出元素
return stack2.pop();
}else{
// 如果栈2为空,则需要把栈2中所有的元素都搬到栈2中来【注意是所有元素】
while(!stack1.empty()){
stack2.push(stack1.pop());
}
return stack2.pop();
}
}
// 偷偷瞄一眼但是不删除
public int peek() {
// 首先判断一下队列是否为空
if(empty()){
return -1;
}
// 如果队列不为空
if(!stack2.empty()){
// 如果栈2不为空就从栈2中取出元素
return stack2.peek();
}else{
// 如果栈2为空,则需要把栈2中所有的元素都搬到栈2中来【注意是所有元素】
while(!stack1.empty()){
stack2.push(stack1.pop());
}
return stack2.peek();
}
}
public boolean empty() {
// 两个栈为空,队列才为空
return (stack1.empty() && stack2.empty());
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/