


专栏: 数据结构(Java版)
个人主页:手握风云
目录
[1.1. 队列的概念](#1.1. 队列的概念)
[1.2. 队列的使用](#1.2. 队列的使用)
[1.3. 队列的模拟实现](#1.3. 队列的模拟实现)
[1.4. 循环队列](#1.4. 循环队列)
[1.5. 设计循环队列](#1.5. 设计循环队列)
[2.1. 用队列实现栈](#2.1. 用队列实现栈)
[2.2. 用栈实现队列](#2.2. 用栈实现队列)
一、队列
1.1. 队列的概念
队列是只允许在⼀端进⾏插⼊数据操作,在另⼀端进⾏删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)⼊队列:进⾏插⼊操作的⼀端称为队尾(Tail/Rear)出队列:进⾏删除操作 的⼀端称为队头(Head/Front)。

1.2. 队列的使用

|-------------------|--------------|
| 方法 | 功能 |
| boolean offer(E) | 入队列 |
| E poll | 出队列 |
| peek | 获取队头元素 |
| int size() | 获取队列中有效元素的个数 |
| boolean isEmpty() | 检查队列是否为空 |
Queue是个接⼝,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接⼝。
java
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
System.out.println(queue.isEmpty());
queue.offer(11);//入队列
queue.offer(22);
queue.offer(33);
queue.offer(44);
queue.offer(55);
System.out.println(queue);
System.out.println(queue.size());//获取有效元素的个数
System.out.println(queue.isEmpty());//检查队列是否为空
System.out.println(queue.peek());//获取头元素
System.out.println(queue.poll());//出队列
System.out.println(queue.poll());
}
}

1.3. 队列的模拟实现
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,这里我们建议使用链表来对队列进行底层的实现。


队列要符合先进先出的原则,我们同时要求时间复杂度为O(1),我们目前所学的单链表是无法实现的。因为我们现在不知道链表的尾部,所以我们必须要去遍历链表,此时时间复杂度为O(n)。如果是从链表的尾部开始进入,时间复杂度为O(n),出时间复杂度为O(1);如果从链表的头部进入,同时需要找到链表的尾部,时间复杂度为O(n)。如果我们知道链表的尾部引用last,从尾部入队,头部出队,使时间复杂度为O(n)。所以单向链表是无法实现时间复杂度为O(1)的队列。
既然单向链表无法实现,我们就可以尝试使用双向链表。
java
public class MyQueue {
static class Node{
public int val;
public Node prev;
public Node next;
public Node(int val) {
this.val = val;
}
}
/**
* 规定队尾是链表的尾巴
* 规定队头是链表的头
*/
public Node front;//队头
public Node rear;//队尾
}

对于插入方法offer,如果队列为空的话,那么插入的第一个元素既是队头又是队尾。后进入的元素都可以使用尾插的方法来实现。
java
public void offer(int val){//入队列
Node node = new Node(val);
//第一个节点的插入
if(front == null){
front = node;
rear = node;
}else{
rear.next = node;
node.prev = rear;
rear = node;
}
}
对于获取队列的长度size(),我们在链表章节里面多次讲过,这里不再多说。
java
public int size(){
int count = 0;
Node cur = front;
while(cur != null){
cur = cur.next;
count++;
}
return count;
}
对于元素出队列的方法poll,front节点的前驱置为空,下一个节点成为对头。
java
public int poll(){//出队列
if(front == null){
return -1;
}
int val = front.val;
front = front.next;
front.prev = null;
return val;
}
但此时上面的代码还是有问题的,如果说队列只有一个节点,front直接变为空,根本不存在前驱。
java
public int poll(){//出队列
if(front == null){
return -1;
}
int val = front.val;
front = front.next;
//防止只有一个节点
if(front != null){
front.prev = null;
}
return val;
}
对于获取队头元素,可以参照上面的代码实现,直接返回front.val。
java
public int peek(){//获取队头元素
if(front == null){
return -1;
}
return front.val;
}
完整代码实现:
java
public class MyQueue {
static class Node{
public int val;
public Node prev;
public Node next;
public Node(int val) {
this.val = val;
}
}
/**
* 规定队尾是链表的尾巴
* 规定队头是链表的头
*/
public Node front;//队头
public Node rear;//队尾
public void offer(int val){//入队列
Node node = new Node(val);
//第一个节点的插入
if(front == null){
front = node;
rear = node;
}else{
rear.next = node;
node.prev = rear;
rear = node;
}
}
public int size(){
int count = 0;
Node cur = front;
while(cur != null){
cur = cur.next;
count++;
}
return count;
}
public int poll(){//出队列
if(front == null){
return -1;
}
int val = front.val;
front = front.next;
//防止只有一个节点
if(front != null){
front.prev = null;
}
return val;
}
public int peek(){//获取队头元素
if(front == null){
return -1;
}
return front.val;
}
}
public class Test {
public static void main(String[] args) {
MyQueue queue = new MyQueue();
queue.offer(11);
queue.offer(22);
queue.offer(33);
queue.offer(44);
System.out.println(queue.size());
System.out.println(queue.peek());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
}

1.4. 循环队列
上面的队列是链式的,当然也有循环队列。我们可以使用数组来实现循环队列,但是存在一些问题。如下图所示,当元素全部出队列时,如果我们再想把元素放入队列中时,那么前面的内存空间就会出现浪费。我们利用一个圆就可以把空间合理地利用起来。

这时候问题来了:rear如何从7下标走到0下标;怎么判断front和rear相遇时是空还是满。
对于第一个问题,rear=(rear+1)%len(数组的长度)。
对于第二个问题,我们有三种解决方案。第一种,我们可以定义一个size来记录存储的个数,当size等于数组长度时,就说明满了;第二种解决方案是牺牲一个空间,当存储元素的时候,判断下一个位置是否为front,如果是,就不再存储了;第三种解决方案是标记,定义一个isFull方法,初始,先假设isFull是false,每次元素入队时,检查rear和front是否重合,如果重合设置rear==front,isFull方法置为true。

1.5. 设计循环队列
我们先看isFull方法,上面已经讲到了,代码如下:
java
public boolean isFull() {
if((rear+1)% elem.length == front){
return true;
}
return false;
}
对于入队列enQueue方法,我们需要先判断队列是否满了,如果满了,直接返回false;如果没满,rear需要后移。
java
public boolean enQueue(int value) {
if(isFull()){
return false;
}
elem[rear] = value;
rear = (rear+1) % elem.length;
return true;
}
对于出队列deQueue方法,我们也需要先判断队列是否为空。如果为空,我们让front向后移动,然后返回true。
java
public boolean isEmpty() {
return front == rear;
}
java
public boolean deQueue() {
if(isEmpty()){
return false;
}
front = (front+1) % elem.length;
return true;
}
对于Front和Rear方法,如果是空,返回-1。如果不是空,Front直接返回elem[front];但Rear方法需要注意一下,如果rear在队列的头,rear-1也会返回-1,所以需要提前判断一下。
java
import java.util.Scanner;
public class MyCircularQueue{
public int[] elem;
public int front;
public int rear;
public MyCircularQueue(int k){
elem = new int[k+1];
}
public boolean enQueue(int value) {
if(isFull()){
return false;
}
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 Front() {
if(isEmpty()){
return -1;
}
return elem[front];
}
public int Rear() {
if(isEmpty()){
return -1;
}
int index = rear == 0 ? elem.length-1 : rear-1;
return elem[index];
}
public boolean isEmpty() {
return front == rear;
}
public boolean isFull() {
if((rear+1)% elem.length == front){
return true;
}
return false;
}
}
java
import java.util.Scanner;
public class Solution {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while(in.hasNextInt()){
int k = in.nextInt();
MyCircularQueue queue = new MyCircularQueue(k);
System.out.println(queue.enQueue(1));
System.out.println(queue.deQueue());
System.out.println(queue.Front());
System.out.println(queue.Rear());
System.out.println(queue.isFull());
System.out.println(queue.isEmpty());
}
}
}
二、栈和队列面试题
2.1. 用队列实现栈
我们首先思考以下,一个队列能否实现栈。队列是先进先出的,而栈是先进后出的。如下图所示,左边是队列,只能出10,而右边的栈只能出20,所以一个队列无法实现栈。

我们先定义两个队列qu1,qu2。模拟出栈,先判断哪个队列为空,另一个队列中前n-1个进入的元素进入到空队列中,最后一个元素再出队列。模拟入栈,每次入到不为空的队列当中;如果开始两个队列都为空,则默认放到qu1中。

java
import java.util.LinkedList;
import java.util.Queue;
public class MyStack {
private Queue<Integer> qu1;
private Queue<Integer> qu2;
public MyStack() {
qu1 = new LinkedList<>();
qu2 = new LinkedList<>();
}
//模拟入栈
public void push(int x) {
if(empty()){
qu1.offer(x);
return;
}
if(!qu1.isEmpty()){//qu1不为空,qu2为空
qu1.offer(x);
}else{//qu2不为空,qu1为空
qu2.offer(x);
}
}
//栈顶元素出栈
public int pop() {
if(empty()) {
return -1;
}
if(!qu1.isEmpty()) {
int size = qu1.size();
while(size-1 != 0) {
qu2.offer(qu1.poll());
size--;
}
return qu1.poll();
}else {
int size = qu2.size();
while(size-1 != 0) {
qu1.offer(qu2.poll());
size--;
}
return qu2.poll();
}
}
//返回栈顶元素
public int top() {
if(empty()){
return -1;
}
if(! qu1.isEmpty()){
int size = qu1.size();
int tmp = -1;
while(size != 0){
tmp = qu1.poll();
qu2.offer(tmp);//size前面的元素出队列,并进入另一个队列
size--;
}
return tmp;
}else{
int size = qu2.size();
int tmp = -1;
while(size-1 != 0){
tmp = qu2.poll();
qu1.offer(tmp);
size--;
}
return tmp;
}
}
//两个队列都是空,说明模拟栈是空的
public boolean empty() {
return qu1.isEmpty() && qu2.isEmpty();
}
}
java
public class Solution {
public static void main(String[] args) {
MyStack stack = new MyStack();
System.out.println(stack.empty());
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.pop());
System.out.println(stack.empty());
}
}
2.2. 用栈实现队列
同上,用栈实现队列,也得同时使用两个栈,并且只能使用标准栈操作。模拟出队列,我们先让元素进入s1中,然后全部进入s2中,再从s2中弹出元素。这样做的好处是可以只使用s2中的元素。如果s2是空的,那么看s1是不是空?s1不是空,s1中的元素全部倒出来。模拟入队列,全部默认放入第一个栈中。
java
import java.util.Stack;
public class MyQueue {
private Stack<Integer> s1;
private Stack<Integer> s2;
public MyQueue() {
s1 = new Stack<>();
s2 = new Stack<>();
}
//模拟入队列
public void push(int x) {
s1.push(x);
}
//出队列
public int pop() {
if(empty()){
return -1;
}
if(s2.isEmpty()){
//把s1中的数据全部倒出
while(! s1.isEmpty()){
s2.push(s1.pop());
}
}
return s2.peek();
}
//获取队头元素
public int peek() {
if(empty()){
return -1;
}
if(s2.isEmpty()){
//把s1中的数据全部倒出
while(! s1.isEmpty()){
s2.push(s1.pop());
}
}
return s2.peek();
}
//两个栈都为空,则队列为空
public boolean empty() {
return s1.isEmpty() && s2.isEmpty();
}
}
java
public class Solution {
public static void main(String[] args) {
MyQueue queue = new MyQueue();
System.out.println(queue.empty());
queue.push(1);
queue.push(2);
queue.push(3);
System.out.println(queue.pop());
System.out.println(queue.peek());
System.out.println(queue.empty());
}
}