Java距离我上次知道栈和队列有多简单还是在上次
Posted 富春山居_ZYY
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java距离我上次知道栈和队列有多简单还是在上次相关的知识,希望对你有一定的参考价值。
那么接下来,就让我来讲讲道理!
文章目录
引子
栈
、队列
和线性表
一样都是一种线性的数据结构,各个元素之间呈线性关系,但是和一般的线性结构又存在着不同。
对栈
所实施的操作限定在表尾
,主要的操作为进栈、出栈、取栈顶的元素
对队列
所实施的操作限定在表头和表尾
,主要操作为表尾入队、表头出队、表头取对头元素
一、栈
1.1 概念
栈的操作有先进后出的特点
在生活中这样的例子到处都是,使用盘子时,总是会自上而下的取走盘子,洗盘子时,总是会自下而上的将盘子堆叠在一起。一摞厚厚的书,想要拿到压在下面的书,比较安全的做法就是将上面的书一本一本的取走,再拿到想拿的书,再一本一本将书放在上面。
规律总结:无论是取盘子、书,放盘子、书,都只在最上端进行,遵循着后放入的先取出的原则
栈的使用就是如此,先进后出,后进先出
。只允许在固定的一段进行插入元素(即入栈
)删除元素(出栈
),该端被称为栈顶
,那么另一端就为栈底
。栈顶会一直随着插入删除变化着,栈底一直固定不变。
1.2 实例
1.2.1 不可能的出栈顺序
题目:
已知一个栈的入栈序列是 mnxyz
,则不可能出现的出栈顺序是?
A.mnxyz B.xnyzm C.nymxz D.nmyzx
题解:
该题目的重点在于入栈后也可以直接又出栈
选项A: 显而易见的,五个元素分别都入栈后又直接出栈,成立
选项B: mnx依次入栈,x出栈,n再出栈,y入栈后又出栈,z入栈后又出栈,m最后出栈,成立
选项C: mn依次入栈,n出栈,xy依次入栈,y出栈,下一个若是出栈必定是x而不是m,错误
选项D: mn依次入栈,n出栈,m出栈,xy依次入栈,y出栈,z入栈后出栈,x最后出栈,成立
答案:C
1.2.2 中缀表达式转后缀表达式
表达式有三种不同的记法,即前缀、中缀、后缀表达式
,我们日常生活
中常用的表达式是中缀表达式
,运算符在操作数的中间
,前缀表达式
的运算符在操作数的前面
,后缀表达式
的运算符在操作数的后面
。
例如:
中缀表达式
:(((2 + 3) * 4) - (6 / 2))
前缀表达式
:- * + 2 3 4 / 6 2
后缀表达式
: 2 3 + 4 * 6 2 / -
对于人们来说,中缀表达式的形式更易于计算,另外两种没有办法一时间看出来写的是什么
对于计算机来说不然,前缀、后缀表达式的计算更为简单,因此在计算表达式的时候,计算机都会将中缀表达式转化为前缀或后缀表达式
就拿中缀表达式转后缀表达式为例,说一个最简便的转换方式:
- 按照运算符的优先级
依次加上()
- 依次将运算符移动到对应的括号的
后面
(如果是转换为前缀表达式的话则移动到括号前面
)去掉括号
,得到后缀表达式
以表达式((2 + 3) * 4)为例
图解:
后缀表达式计算的方式:
- 如果后缀表达式为数字,就依次入栈
- 如果遇见运算符,就从栈中弹出一个数字放在运算符的
右侧
,再弹出一个数字放在运算符的左侧
- 计算后,将计算结果入栈
- 循环往复,直到栈中没有数字,就完成了整个计算过程
1.3 自我实现
可以用顺序表的方式来实现栈,也可以用链表的方式来实现栈,相对而言,顺序表的实现更加方便简单,这里就用顺序表
来实现栈。
栈的顺序存储结构是指使用一组地址连续的存储单元来依次存放自栈底到栈顶的数据元素,采用顺序的存储方式存储的栈叫做顺序栈,在这里实现这个栈的类 MyStack 就叫顺序栈类
在此处,使用了一维数组int[] elem
来存储栈中的数据元素,设置合适的初始长度;使用变量 usedSized
来记录栈中的数据元素的个数,如果想要删除一个元素(出栈),使其减一,如果想要入栈,使其加一,如果想要清空栈,就可以使其为0
实现方法:
public MyStack()
初始化数组的长度
public boolean isFull()
当发现栈中的元素的个数和数组的长度相等,代表栈满,返回 true,否则返回 false
public void push(int val)
将数据元素val推入栈中,如果栈满,则将实现栈的数组进行扩容,再将数据元素放到数组下标为 usedSized 的地方,数组大小加一
public boolean empty()
判断栈是否为空,当发现 usedSized 大小为0,返回 true,否则返回 false
public int peek()
如果栈不为空,则返回栈顶的数据元素;如果为空,就报异常,提示“栈空了”
public int pop()
如果栈不为空,usedSized 大小减一,则删除栈顶的数据元素并将其返回;如果为空,就报异常,提示“栈空了”
📑代码示例:
import java.util.Arrays;
public class MyStack {
public int[] elem;
public int usedSized;
//初始化
public MyStack() {
this.elem = new int[100];
}
//判断栈是否满
public boolean isFull() {
return (this.usedSized == this.elem.length);
}
//入栈
public void push(int val) {
if (isFull()){
this.elem = Arrays.copyOf(this.elem,this.elem.length*2);
}
this.elem[this.usedSized] = val;
this.usedSized++;
}
//判断栈是否为空
public boolean empty() {
return this.usedSized == 0;
}
//取栈顶元素
public int peek() throws RuntimeException{
if (empty()){
throw new RuntimeException("栈空了");
}
return this.elem[this.usedSized-1];
}
//出栈
public int pop() throws RuntimeException{
if (empty()){
throw new RuntimeException("栈空了");
}
this.usedSized--;
return this.elem[this.usedSized - 1];
}
}
二、队列
1.1 概念
队列的操作有先进先出的特点
和生活当中的排队一样,讲究先来先服务先走,在排队事件中,新成员加入到队尾,每次离开的成员都是来自队头的,即队尾入队,队头出队。
队列的使用就是如此,先进先出,后进后出
。队列只限定在一端进行插入,该端叫队尾
,只限定在另一端删除,另一端叫队头
,插入操作叫入队
,删除操作叫出队
。随着插入和删除,队列的队头和队尾一直在发生改变。
a1是队头元素,an是队尾元素。队列中的元素是以 a1,a2,a3,…,an-1,an的顺序进队列的,按照队列的概念,如果要执行入队操作,入队的元素为an+1,其变成队尾,如果执行出队操作,出队的元素为a1,此时队头为a2。
1.2 实例
1.2.1 无法吃午餐的学生数量
题目:
学校的自助午餐提供圆形和方形的三明治,分别用数字0 和 1
表示。所有学生站在一个队列里,每个学生要么喜欢圆形的要么喜欢方形的。
餐厅里三明治的数量与学生的数量相同。所有三明治都放在一个栈里,每一轮:
如果队列最前面的学生喜欢栈顶的三明治,那么会 拿走它 并离开队列。
否则,这名学生会放弃这个三明治并回到队列的尾部。
这个过程会一直持续到队列里所有学生都不喜欢栈顶的三明治为止。
给你两个整数数组 students
和 sandwiches
,其中 sandwiches[i]
是栈里面第 i 个三明治的类型(i = 0 是栈的顶部), students[j]
是初始队列里第 j 名学生对三明治的喜好(j = 0 是队列的最开始位置)。请你返回无法吃午餐的学生数量
。
题解:
如果将这道题用队列的思想进行解决,那么就应该是这样的。。。
步骤一:首先对学生数组和三明治数组进行判断是否为空,增强代码的健壮性
步骤二:将同学们对三明治的喜好放进一个队列当中
步骤三:进入一个循环,条件就是学生的队列不为空,三明治数组的长度不可超过(本题目三明治的数量与学生的数量相同,该条件一定会成立,所以该条件针对数量不同的情况)
步骤四:在上述的循环当中进行循环判断,如果轮的过程中喜好匹配成功,就将队头的学生喜好送走,三明治数组的 j 下标向后走一格,若学生队头和三明治数组的 j 下标的元素不同,就将其从队头送走,送到队尾去,队列中所有成员都轮了一遍,都没人领走三明治,就说明此时队列的大小就是没饭吃的学生数目
步骤五:循环走完了,仍然没有返回,在本题的条件下,队列一定为空了,如果三明治的数量与学生的数量不相同的话,也可能是因为三明治数量少了,队列的大小就是没饭吃的学生数目
图解举例:
📑代码示例:
class Solution {
public int countStudents(int[] students, int[] sandwiches) {
//对学生数组和三明治数组进行判断是否为空
if (students == null || students.length == 0) return 0;
if (sandwiches == null || sandwiches.length == 0) return students.length;
//将同学们对三明治的喜好放进一个队列当中
Queue<Integer> queue1 = new LinkedList<>();
for (int i = 0; i < students.length; i++) {
queue1.add(students[i]);
}
int j = 0;
while (!queue1.isEmpty() && j < sandwiches.length) {
int i = 0;
for (; i < queue1.size(); i++) {
//学生队头和三明治数组的 j 下标的元素不同,就将其从队头送走,送到队尾去
if (queue1.peek() != sandwiches[j]) {
queue1.add(queue1.poll());
}else {
//喜好匹配成功
break;
}
}
//队列所有成员都轮了一遍,此时队列的大小就是没饭吃的学生数目
if (i == queue1.size()) {
return queue1.size();
}
//喜好匹配成功,就将队头的学生喜好送走,三明治数组的 j 下标向后走一格
queue1.poll();
j++;
}
//返回队列的大小
return queue1.size();
}
}
1.3 自我实现
由于队列先进先出的特点,如果要用普通的一维数组来实现队列,当想要在队尾插入元素时,时间复杂度为O(1),但是当在队头删除元素的时候,由于下标为0的元素被移走了,就意味着后面的元素都要集体向前移动一格,时间复杂度为O(N)
因此采取链式存储结构
比采取顺序存储结构更为有利,这里我们将使用链表来实现队列,可以使用双向链表,也可以使用单链表,在这里我们就将使用单链表来实现一个队列,定义一个 front 节点来记录链表的头即队头,定义一个 rear 节点来记录链表的尾即队尾
链式队列的操作实际上就是单链表的插入和删除的一种特殊情形,入队操作相当于从链尾插入一个数据元素,出队操作相当于从链头删除一个数据元素,入队和出队操作均可以通过修改 front 节点和 rear 节点的指向来实现。
实现图例:
实现方法:
public void offer(int val)
在链队列中插入新的队尾元素 val。
步骤一:生成一个元素值为 val 的新节点 newNode
步骤二:判断链队列是否为空,若为空使头结点 front 指向新节点,若不为空,则需要使尾节点 rear 的引用域指向新节点
步骤三:使尾节点指向新节点
步骤四:链队列的元素个数 usedSize 加一
public int poll()
若链队列为空,抛出一个"队列为空"的异常;若不为空,就从队列中取出队头元素并返回
步骤一:判断是否队列为空,为空抛异常
步骤二:定义变量 ret 来记录此时队头元素数据域的值
步骤三:若此时只有一个节点,就将头结点和尾节点都设置为 null,若不止一个节点,就使头结点 front 指向其指向的下一个元素,即删除了队头的节点
步骤四:链队列的元素个数 usedSize 减一
步骤五:返回 ret
public int peek()
若链队列为空,抛出一个"队列为空"的异常;若不为空,就从队列中返回队头元素
步骤一:判断是否队列为空,为空抛异常
步骤二:返回头结点 front 此时指向的队头元素数据域的值
public boolean isEmpty()
判断链队列是否为空,为空时 usedSize 为 0
public int size()
返回链队列的长度
📑代码示例:
class Node {
public int data;
public Node next;
public Node(int data) {
this.data = data;
}
}
public class MyQueueLinked {
//未初始化时,默认front 和 rear 都指向 null,usedSize 为 0
private Node front;
private Node rear;
private int usedSize;
//入队列
public void offer(int val) {
Node newNode = new Node(val);
if (isEmpty()){
this.front = newNode;
}else {
this.rear.next = newNode;
}
this.rear = newNode;
this.usedSize++;
}
//出队列
public int poll() throws RuntimeException{
//判断队列是否为空
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
int ret = this.front.data;
//判断是否为一个节点
if (this.front.next == null) {
this.front = null;
this.rear = null;
}else {
this.front = this.front.next;
}
this.usedSize--;
return ret;
}
//取队头元素
public int peek() throws RuntimeException{
//判断队列是否为空
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return this.front.data;
}
//判断链队列是否为空
public boolean isEmpty() {
return this.usedSize == 0;
}
//返回链队列的长度
public int size() {
return this.usedSize;
}
}
1.4 双端队列(deque)
顾名思义就是指允许两端
都可以进行入队和出队操作的队列
,那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
事实上,经过上面的队列的实现,双端队列的实现方法基本差不多,为方便起见,此处采用双向链表
的思想来实现双端队列,在此就话不多说,上代码!
📑代码示例:
class Node {
public int data;
public Node next;
public Node prev;
public Node(int data) {
this.data = data;
}
}
class MyDeque {
public Node head;//记录队列的队头
public Node rear;//记录队列的队尾
public int usedSized;//队列中的元素个数
public int capacity;//队列的容量大小
//初始化队列的容量大小
public MyDeque(int k) {
this.capacity = k;
}
//将一个元素添加到双端队列头部,如果操作成功返回 true
public boolean insertFront(int value) {
Node newNode = new Node(value);
if (isFull()) {
return false;
}
if (!isEmpty()) {
newNode.next = this.head;
this.head.prev = newNode;
}else {
this.rear = newNode;
}
this.head = newNode;
this.usedSized++;
return true;
}
//将一个元素添加到双端队列尾部,如果操作成功返回 true
public boolean insertLast(int value) {
Node newNode = new Node(value);
if (isFull()) {
return false;
}
if (!isEmpty()) {
this.rear.next = newNode;
newNode.prev = this.rear;
}else {
this.head = newNode;
}
this.rear = newNode;
this.usedSized++;
return true;
}
//从双端队列头部删除一个元素,如果操作成功返回 true
public boolean deleteFront() {
if (isEmpty()) {
return false;
}
//如果双端队列中只有一个节点
if (this.head == this.rear) {
this.head = null;
this.rear = null;
}else {
this.head = this.head.next;
}
this.usedSized--;
return true;
}
//从双端队列尾部删除一个元素,如果操作成功返回 true
public boolean deleteLast() {
if (isEmpty()) {
return false;
}
//如果双端队列中只有一个节点
if (this.head == this.rear) {
this.head = null;
this.rear = null;
}else {
this.rear = this.rear.prev;
}
this.usedSized--;
return true;
}
//从双端队列头部获得一个元素,如果双端队列为空,返回 -1
public int getFront() {
if (isEmpty()) return -1;
return this.head.data;
}
//从双端队列尾部获得一个元素,如果双端队列为空,返回 -1
public int getRear() {
if (isEmpty()) return -1;
return this.rear.data;
}
//检查双端队列是否为空,标准为双端队列中的数据元素个数等于0
public boolean isEmpty() {
return this.usedSized == 0;
}
//检查双端队列是否满了,标准为双端队列中的数据元素个数等于队列容量
public boolean isFull() {
return this.usedSized == this.capacity;
}
}
三、Java中的栈和队列
Java 中可以直接使用栈和队列,具体方法如下
3.1 Java中的栈
方法 | 作用 |
---|---|
public E push(E item) | 入栈 |
public synchronized E peek() | 出栈 |
public synchronized E pop() | 查看栈顶元素 |
public boolean empty() | 判断栈是否为空 |
📑代码示例:
import java.util.Stack;
public class TestDemo {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(2);
stack.push(4);
System.out.println(stack.peek());//4
System.out.println(stack.pop());//4
System.out.println(stack.empty());//false
}
}
3.2 Java 中的队列
作用 | 方式一 | 方式二 |
---|---|---|
入队列 | boolean add(E e) | boolean offer(E e) |
出队列 | E remove() | E poll() |
查看队首元素 | E element() | E peek() |
📑代码示例:
以上是关于Java距离我上次知道栈和队列有多简单还是在上次的主要内容,如果未能解决你的问题,请参考以下文章