🌠作者:@TheMythWS.
🎆专栏:《集合与数据结构》
🎇座右铭:不走心的努力都是在敷衍自己,让自己所做的选择,熠熠发光。
目录
栈 ( Stack )
栈的概念
栈的使用
栈的模拟实现
栈的应用场景
1. 改变元素的序列
2. 将递归转化为循环
3. 括号匹配
4. 逆波兰表达式求值
5. 出栈入栈次序匹配
6. 最小栈
概念区分
队列(Queue)
概念
队列的使用
队列模拟实现
顺序队列
循环队列
练习题
双端队列 (Deque)
面试题
1. 用队列实现栈
2. 用栈实现队列
栈 ( Stack )
栈的概念
定义:限定只在表的一端(表尾)进行插入和删除操作的线性表
特点:后进先出(LIFO) 或者 先进后出(FILO)
允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈, 出数据在栈顶。
形象理解:
栈的使用
因为栈相对比较简单, 我们先使用栈再模拟实现.
public static void main(String[] args) {
Stack<Integer> s = new Stack();
s.push(1);
s.push(2);
s.push(3);
s.push(4);
System.out.println(s.size()); // 获取栈中有效元素个数---> 4
System.out.println(s.peek()); // 获取栈顶元素---> 4
s.pop(); // 4出栈,栈中剩余1 2 3,栈顶元素为3
System.out.println(s.pop()); // 3出栈,栈中剩余1 2 栈顶元素为3
if (s.empty()) {
System.out.println("栈空");
} else {
System.out.println(s.size());
}
}
public static void main1(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
Integer a = stack.pop();//3
System.out.println(a);
Integer b = stack.peek();//2
System.out.println(b);
Integer b2 = stack.peek();//2
System.out.println(b2);
System.out.println(stack.size());//2
System.out.println(stack.isEmpty());
}
栈的模拟实现
从上图中可以看到,Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的,效率低(淘汰)。
public class MyStack {
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++;*/
elem[usedSize++] = val;
}
public boolean isFull() {
return usedSize == elem.length;
}
//出栈
public int pop() {
if (isEmpty()) {
throw new EmptyException("栈空");
}
/*int val = elem[usedSize - 1];//要出去的值
usedSize--;
return val;*/
/*usedSize--;
return elem[usedSize];*/
return elem[--usedSize];
}
public boolean isEmpty() {
return usedSize == 0;
}
//获取栈顶元素
public int peek() {
if (isEmpty()) {
throw new EmptyException("栈为空, 无法获取栈顶元素.");
}
return elem[usedSize - 1];
}
public int size() {
return usedSize;
}
}
public class EmptyException extends RuntimeException{
public EmptyException() {
}
public EmptyException(String message) {
super(message);
}
}
栈的应用场景
1. 改变元素的序列
(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
(2)一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( )。
A: 12345ABCDE B: EDCBA54321 C: ABCDE12345 D: 54321EDCBA
2. 将递归转化为循环
比如:逆序打印链表
/*
逆序打印链表(递归的方式)
*/
public void reverseDisplay(ListNode pHead) {
//写法1:
if (pHead == null) {//没有结点
return;
}
if (pHead.next == null) {//只有一个结点
System.out.print(pHead.val + " ");
return;
}
reverseDisplay(pHead.next);
System.out.print(pHead.val + " ");
//写法2:
/*if (pHead != null) {//出口:pHead.next == null
reverseDisplay(pHead.next);
System.out.print(pHead.val + " ");
}*/
}
/*
逆序打印链表(遍历栈的方式)
*/
public void displayWithStack(ListNode head) {
if (head == null) {
return;
}
Stack<ListNode> s = new Stack<>();
//将链表中的结点保存在栈中
ListNode cur = head;
while (cur != null) {
s.push(cur);
cur = cur.next;
}
//遍历栈, 将栈中的元素出栈
while (!s.empty()) {
System.out.print(s.pop().val + " ");
}
}
public static void main(String[] args) {
MySingleLinkedList list = new MySingleLinkedList();
list.addLast(1);
list.addLast(2);
list.addLast(3);
list.addLast(4);
//逆序打印链表(递归)
list.reverseDisplay(list.head);
System.out.println("");
//逆序打印链表(遍历栈)
list.displayWithStack(list.head);
}
如有需要,MySingleLinekdList的代码参看前面的LInkedList章节。
3. 括号匹配
做题链接:力扣
public boolean isValid(String s) {
/*
([)]
())
(()
)()
*/
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);//ch ( ) { } [ ]
if (ch == '(' || ch == '{' || ch == '[') {//左括号就入栈
stack.push(ch);
//stack里面的一定是左括号
} else {
//遇到右括号
if (stack.empty()) {
//一开始就遇到右括号, 且栈是空的.
return false;//说明此时右括号多.
}
char ch2 = stack.peek();//查看栈顶元素, 判断是否跟遍历的右括号匹配, 匹配就出栈.
if (ch == ')' && ch2 == '(' || ch == '}' && ch2 == '{' || ch == ']' && ch2 == '[') {
stack.pop();//匹配就出栈
} else {
return false;//不匹配
}
}
}
if (!stack.empty()) {//栈不为空, 左括号多
return false;
}
return true;
}
4. 逆波兰表达式求值
做题链接:力扣
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (String x : tokens) {
if (!isOperation(x)) {//是数字就入栈
stack.push(Integer.parseInt(x));
} else {//是操作符就取出两个元素
int num2 = stack.pop();//第二个操作数, 栈顶先弹出
int num1 = stack.pop();//第一个操作数
switch (x) {
case "+":
stack.push(num1 + num2);
break;
case "-":
stack.push(num1 - num2);
break;
case "*":
stack.push(num1 * num2);
break;
case "/":
stack.push(num1 / num2);
break;
}
}
}
return stack.pop();//peek()
}
private boolean isOperation(String x) {
return (x.equals("+") || x.equals("-") || x.equals("/") || x.equals("*"));
}
}
5. 出栈入栈次序匹配
做题链接:力扣
class Solution {
public boolean validateStackSequences(int[] pushA, int[] popA) {
Stack<Integer> stack = new Stack<>();
int j = 0;
for (int i = 0; i < pushA.length; i++) {
stack.push(pushA[i]);//先入栈, 再判断
//注意j下标越界, 栈空指针异常的情况.
//stack.peek() == popA[j] 修改为 stack.peek().equals(popA[j]), -128-127比较的是数值, 不在这个范围==比较的就是地址了, 所以保险起见, 用equals
while (j < popA.length && !stack.empty() &&
stack.peek().equals(popA[j])) {//如果栈顶元素和j下标的值相等就出栈,同时j++
stack.pop();
j++;
}
}
return stack.empty();
}
}
6. 最小栈
做题链接:力扣
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为空就入栈
minStack.push(val);
} else {
//minStack不为空, 每次存入最小的值
if (val <= minStack.peek()) {//注意<=的细节!!!遇到相同的最小的值, minStack也要入栈.
minStack.push(val);
}
}
}
public void pop() {
/*
if (!stack.empty()) {
Integer val = stack.pop();
if (val.equals(minStack.peek())) {
minStack.pop();
}
}
*/
//如果stack出栈的元素 跟 最小栈的元素相等, 那么最小栈的元素也要出栈
if (!stack.empty()) {//栈不为空, 才能出栈.
//维护最小栈
if (stack.pop().equals(minStack.peek())) {
minStack.pop();
}
}
}
public int top() {//peek()
if (!stack.empty()){
return stack.peek();
}
return -1;
}
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();
*/
上面只介绍了栈的顺序存储,当然栈还有链式存储, 下面是链栈的使用:
public static void main(String[] args) {
//链栈
LinkedList<Integer> linkedStack = new LinkedList<>();
linkedStack.push(1);
linkedStack.push(2);
linkedStack.push(3);
linkedStack.push(4);
System.out.println(linkedStack.pop());
System.out.println(linkedStack.peek());
}
思考:为什么LinkedList可以被当作栈来使用?
主要是因为LinkedList具有栈的几个基本功能:
1.元素的插入和删除
LinkedList可以在列表的首部和尾部插入元素,也可以在指定位置删除元素。这与栈的特性相似,栈也是在栈顶插入元素和在栈顶删除元素。
2.后进先出(LIFO)
LinkedList也可以遵循后进先出(LIFO)的规则来访问和操作元素。通过在列表的头部插入元素和删除元素,就可以实现后进先出的功能。
3.可动态调整大小
LinkedList的大小是可动态调整的,可以动态添加或删除元素。同样,栈的大小也可以动态调整。
因此,由于LinkedList具有以上基本功能,可以像栈一样使用它。
概念区分
栈、虚拟机栈、栈帧有什么区别呢?
-
栈(Stack):栈是一种数据结构,具有“后进先出”的特点。在计算机程序中,栈常常被用来保存程序执行过程中的临时变量、函数调用的参数和返回值等。栈的存储空间是连续的,一般由操作系统分配和管理。
-
虚拟机栈(Java Virtual Machine Stack):虚拟机栈是Java虚拟机的一部分,用来存储Java方法的执行信息,包括方法的局部变量表、操作数栈、动态链接、方法返回地址和异常处理信息等。虚拟机栈的大小可以在启动JVM时指定,一般由JVM自动管理。
-
栈帧(Stack Frame):栈帧是程序执行时栈中存储的一块存储空间,用来保存一个方法的执行信息。栈帧一般包括局部变量表、操作数栈、动态链接、方法返回地址和异常处理信息等。每当一个方法被调用时,就会在栈中分配一个新的栈帧,该栈帧保存了调用方法的信息。当方法执行完毕时,该栈帧就被弹出栈。在Java虚拟机中,一个栈帧对应一个方法的执行。
总结起来,栈是一种数据结构,用来存储临时变量等信息;虚拟机栈是Java虚拟机的一部分,用来存储Java方法的执行信息;而栈帧则是程序执行时栈中存储的一块存储空间,用来保存一个方法的执行信息。
队列(Queue)
概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(FirstIn First Out)
入队列:进行插入操作的一端称为队尾(Tail/Rear)
出队列:进行删除操作的一端称为队头(Head/Front)
队列的使用
在Java中,Queue是个接口,底层是通过链表实现的。
public static void main(String[] args) {
Queue<Integer> q = new LinkedList<>();
q.offer(1);
q.offer(2);
q.offer(3);
q.offer(4);
q.offer(5); // 从队尾入队列
System.out.println(q.size());//5
System.out.println(q.peek()); // 获取队头元素1
q.poll();//1出队列
System.out.println(q.poll()); // 2从队头出队列,并将删除的元素返回
if (q.isEmpty()) {
System.out.println("队列空");
} else {
System.out.println(q.size());//剩余3个元素
}
}
思考:为什么LinkedList可以当作队列来使用?
LinkedList 可以当作队列来使用,是因为 LinkedList 实现了 Queue 接口,而 Queue 接口就是队列的抽象表示。因此,LinkedList 可以使用 Queue 接口中定义的方法,如 `add()`,`offer()`,`remove()`,`poll()`,`element()` 和 `peek()`等来操作元素,从而实现队列的功能。具体来说,LinkedList 的 `add()` 和 `offer()` 方法在队列尾部添加元素,`remove()` 和 `poll()` 方法移除队列头部的元素,`element()` 和 `peek()` 方法获取队列头部的元素,这些方法都符合队列的特点。
注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。
队列模拟实现
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,通过前面线性表的学习了解到常见的空间类型有
两种:顺序结构 和 链式结构。
思考下:队列的实现使用顺序结构还是链式结构好?
对于队列的实现,顺序结构和链式结构都有各自的优缺点。
(1)顺序结构的优点是:实现简单,易于理解和实现,并且空间利用率高,不会出现指针浪费空间的情况。另外,顺序结构的访问速度比链式结构要快,因为可以直接通过下标来访问队列元素。
顺序结构的缺点是:在队列插入和删除元素时,需要移动大量的元素,效率较低,并且如果队列元素数量超过数组的长度,需要进行扩容操作,空间复杂度较高。
(2)链式结构的优点是:插入和删除元素时不需要移动其他元素,只需要改变指针的指向即可,效率较高,并且可以动态地分配存储空间,节约空间。
链式结构的缺点是:实现相对比较复杂,需要考虑指针的管理和指向的问题,并且由于指针本身需要占用额外的存储空间,因此空间利用率相对较低。
综上所述,选择顺序结构还是链式结构,需要根据具体的需求和实际情况来选择。如果需要高效地访问队列元素,并且队列元素数量不会太大,可以选择顺序结构。如果队列元素数量较大或者需要频繁插入和删除元素,可以选择链式结构。
单链表实现
public class MyQueue {
//单链表实现, 多了一个last尾巴结点的引用, 局限:只能尾部增加, 头部删除
static class Node {
public int val;
public Node next;
public Node(int val) {
this.val = val;
}
}
public Node head;
public Node last;//这儿引入一个尾结点的引用, 尾插法会很方便
public int usedSize;
//入队
public void offer(int val) {
Node node = new Node(val);
if (head == null) {//第一次入队
head = node;
last = node;
} else {//尾巴后面入队
last.next = node;
last = node;
}
usedSize++;
}
//出队
public int poll() {
if (empty()) {
throw new EmptyException("队列为空");
}
int ret = head.val;
head = head.next;
if (head == null) {
last = null;//只有一个结点, last也要置空
}
usedSize--;
return ret;
}
public boolean empty() {
return usedSize == 0;
}
//查看队首元素
public int peek() {
if (empty()) {
throw new EmptyException("队列为空");
}
return head.val;
}
}
双向链表实现
public class MyDoubleQueue {
// 双向链表结点
public static class ListNode {
ListNode prev;
ListNode next;
int value;
ListNode(int value) {
this.value = value;
}
}
ListNode head; // 队头
ListNode last; // 队尾
int size = 0;
// 入队列---向双向链表位置插入新节点
public void offer(int e) {
ListNode newNode = new ListNode(e);
if (head == null) {//第一次入队
head = newNode;
// last = newNode;
} else {//尾巴后面入队
last.next = newNode;
newNode.prev = last;
// last = newNode;
}
last = newNode;
size++;
}
// 出队列---将双向链表第一个节点删除掉
public int poll() {
// 1. 队列为空
// 2. 队列中只有一个元素----链表中只有一个节点---直接删除
// 3. 队列中有多个元素---链表中有多个节点----将第一个节点删除
int value = 0;
if (empty()) {
throw new EmptyException("队列为空");
} else if (head == last) {
last = null;
head = null;
} else {
value = head.value;
head = head.next;
head.prev.next = null;
head.prev = null;
}
size--;
return value;
}
// 获取队头元素---获取链表中第一个节点的值域
public int peek() {
if (empty()) {
throw new EmptyException("队列为空");
}
return head.value;
}
public int size() {
return size;
}
public boolean empty() {
return size == 0;
}
}
顺序队列
循环队列
实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列通常使用数组实现。
如何区分空与满:
1. 通过添加 size 属性记录
2. 保留一个位置
3. 使用标记
设计循环队列:力扣
采用保留一个位置的写法:
class MyCircularQueue {
private int[] elem;
private int front;//队首
private int rear;//队尾
public MyCircularQueue(int k) {
//浪费一个空间的写法(保留一个位置的写法), 这儿必须要多 + 1
this.elem = new int[k + 1];
//this.elem = new int[k];
}
/**
* 入队列
*
* @param value
* @return
*/
public boolean enQueue(int value) {
//1.检查队列是否为满
if (isFull()) {
return false;
}
//2.队列不满
elem[rear] = value;
//rear++;//x
rear = (rear + 1) % elem.length;
return true;
}
/**
* 出队列
*
* @return
*/
public boolean deQueue() {
//1.检查队列是否为空
if (isEmpty()) {
return false;
}
//2.队列不为空
//front++;//x
front = (front + 1) % elem.length;
return true;
}
/**
* 得到队首元素
*
* @return
*/
public int Front() {
if (isEmpty()) {
return -1;
}
return elem[front];
}
/**
* 得到队尾元素
*
* @return
*/
public int Rear() {
if (isEmpty()) {
return -1;
}
/*
return elem[rear - 1];
这种写法有问题, 假设rear在0下标? 0 - 1 = -1 下标有问题.
应该定义一个变量来判断rear在不同位置的下标变化.
如果rear下标在0的位置, 说明数组的最后一个元素就是队尾元素
否则就是rear - 1下标的元素
*/
int index = (rear == 0) ? elem.length - 1 : rear - 1;
return elem[index];
}
public boolean isEmpty() {
return front == rear;
}
/**
* 队列是否为满
*
* @return
*/
public boolean isFull() {
/*if ((rear + 1) % elem.length == front) {
return true;
}
return false;*/
return (rear + 1) % elem.length == front;
}
}
练习题
1.数组Q[n]用来表示一个循环队列,f为当前队列头元素的前一位置,r为队尾元素的位置,假定队列中元素的个数小于n,计算队列中元素个数的公式为( )。
A. r-f
B. (n+f-r)%n
C. n+r-f
D.(n+r-f)%n
答案:D
解释:对于非循环队列,尾指针和头指针的差值便是队列的长度,而对于循环队列,差值可能为负数,所以需要将差值加上MAXSIZE(本题为n),然后与MAXSIZE(本题为n)求余,即(n + r - f) % n。
2.为解决计算机主机与打印机间速度不匹配问题,通常设一个打印数据缓冲区。主机将要输出的数据依次写入该缓冲区,而打印机则依次从该缓冲区中取出数据。该缓冲区的逻辑结构应该是( ) 。
A.队列
B.栈
C.线性表
D.有序表
答案:A
解释:解决缓冲区问题应利用一种先进先出的线性表,而队列正是一种先进先出的特殊线性表。
双端队列 (Deque)
双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。
那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
Deque是一个接口,使用时必须创建LinkedList的对象。
在实际工程中,使用Deque接口是比较多的,栈和队列均可以使用该接口。
public static void main(String[] args) {
Queue<Integer> q = new LinkedList<>();//链式队列, 底层是双向链表
Deque<Integer> deque1 = new ArrayDeque<>();//双端队列的线性实现,底层数组
Deque<Integer> deque2 = new LinkedList<>();//双端队列的链式实现,底层是双向链表
Deque<Integer> arrayStack1 = new ArrayDeque<>();//线性栈, 底层是数组
Stack<Integer> arrayStack2 = new Stack<>();//线性栈, 底层是数组
Deque<Integer> linkedStack1 = new LinkedList<>();//链式栈, 底层是双向链表
LinkedList<Integer> linkedStack2 = new LinkedList<>();//链式栈,底层是双向链表
LinkedList<Integer> linkedQueque = new LinkedList<>();//链式队列,底层是双向链表
}
面试题
1. 用队列实现栈
OJ链接
public class MyStack {
private Queue<Integer> qu1;
private Queue<Integer> qu2;
public MyStack() {
qu1 = new LinkedList<>();
qu2 = new LinkedList<>();
}
//入栈: 不为空的队列
public void push(int x) {
if (!qu1.isEmpty()) {
qu1.offer(x);
} else if (!qu2.isEmpty()) {
qu2.offer(x);
} else {//两个都为空, 放进第一个队列.
qu1.offer(x);
}
}
//出栈: 不为空的队列,出size-1个元素到另一个队列当中
public int pop() {
if (empty()) {
return -1;//两个队列都为空了, 说明栈一定为空.
}
if (!qu1.isEmpty()) {
int size = qu1.size();
for (int i = 0; i < size - 1; i++) {
/*Integer val = qu1.poll();
qu2.offer(val);*/
qu2.offer(qu1.poll());
}
//不能是以下的写法!!!每次size会变化.
/*for (int i = 0; i < qu1.size() - 1; i++) {
qu2.offer(qu1.poll());
}*/
return qu1.poll();
} else {
int size = qu2.size();
for (int i = 0; i < size - 1; i++) {
/*Integer val = qu2.poll();
qu1.offer(val);*/
qu1.offer(qu2.poll());
}
return qu2.poll();
}
}
//peek
public int top() {
//注意和pop的区别.
if (empty()) {
return -1;//两个队列都为空了, 说明栈一定为空.
}
if (!qu1.isEmpty()) {
int size = qu1.size();
Integer val = -1;
for (int i = 0; i < size; i++) {//要出所有的元素, val记录的最后一次出的元素就是栈顶元素
val = qu1.poll();
qu2.offer(val);
}
return val;
} else {
int size = qu2.size();
Integer val = -1;
for (int i = 0; i < size; i++) {
val = qu2.poll();
qu1.offer(val);
}
return val;
}
}
//判断栈是否为空
public boolean empty() {
//两个队列都为空了, 说明栈一定为空.
return qu1.isEmpty() && qu2.isEmpty();
}
}
2. 用栈实现队列
OJ链接
public class MyQueue {
private Stack<Integer> stack1;
private Stack<Integer> stack2;
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
//入队列: 往stack1栈入栈
public void push(int x) {
stack1.push(x);
}
//出队列:
public int pop() {
if (empty()) {//两个栈同时为空了, 说明此时队列一定为空.
return -1;
}
if (stack2.empty()) {//如果stack2栈为空, 需要把stack1栈的所有元素入stack2栈
while (!stack1.empty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();//要出stack2栈的栈顶元素 就是 出队列的元素
}
//查看队首元素:
public int peek() {
if (empty()) {//两个栈同时为空了, 说明此时队列一定为空.
return -1;
}
if (stack2.empty()) {//如果stack2栈为空, 需要把stack1栈的所有元素入stack2栈
while (!stack1.empty()) {
stack2.push(stack1.pop());
}
}
return stack2.peek();//查看stack2栈的栈顶元素 就是 队首元素
}
//判断队列是否为空
public boolean empty() {
//两个栈都为空了, 说明队列一定为空.
return stack1.empty() && stack2.empty();
}
}