目录
一.队列(Queue)
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出 FIFO(First In First Out) 后进后出的特点
进行插入操作的一端称为队尾
出队列,进行删除操作的一端称为队头
二.队列的使用
在Java中,Queue是个接口,底层是通过链表实现的。
如图,队列中主要有这些方法
其实,1和2中方法的效果几乎差不多,但是我们经常使用的是方框2中的方法,某种程度上,2中的几个方法更好
方法 | 功能 |
boolean offer(E e) | 入队列 |
E poll() | 出队列 |
peek() | 获取队头元素,但不删除元素 |
int size() | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue.poll());//从队头出队列,并将删除的元素返回
Integer val = queue.peek();//获取队头元素,但不删除
System.out.println(val);
if (queue.isEmpty()) {
System.out.println("队列空");
} else {
System.out.println(queue.size());
}
}
三.队列的模拟实现(单链表实现)
首先,我们提出一个疑问:入队采用头插法还是尾插法呢?
如果入队采用的是头插法:入队的时候就是从头入:头插法【O(1)】
出队的时候:删除尾节点【O(n)】
如果入队采用的是尾插法:入队的时候就是从尾入:尾插法【O(n)】
出队的时候:删除头节点【O(1)】
可当我们给单向链表加一个last属性以标记尾节点时,采用尾插法入队的入队和出队的时间复杂度都是O(1),而采用头插法入队的入队是O(1),出队仍是O(n) 【这是因为要删除尾节点,就得找到尾节点的前一个】
所以,我们如果要用单链表,采用尾插法入队更好,即尾巴入队,从头出队的方法
但如果是双向链表,不管从哪边入队,时间复杂度都是O(1)
下面我们来看一下用单链表模拟实现队列的代码吧
public class MyLinkedList {
class Node{
public int val;
public Node next;
public Node(int val){
this.val = val;
}
public Node head;
public Node last;
public int UsedSize;
/**
* 入队
* @param val
*/
public void offer(int val){
Node node = new Node(val);
if(this.head == null){
head = node;
last = node;
}else{
last.next = node;
last = node;
}
this.UsedSize++;
}
/**
* 出队(从头出)
* @return
*/
public int poll(){
if(isEmpty()){
throw new RuntimeException("队列为空!");
}else{
int val = this.head.val;
head = head.next;
//处理只有一个节点的情况
if(this.head == null){
last = null;
}
this.UsedSize--;
return val;
}
}
/**
* 出队但是不删除
* @return
*/
public int peek(){
if(isEmpty()){
throw new RuntimeException("队列为空!");
}
return this.head.val;
}
public int size(){
return this.UsedSize;
}
public boolean isEmpty(){
return UsedSize==0 ;
}
}
}
四.循环队列
实际使用中,我们有时还会使用一种特殊的队列——循环队列
循环队列通常用数组实现
我们分析一下如何实现
数组下标循环的小技巧
1. 下标最后再往后(offset 小于 array.length): index = (index + offset) % array.length
2. 下标最前再往前(offset 小于 array.length): index = (index + array.length - offset) % array.length
如何区分空与满
- 通过添加 size 属性记录(UsedSize)
- 使用标记
- 保留一个位置(空一个位置)
例题
设计循环队列:具体的解析都写在注释里啦~
class MyCircularQueue {
private int[] elem;
private int front;//表示队头下标
private int rear;//表示队尾下标
/**
* 构造方法
* @param k k个大小
*/
public MyCircularQueue(int k) {
elem = new int[k+1];//由于我们需要浪费一个空间来确定是否为满,所以k要加 1
//如果不加 1 ,就用UsedSize==length来判断满
}
/**
* 入队
* 1.判断队列是否是满的
* 2.不满,则把当前需要的元素放到 rear下标的地方
* @param value
* @return
*/
public boolean enQueue(int value) {
if(isFull()) {
return false;
}
this.elem[rear] = value;
//this.rear++; 这是错误的,因为是一个循环队列,要考虑大小,直接加可能会导致数组越界
this.rear = (rear+1) % elem.length;
return true;
}
/**
* 出队
* 1.判断队列是否为空的
* @return
*/
public boolean deQueue() {
if(isEmpty()) {
return false;
}
this.front = (this.front+1) % this.elem.length;//不能直接加 1
return true;
}
/**
* 得到队头元素
* @return
*/
public int Front() {
if(isEmpty()) {
return -1;
}
return elem[front];
}
/**
* 得到队尾元素
* @return
*/
public int Rear() {
if(isEmpty()) {
return -1;
}
/*
return elem[rear];
这样写是错误的,由于(0-1)=-1,会越界,我们应该考虑 rear=0的情况
*/
int index = (this.rear==0) ? (this.elem.length-1) : (this.rear-1);
return elem[index];
}
/**
* 判断当前队列是否为空
* rear和front相遇就是空的
* @return
*/
public boolean isEmpty() {
return rear == front;//rear和front相遇就是空的
}
/**
* 判断当前队列是否为满
* 浪费一个空间来表示满
* @return
*/
public boolean isFull() {
if( (rear+1)%elem.length == front ){
return true;
}
return false;
}
}
五.双端队列(了解)
下面我们再来简单的介绍一下双端队列(Deque)
双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。
那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
Deque是一个接口,使用时必须创建LinkedList的对象。
以上是一些队列的有关知识