后缀表达式也就是我们常说的逆波兰表达式
我们先通过一道练习题再来说明什么是前缀,什么是中缀,什么是后缀
练习题1
这道题目的做法如下:
为了方便区分括号,我们把括号都换成不同的颜色:
1:首先对已有的括号进行换色:
2:然后依照从左到右,先乘除,后加减的原则来对运算加上括号
:
3:然后将每个运算符移动到自己所对应的括号的外面
:
4:最后将所有括号去掉,得到的就是我们的后缀表达式
所以这道题目最终答案为A.
总结
一般的题目就是考察中缀转后缀
,中缀表达式一般题目中会直接给出的
前缀表达式
就是在我们加完括号移动的过程中将运算符移动到自己对应的括号的前面
后缀表达式
是在我们加完括号后移动的过程中将运算符移动到自己对应的括号的后面
前缀表达式和后缀表达式都是没有括号的表达式
计算机一般对于计算前缀和后缀表达式比较容易
为什么这么说呢?我们来举个例子
对于题目中的中缀表达式来说我们把它换成数字的形式,如下图所示:
转换成后缀表达式的形式为:
那么计算机是如何对这个后缀表达式进行运算的呢?来看:
1:首先初始化一个空栈,当遇到数字的时候就将数字压栈,如下所示
2:这时候遇到第一个符号是+号,
此时把栈顶元素作为右操作数,把栈顶元素的下一个元素作为左操作数
,那么此处的式子就变成了1+2=3
3:然后将3这个元素压栈:如下所示:
4:此时又碰到了减号,那就将3作为右操作数,4作为左操作数,得到4-3=1.,然后将1压栈,如下所示:
5:此时又碰到了乘号,那就将1作为右操作数,2作为左操作数,得到2*1=2.,然后将2压栈,如下所示:
6:此时碰到了数字2,就将其压栈即可:
7:此时又碰到了除号,那就将2作为右操作数,下一个2作为左操作数,得到2/2=1.,然后将1压栈,如下所示:
8:最后碰到了加号,那就将1作为右操作数,下一个1作为左操作数,得到1+1=2,而这个2与我们中缀表达式算出来的结果一摸一样,这也是计算机在处理中缀表达式时的时候会先将其转变为后缀表达式,然后将其按照上述方式进行运算,最终得出结果
练习题2
答案:
栈的底层其实就是一个数组:来看代码实现:
public class MyStack {
//栈的底层其实也是数组
private int[] elem;
// top可以代表下标,表达的意思是当前栈中可以插入的元素的位置的下标
// top还可以代表当前栈中元素的个数
private int top;
public MyStack() {
this.elem = new int[10];
}
//判断栈是否满
public boolean isFull() {
return this.top == this.elem.length;
}
//将元素压栈
public int push(int item) {
if (isFull()) {
throw new RuntimeException(“栈已经满了”);
}
this.elem[this.top] = item;
this.top++;
//注意压栈的时候的下标为this.top-1
return this.elem[this.top - 1];
}
/**
-
弹出栈顶元素 并且删除
-
@return
*/
public int pop() {
if (isEmpty()) {
throw new RuntimeException(“栈是空的,不能进行删除”);
}
this.top–;
//注意此处删除元素的时候下标应该是this.top
//这个下标相当于下次插入的时候将元素进行替换
return this.elem[this.top];
}
/**
-
拿到栈顶元素不删除
-
@return
*/
public int peek() {
if (isEmpty()) {
throw new RuntimeException(“栈为空”);
}
return this.elem[this.top - 1];
}
//判断栈是否为空
public boolean isEmpty() {
return this.top == 0;
}
//得到栈的长度
public int size() {
return this.top;
}
}
测试类:
public class TestDemo {
public static void main(String[] args) {
MyStack stack = new MyStack();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(1);
stack.push(2);
stack.push(3);
//stack.peek() 拿到栈顶元素,但是不是删除
System.out.println(stack.peek());//3
//弹出栈顶元素 3
System.out.println(stack.pop());//3
System.out.println(stack.peek());//2
System.out.println(stack.pop());//2
System.out.println(stack.pop());//1
System.out.println(stack.empty());
System.out.println(stack.pop());//3
}
}
如果此时用链表实现栈,那么入栈用头插法好还是尾插法好呢?
答:头插法好,不管是出栈还是入栈时间复杂度都会达到O(1).
如果使用尾插法的话,此时我们需要找到链表的尾节点,就需要我们去遍历整个链表,那么不管是出栈还是入栈时间复杂度都会达到O(N),
=================================================================
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出
FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾
(Tail/Rear) 出队列:进行删除操作的一端称为队头
(Head/Front)
经过后面的学习我们就会知道,上述队列只是一个普通队列
.
1:普通队列:也就是只允许我们在一端进行插入数据操作的队列.
2:优先级队列:也就是我们的堆
3:双端队列:双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
4:循环队列:后面会详细介绍
可以看到实体类有两个
一个是LinkedList:LinkedList
既可当作双端队列
,因为它实现了Deque接口
,也可以当作普通队列
,因为其也实现Queue接口
,LinkedList的底层其实就是我们学过的双向链表
,其具备了链表的特性
一个是PriorityQueue:这个是优先级队列
,因为它实现了Queue接口,优先级队列的底层其实是二叉树
,但是其具备队列的特性.
Queue接口常用方法(普通队列)
add方法
public class TestDemo5 {
public static void main(String[] args) {
//此时是一个普通队列
Queue queue = new LinkedList<>();
//此时调用add方法,默认是尾部入队.
queue.add(1);
//输出普通队列的长度,为1
System.out.println(queue.size());
}
}
此时我们调用普通队列中的add方法,默认是尾部入队列
此时 Queue接口发生了向上转型,我们的LinkedList类中重写了Queue接口中的add方法,所以在调用add方法的时候发生了运行时绑定,来看源码:
这个是LinkedList类中的add方法,可以看到其内部还调用了一个方法叫做linkLast方法,这个方法就是尾插法,如下图所示:
所以普通队列中的add方法其实默认就是尾插法.
offer方法
同样也是实现我们普通队列插入的一个方法,其效果与add方法是一样的,但是与add方法的区别是add方法当队列满了再插入的时候会抛出异常,而offer方法只返回true或者false,所以一般我们建议用offer方法
remove方法
删除普通队列的队头元素
poll方法
删除普通队列的队头元素,效果与remove方法相同,我们一般还是建议使用poll方法
public class TestDemo5 {
public static void main(String[] args) {
//此时是一个普通队列
Queue queue = new LinkedList<>();
//此时调用add方法,默认是尾部入队.
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
//此处的输出值为1,代表所删除的队头元素
System.out.println(queue.poll());
//此处的输出值为【2,3,4】
System.out.println(queue);
}
}
element方法
element方法是获取队列的头元素,但不删除
peek方法
peek方法是获取队列的头元素,但不删除,与element方法的效果相同.
public class TestDemo5 {
public static void main(String[] args) {
//此时是一个普通队列
Queue queue = new LinkedList<>();
//此时调用add方法,默认是尾部入队.
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
//此处的输出值为1
System.out.println(queue.peek());
//此处的输出值为【1,2,3,4】
System.out.println(queue);
}
}
Deque接口常用方法(双端队列)
以下方法只是Deque接口中非常常用的一些方法,当然Deque接口总还有很多其他常用的方法,如果大家想要查看的话可以直接打开我们Deque接口的源码,Alt+7
之后便可以查看里面的其他实现方法.
代码示例:
public class TestDemo5 {
public static void main(String[] args) {
Deque deque = new LinkedList<>();
//从双端队列的队头插入元素
deque.addFirst(1);
//从双端队列的队尾插入元素
deque.addLast(2);
//输出结果为:[1, 2]
System.out.println(deque);
}
}
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些
,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低,时间复杂度会达到O(N)
那么在这里我们就拿单链表
实现一个普通队列
:来看代码:
因为是单链表,所以其方向是固定的,又因为我们在插入元素的时候的方法是尾插法,所以队尾和队头如下所示:
来看我们的实现代码:
class Node {
public int val;
public Node next;
public Node(int val) {
this.val = val;
}
}
public class MyQueue {
//定义队列头
public Node first;
//定义队列尾
public Node last;
//判断列表是否为空
public boolean isEmpty() {
if (this.first == null && this.last == null) {
return true;
}
return false;
}
//实现队列中的offer方法,在其源码中默认是尾插法
public boolean offer(int val) {
Node node = new Node(val);
//假如此时队列中还没有元素
if (this.first == null) {
this.first = node;
this.last = node;
} else {
//假设队列中已经插入了元素
this.last.next = node;
this.last = node;
}
return true;
}
/**
-
得到队头元素且删除
-
@return
-
@throws RuntimeException
*/
public int poll() throws RuntimeException {
if (isEmpty()) {
throw new RuntimeException(“队列为空”);
}
int num = this.first.val;
this.first = this.first.next;
return num;
}
/**
-
得到队列的头元素但是不删除
-
@return
*/
public int peek() {
if (isEmpty()) {
throw new RuntimeException(“队列为空”);
}
return this.first.val;
}
public static void main(String[] args) {
}
}
对于上述代码在进行入队操作(尾插法)
时候的时间复杂度为O(1)
在进行出队(头删法)
时候的时间复杂度为O(1)
.
假设此时我们入队操作采用头插法,时间复杂度为O(1),但是当出队操作采用尾删法,时间复杂度就成了O(N).因为需要遍历链表查找到链表的最后一个元素.
所以当遇见这种情况的时候,我们都是采用双向链表,不用单向链表.
非常麻烦,出队的时候的找到队头在哪里,入队的时候还需要往空位置插,因为我们普通队列的插入是尾插法,如果此时数组后面几个元素插满了,再进行尾插法会发生数组下标越界异常:例如下所示,此时再往7下标后面插入会发生数组下标越界异常,所以只能往0到3号下标处进行插入.
此时我们小伙伴们就想啦,假如这个队列能够首尾相接,循环起来,是不是就能够实现插入了呢
?小伙伴们的想法非常的好,的确是这样,由此就引入了我们的循环队列
的概念,而循环队列底层也就是一个循环数组
.相当于当我们实现队列的尾插法的时候,本来应该插在8号下标处的元素插在了0号下标处.所以说用数组是可以实现队列的,下面我们来看具体实现.
什么是循环队列?我们先来给出一个示意图:
此时我们相当于将数组首尾相连形成了一个循环队列,0-7代表数组下标,此时定义两个整形
指针,一个是指向队列头元素的头指针front
,一个是指向队尾元素的尾指针rear,
此时因为队列为空,所以此时front指针和rear指针都指向了0下标的位置.
然后我们给出一组想要插入的数据:
12,23,34,45,56,67,78,89
当我们挨个向队列中插入数据的时候,front指针不动,rear指针开始向后移动,rear指针所指向的位置就代表当前可以插入元素的位置,所以rear指针同样代表当前可以存放数组元素的下标
,假如我们要删掉队列中的元素,只需要front++即可
,这样队列中的元素在遍历的时候就会被删除掉.
注意事项(结论1)
当在删除循环队列中的元素的时候,我们的rear指针其实是不动的,只有front指针一直在动,所以最终front指针与rear指针终会相遇,
这时我们的循环队列为空`,所以总结出一个结论,当front和rear相遇的时候,队列为空,如下所示:此时front指针遍历过的元素:12,23,34,45全部都出队列了,此时两个指针相遇代表队列为空
欧克现在回到我们向队列中插入元素的时候,此时front指针回到我们0下标的位置,然后继续开始插入我们的元素,rear指针继续向下遍历,直到rear指针与我们front指针的第二次重合
:
此时有些同学就开始纳闷了:首先当rear走到7号下标的时候,它怎么能再走到front所在的0号下标呢,难道不会发生数组下标越界异常吗?其次就算走到了,之前我们总结到说当front和rear相遇的时候,队列为空,但此时队列竟然满了?这不是悖论吗?
如下所示:
所以接下来当我们将要重点解决同学们的两个疑惑:
问题1:front和rear相遇,此时到底队列为空,还是队列为满?
问题2:front和rear当都走到7号下标的时候,都会面临一个问题:
front+1此时数组下标越界了,rear+1此时数组下标也越界了
那么首先我们来解决问题1:
假设此时还剩最后一个元素89未插入到队列当中,我们在这里需要牺牲一个空间,来判断当前队列是否是满的,也就是判断rear的下一个是否是front
所以当front和rear相遇的时候,队列为空结论正确
.
那么接下来我们来解决第二个问题:
rear此时的下标为7,front此时的下标为0,rear+1=7+1=8,8是永远不可能等于0的,所以此时我们需要用到这个表达式来判断当前队列是否是满的:(rear+1)%len==front,例如(7+1)%8=0,说明此时队列满了
注意事项(结论2)
判断循环队列是否满就使用(rear+1)%len==front
这个式子来判断.适用于所有情况
.
自己实现一个循环队列(数组实现)
经过前面的分析我们可以知道的是,我们的循环队列的底层其实就是一个数组,代码实现如下:
public class MyCircularQueue {
private int front;
//rear表示当前可以插入元素的数组的下标
private int rear;
private int[] elem;
public MyCircularQueue(int k) {
this.elem = new int[k];
}
/**
-
向循环队列插入一个元素。如果成功插入则返回真。
-
@param value
-
@return
*/
public boolean enQueue(int value) {
//此时代表队列已满,无法再进行插入
if (isFull()) {
return false;
}
//放到数组的rear下标
this.elem[this.rear] = value;
//rear指针往后走,取余是为了循环起来
this.rear = (this.rear + 1) % this.elem.length;
return true;
}
public boolean deQueue() {
if (isEmpty()) {
return false;
}
//只需要挪动front这个下标就好了,取余是为了循环起来
this.front = (this.front + 1) % this.elem.length;
return true;
}
/**
-
得到队头元素
-
@return
*/
public int Front() {
if (isEmpty()) {
return -1;
}
return this.elem[this.front];
}
/**
-
得到队尾元素
-
注意特殊情况
-
@return
*/
public int Rear() {
if (isEmpty()) {
return -1;
}
int index = -1;
if (this.rear == 0) {
index = this.elem.length - 1;
} else {
index = this.rear - 1;
}
return this.elem[index];
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/7a301ef73f9451c8d9588240a8a170e0.jpeg)
最后
如果觉得本文对你有帮助的话,不妨给我点个赞,关注一下吧!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
his.elem.length - 1;
} else {
index = this.rear - 1;
}
return this.elem[index];
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-TEkc3SI6-1713437046923)]
[外链图片转存中…(img-w2P8NtGW-1713437046923)]
[外链图片转存中…(img-0Rd86tyw-1713437046924)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/7a301ef73f9451c8d9588240a8a170e0.jpeg)
最后
如果觉得本文对你有帮助的话,不妨给我点个赞,关注一下吧!
[外链图片转存中…(img-aAnJAEAk-1713437046924)]
[外链图片转存中…(img-w4kEp5dG-1713437046924)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!