目录
1.什么是队列
2.队列的使用
3.队列的模拟实现
4.部分队列oj题目解析
在队列这种数据结构中,最先插入在元素将是最先被删除;反之最后插入的元素将最后被删除,因此队列又称为“先进先出”(FIFO—first in first out)的线性表。
这里放出队列常用方法,具体使用将在后面讲解
注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。
有关队列的实现我们主要有两种做法,顺序结构和链式结构。
所谓顺序结构就是使用数组来实现队列,而链式结构则是使用链表来实现队列。
链表实现队列采取双向链表或者单向链表均可,这里我们为了方便演示采用单向链表。
但是这里我们为了实现O(1)的插入和删除操作我们需要为单链表添加一个last节点指向链表的最后一个元素。
这里我们采用从链表尾插入(push)从链表头删除(pop),为什么不能反过来呢?
因为单链表无论添不添加last节点删除操作都需要找到last的前驱,时间复杂度为O(n)
理解了基本原理之后我们就直接放代码吧,应该挺好理解的
public class MyQueueDemo {
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;
}
this.usedSize++;
}
public int poll(){
if(isEmpty()){
throw new RuntimeException("队列为空!");
}
int val=head.val;
head=head.next;
if(head==null){//处理只有一个元素的情况
last=null;
}
this.usedSize--;
return val;
}
public int size() {
return usedSize;
}
public boolean isEmpty(){
return this.usedSize==0;
}
public int peek() {
if(isEmpty()) {
throw new RuntimeException("队列为空!");
}
return head.val;
}
}
关于数组实现队列,我们一般用于实现环形队列,因为这样相对一般的队列我们减少了空间的浪费,做到了空间重复利用。
首先我们要知道要实现环形队列,我们必须要有两个指针,front指向队列的首元素,rear指向队列首元素的下一位
但是既然我们说到了他是一个循环链表,那么我们该如何做到“循环”这一点呢?
最直观的问题就是我们如何处理数组的下标,如果他超过了我们数组的长度该怎么办呢?
对于这个问题我们比较容易可以想到要对下标取模(%)
具体到代码就是:: index = (index + offset) % array.length
另一个问题就是我们如何判断队列是否为空或者满?
我们一般采用以下几种方法:
1. 通过添加 size 属性记录
2. 使用标记
3. 保留一个位置
这里我们选择第三种方法作为示例,剩余的做法大家可以自行实现
我们先实现入队方法:
/*
* 入队
* 1、判断是不是满的?
* 2、把当前需要存放的元素放到rear下标的地方。
*/
public boolean enQueue(int value){//插入
if(isFull()) {
return false;
}
this.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 Rear(){
if(isEmpty()) {
return -1;
}
return rear==0? this.elem[elem.length-1]:this.elem[rear-1];
}
这里解释一下为什么最后需要判断,假如rear!=0时,rear-1可以直接得到队尾元素的下标,但是当rear==0的时候,我们直接-1会造成数组越界,因为我们实现的是一个环形队列,所以我们这里需要特判一下
获取队头元素 :
public int Front() {
if(isEmpty()) {
return -1;
}
return elem[front];
}
判断队列是否已满:
public boolean isFull(){
if((rear+1)%elem.length==front){
return true;//当rear+1==front时队列为满,不理解可以看前面的图,记得取模
}
return false;
}
判断队列是否已空:
public boolean isEmpty() {
return rear == front;//rear==front时为空,具体可看前面的图
}
这里放上一道模拟实现环形队列的题目供大家练习:力扣
关于队列的所有代码我也放在gitee供大家参考:MyQueue: 模拟实现队列
最后和大家简要提一下链式结构与顺序结构各自的优缺点
1.链式结构相对顺序结构有着方便扩容的特点,只需要在链表尾新增节点即可,而顺序结构则不方便扩容,一旦涉及到扩容一般会用到copyOf方法,这样扩容一次对于系统的资源耗费是比较大的
2.但链式结构在新增或者删除节点时所涉及的操作相对速度比较慢,而顺序结构用数组实现只需要操作两个指针即可速度很快
这题的题目非常简单,就是让我们利用栈来实现队列。
思路:
首先我们需要先分析栈与队列的各自特点,既然栈是一个先进后出的结构,而队列是一个先进先出的结构。我们可以想到用两个队列来辅助操作。
可以使用两个队列实现栈的操作,其中queue 1用于存储栈内的元素,queue 2作为入栈操作的辅助队列。
入栈操作时,首先将元素入队到 queue 2 ,然后将queue 1的全部元素依次出队并入队到queue 2,此时 queue 2的前端的元素即为新入栈的元素,再将 queue 1和queue 2互换,则 queue 1的元素即为栈内的元素,queue 1的前端和后端分别对应栈顶和栈底。
如果以上说了那么多你不太理解的话,用通俗易懂的话来说就是“利用两个队列来回倒腾,将一个队列翻转到另一个队列后就变成了我们所需要的栈”
当然我们如果能够想到将一个队列看作环的话我们也能够只用一个队列来实现此操作,因为有一个队列只是相当于一个临时变量
AC代码:
class MyStack {
Queue queue;
public MyStack() {
queue = new LinkedList();
}
public void push(int x) {
int n = queue.size();
queue.offer(x);//这两步顺序不能反,不然会把新插入元素放到“栈底”
for (int i = 0; i < n; i++) {
queue.offer(queue.poll());//相当于翻转操作
}
}
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
有了上一题的经验,相信这题我们应该能够更快想到思路。
这题题意同样简单,就是让我们用栈来实现一个队列
思路:
首先我们先用两个栈作为辅助,每次push都直接放入第一个栈内,pop时先判断第二个栈是否为空,不为空则直接弹出栈顶元素,否则将第一个栈内的元素压入第二个栈(相当于实现了翻转操作)。
AC代码:
class MyQueue {
Stack stack1;
Stack stack2;
public MyQueue() {
stack1=new Stack<>();
stack2=new Stack<>();
}
public void push(int x) {
stack1.push(x);
}
public int pop() {
if(stack2.isEmpty()){
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
public int peek() {
if(stack2.isEmpty()){
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}