一、队列的实现
1.队列的介绍
2.队列的相关操作
JAVA语言中对队列这一数据结构有专门的一个接口,这个接口的名字就是Queue,而实现他的类其中有一个就是LinkedList,所以想实例化一个Queue对象可以像这样:
import java.util.LinkedList;
import java.util.Queue;
public class Test {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
//Queue<Integer> queue = new Queue<>();向这种实例化方式是错误的方式.
//Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。
queue.offer(1);
}
}
当然对象的实例化可以有很多种形式,因为有很多类实现了Queue这个接口,例如:PriorityQueue,ConcurrentLinkedQueue等这里只是先介绍一个比较简单的方式。
在Queue接口里有很多方法,以下就是队列的常用的操作(方法):
方法 | 功能 |
boolean offer(E e) | 入队列 |
E poll() | 出队列 |
E peek() | 获取对头元素 |
int size() | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
3.队列的模拟实现
这里我们就以双向链表的方式来模拟实现一个链式队列,同时实现上述常用的方法,为了方便理解,我使用整形数据来进行演示讲解。
//自定义异常,用来提示队列为空时所进行的错误操作
public class EmptyException extends RuntimeException{
public EmptyException(String msg) {
super(msg);
}
}
public class MyLinkQueue {
//创建静态内部类,实例对象作为队列中的节点
public static class Node{
int data;//存放数据
Node next;//存放下一个节点信息
Node per;//存放前一个节点信息
//构造方法
public Node(int data) {
this.data = data;
}
}
//头结点,指向队列中第一个节点
private Node front;
//尾节点,指向队列最后一个节点
private Node rear;
//记录队列中节点个数
private int useSize;
//为了体现队列的先进先出特点,规定从尾入,从头出(也可以头进尾出)
//插入操作,原理为双链表的尾插法
boolean offer(int val){
Node node = new Node(val);
if (isEmpty()){
front = node;
rear = node;
}else{
rear.next = node;
node.per = rear;
rear = node;
}
useSize++;
return true;
}
//将队头元素出队列
int poll(){
if (isEmpty()){
//队列为空,抛异常,提示不能对空队列进行出队操作
throw new EmptyException("队列为空,操作错误!!");
}
//用ret记录返回的队头元素的数据
int ret = front.data;
if (front.next == null){
front = null;
rear = null;
useSize--;
return ret;
}
front = front.next;
front.per = null;
useSize--;
return ret;
}
//获取队头元素的值,不出队列
int peek(){
if (isEmpty()){
return -1;
}
return front.data;
}
//获取队列的长度
int size(){
return useSize;
}
//判断队列是否为空
boolean isEmpty(){
//当队列的头结点为null时,说明队列为空
return front==null;
}
}
二、队列的应用
例题1:用队列实现栈
1.问题介绍
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
- 每次调用
pop
和top
都保证栈不为空
2.解题思路
我们定义两个队列,进行入队操作时,如果两个都是空队列那么往哪个队列里放都可以,如果有一个队列不为空要往不为空的队列里放元素,我们要保证有一个队列为空,因为栈是一个先进后出的数据结构,所以进行出栈操作就要将不为空的队列里的元素转移到空队列里,转移到剩最后一个元素为止,此时最后一个元素就是要出栈的元素,当两个队列都为空也就说明此时的栈为空。
3.代码实现
import java.util.LinkedList;
import java.util.Queue;
class MyStack {
//建立两个队列,用来模拟实现栈
Queue<Integer> queue1;
Queue<Integer> queue2;
//构造方法
public MyStack() {
//对queue1和queue2进行实例化
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
//入栈操作
public void push(int x) {
//关键逻辑就是对不为空的队列进行入队操作
//如果两个队列都为空则默认是对queue1进行入队操作
if (queue2.isEmpty()){
queue1.offer(x);
}else {
queue2.offer(x);
}
}
//出栈操作:
//关键逻辑就是将不为空的队列里除了最后一个元素,全部转移到另一个空队列中
//剩下的最后一个元素就是要出栈的元素
public int pop() {
//判断此时queue1与queue2哪个队列是空的,然后进行相应操作
if (queue1.isEmpty()){
//当就剩一个元素,就不用进行转移了,这个元素就是要出栈的元素
while (queue2.size()!=1){
queue1.offer(queue2.poll());
}
//此时queue2里剩的就是要出栈的元素
return queue2.poll();
}else {
//当就剩一个元素,就不用进行转移了,这个元素就是要出栈的元素
while (queue1.size()!=1){
queue2.offer(queue1.poll());
}
//此时queue1里剩的就是要出栈的元素
return queue1.poll();
}
}
//获取栈顶元素:
//逻辑和出栈逻辑类似,不过我们这里用临时变量ret记录一下
//转移的最后一个元素值,记录完成还要把最后一个元素进行转移
public int top() {
//此时queue1里剩的就是要出栈的元素
if (queue1.isEmpty()){
while (queue2.size()!=1){
queue1.offer(queue2.poll());
}
//记录最后一个元素值
int ret = queue2.peek();
//将最后一个元素进行转移
queue1.offer(queue2.poll());
//此时ret就是栈顶元素
return ret;
}else {
while (queue1.size()!=1){
queue2.offer(queue1.poll());
}
//记录最后一个元素值
int ret = queue1.peek();
//将最后一个元素进行转移
queue2.offer(queue1.poll());
//此时ret就是栈顶元素
return ret;
}
}
//对栈进行判空操作
public boolean empty() {
//当queue1与queue2都为空,说明模拟实现的栈为空
return queue1.isEmpty() && queue2.isEmpty();
}
}
例题2:用栈实现队列
1.问题介绍
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
)。
- 假设所有操作都是有效的 (例如,一个空的队列不会调用
pop
或者peek
操作)
2.解题思路
定义两个栈,每次入队的元素存放到栈stackIn中,每次出队时都从栈stackOut中出,因为队列是一种先进先出的数据结构,所以stackOut里的元素是stackIn里的元素转移进来的,stackIn出栈的顺序是入队顺序的逆序,所以将stackIn里元素转移到stackOut中后再由stackOut出栈得到的就是正常出队列顺序,这里有个前提,只有当stackOut中没有元素才能进行元素转移,当stackOut和stackIn均为空时,模拟的队列也为空。
3.代码实现
import java.util.Stack;
class MyQueue {
//建立两个栈
//入队元素进入stackIn
Stack<Integer> stackIn;
//出队元素从stackOut出
Stack<Integer> stackOut;
//构造方法
public MyQueue() {
//对stackIn和stackOut进行实例化
stackIn = new Stack<>();
stackOut = new Stack<>();
}
//入队列操作
public void push(int x) {
//直接进入stackIn中
stackIn.push(x);
}
//出队列操作
public int pop() {
//判断一下stackOut是否为空,
//如果为空就将stackIn中所有元素转移到stackOut中
if (stackOut.empty()){
while (!stackIn.empty()){
stackOut.push(stackIn.pop());
}
}
//此时stackOut的栈顶元素就是要出队的元素
return stackOut.pop();
}
//获取队列第一个元素
public int peek() {
//判断一下stackOut是否为空,
//如果为空就将stackIn中所有元素转移到stackOut中
if (stackOut.empty()){
while (!stackIn.empty()){
stackOut.push(stackIn.pop());
}
}
//此时stackOut的栈顶元素就是要获取的元素
return stackOut.peek();
}
//队列判空操作
public boolean empty() {
//当stackIn和stackOut均为空,说明模拟队列为空
return stackIn.empty() && stackOut.empty();
}
}
例题3:设计循环队列
1.问题介绍
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k)
: 构造器,设置队列长度为 k 。Front
: 从队首获取元素。如果队列为空,返回 -1 。Rear
: 获取队尾元素。如果队列为空,返回 -1 。enQueue(value)
: 向循环队列插入一个元素。如果成功插入则返回真。deQueue()
: 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty()
: 检查循环队列是否为空。isFull()
: 检查循环队列是否已满。
2.解题思路
首先我来给大家介绍一下循环队列空与满的状态:
- front指向循环队列的头部
- rear指向循环队列的尾部
- length表示模拟实现循环队列数组的长度
这里循环队列为满有三种判断条件:使用useSize记录:当useSize与模拟实现循环队列的这个数组长度相同时,队列为满;
- 浪费一个空间:即rear指向的位置永远为空,当(rear+1)%length==front时队列为满;
- 使用标记:第一次rear与front相遇时队列为空标记一下,下次再相遇就是队列为满。
这里我们需要注意,在进行获取队尾元素,向循环队列插入一个元素,从循环队列中删除一个元素,这几个操作时都要注意rear与front的变化不是简单的+1和-1,一定要用取余操作,防止下标越界,下面具体逻辑放在代码注释中进行讲解。
3.代码实现
class MyCircularQueue {
//用来模拟实现循环队列的数组
private int[] elem;
//记录队头下标
private int front;
//记录队尾下标
private int rear;
//构造方法,构造长度为k的循环队列
public MyCircularQueue(int k) {
//因为用浪费一个空间的方法判断循环队列为满,
//所以创建数组长度要是循环队列长度+1
elem = new int[k+1];
}
//向循环队列插入一个元素
public boolean enQueue(int value) {
//如果循环队列为满,插入失败
if (isFull()){
return false;
}else {
//rear位置插入元素
elem[rear] = value;
//rear向后移动,防止下标越界,用取余操作
rear = (rear + 1) % elem.length;
return true;
}
}
//从循环队列中删除一个元素
public boolean deQueue() {
//如果循环队列为空,删除失败
if (isEmpty()){
return false;
}else {
//front向后移动,代表出队一个元素
//防止下标越界,用取余操作
front = (front +1) % elem.length;
return true;
}
}
//从队首获取元素
public int Front() {
//如果循环队列为空,返回-1,代表失败
if (isEmpty()){
return -1;
}
//返回front位置的元素
return elem[front];
}
//获取队尾元素
public int Rear() {
//如果循环队列为空,返回-1,代表失败
if (isEmpty()){
return -1;
}
//rear记录的是队尾元素的下一个位置
//所以返回rear前一个位置的元素
//防止下标越界,用取余操作
return elem[(rear+elem.length-1)%elem.length];
}
public boolean isEmpty() {
//当队头与队尾相遇,代表循环队列为空
return rear == front;
}
public boolean isFull() {
//当队尾下一个是队头,说明队列为满
//获取rear下一个位置下标,防止下标越界,用取余操作
return (rear+1)%elem.length == front;
}
}
三、例题链接
文章到此就要结束了,本篇文章以Java中基础的队列实现类来进行讲解与演示了队列的相关操作还有例题的解决,Java中还有很多其他队列,比如优先级队列,双端队列,阻塞队列等比较常用,这里我们主要讲解队列这一数据结构的特点,所以没有引入其他较复杂的队列,在后续的文章中会对其他队列再进行介绍,那么今天的分享也就到此结束了,谢谢友友们的观看,喜欢本篇文章的希望能给个三连(点赞,关注+收藏),你的支持就是我最大的动力,Thanks♪(・ω・)ノ~~