9.面试算法-队列专题

1. 队列的概念和基本特征

1.1 队列的基本概念

队列的特点是节点的排队次序和出队次序按入队时间先后确定,即先入队者先出队,后入队者后出队,即我们常说的FIFO(first in first out)先进先出。队列实现方式也有两种形式,基于数组和基于链表。对于基于链表,因为链表的长度是随时都可以变的,实现起来比较简单。如果是基于数组的,会有点麻烦,例如假如我们初始化一个长度是 7的队列:
在这里插入图片描述
顺序存储结构存储的队列称为顺序队列,内部使用一个一维数组存储,用一个队头指针front指向队列头部节点(即使用int类型front来表示队头元素的下标),用一个队尾指针rear(有的地方会用tail,只要在一个问题里统一起来就行了),指向队列尾部元素(int类型rear来表示队尾节点的下标)。

初始化队列时: front = rear = -1 (非必须,也可设置初始值为0,在实现方法时具体修改)

队列满时: rear = maxSize-1 (其中maxSize为初始化队列时,设置的队列最大元素个数)

队列为空时: front = rear

front指向的是队列的头, rear指向的是队列尾的下一个存储空间,最初始的时候front=0, rear=0,每添加一个元素rear就加1,每移除一个元素front就加1,但是这样会有一个问题,如果一个元素不停的加入队列,然后再不停的从队列中移除,会导致rear和front越来越大,最后会导致队列无法再加入数据了,但实际上队列前面全部都是空的,这导致空间的极大浪费。

在代码中初始化了一个大小为6的顺序队列,下图展示了第一步(即代码ArrayQueue queue = new ArrayQueue(6)) 中队列元素及指针情况:
在这里插入图片描述
其中front和rear指向的虚线框实际并不存在,仅用来表示初始化时的默认状态,因为我们实现的队列元素使用int[] 存储元素,所以初始值均为0(如用Object[]或范型则初始值为null),执行queue.add(1)方法后队列的状态如下图:
在这里插入图片描述
可以看到向队列中添加元素后, rear指针向后移动一个位置指向第一个元素位置,后面继续添加后面5个元素后队列如下图所示:
在这里插入图片描述
接下来看下队列的出队情况 :
在这里插入图片描述
当第一次执行queue.pop()方法后,队列元素如上图所示,此时队列剩下5个元素

当第六次执行queue.pop()方法后,队列元素如下图所示:
在这里插入图片描述
此时队列中元素已全部出队,按正常逻辑应该可以添加元素到队列中,但此时添加元素却会报队列已满错误(rear=maxSize-1),当然即使前面元素未出队也会报相同错误。这就是我们常说的 “假溢出” 问题。为解决这个问题,就引出了我们的环形队列。

1.2 基于数组实现简单的队列

根据上面的说明,这里先使用java实现一个基于一维数组的顺序队列,代码如下:

class ArrayQueue{
	//队列中存放的数据 
	private int[] data ; 
	//队列的大小
	private int maxSize ;
	//指向队列头部的指针
	private int front ;   
	//指向队列尾部的指针
	private int rear ; 

	public ArrayQueue(int maxSize){
		this.maxSize = maxSize;
		data = new int[maxSize];
		front = -1;
		rear = -1;
	}
	/**
	* 判断队列是否已满
	*/
	public boolean isFull(){
		return rear == maxSize -1 ;
	}
	/**
	* 判断队列是否为空
	*/
	public boolean isEmpty(){
		return rear == front;
	}
	/**
	* 添加数据到队列
	*/
	public void add(int n){
		if(isFull()){
			System.out.println("队列已满,不能添加"); return;
		}
		data[++rear] = n;
	}
	/**
	* 显示头部数据
	*/
	public void head(){
		if(isEmpty()){
			throw new RuntimeException("队列为空");
		}
		System.out.println(data[front+1]);
	}
	/**
	* 取出头部数据
	*/
	public int pop(){
		if(isEmpty()){
			throw new RuntimeException("队列为空");
		}
		int a = data[++front];
		data[front] = 0;
		return a;
	}
	/**
	* 打印全部数据
	*/
	public void print(){
		if(isEmpty()){
			System.out.println("队列为空"); return;
		}
		for(int i=0;i<data.length;i++){
			System.out.printf("array["+i+"]=%d\n",data[i]);
		}
	}
}

简单描述顺序队列的入队(add方法):

public static void main(String []args) {
	//1.声明一个可以存储6个元素的顺序队列,默认值为0,front 和rear指针为-1
	ArrayQueue queue = new ArrayQueue(6); 
	//2.向顺序队列中添加元素
	queue.add(1);
	queue.add(2);
	queue.add(3);
	queue.add(4);
	queue.add(5); 
	queue.add(6);
	//2.1打印当前队列元素
	queue.print();
	//3.将顺序队列中元素取出 
	queue.pop();
	queue.pop(); 
	queue.pop(); 
	queue.pop(); 
	queue.pop(); 
	queue.pop();

1.3 基于链表实现队列

基于链表实现队列还是比较好处理的,只要在尾部后插入元素,在front删除元素就行了。

public class LinkQueue {
	private Node front;
	private Node rear;
	private int size;
	
	public LinkQueue() {
		this.front = new Node(0);
		this.rear = new Node(0);
	}
	/**
	* 入队
	*/
	public void push(int value) {
		Node newNode = new Node(value);
		Node temp = front;
		while (temp.next != null) {
			temp = temp.next;
		}
		temp.next = newNode;
		rear = newNode;
		size++;
	}

	/**
	* 出队 
	*/
	public int pull() {
		if (front.next == null) {
			System.out.println("队列已空");
		}
		Node firstNode = front.next;
		front.next = firstNode.next;
		size--;
		return firstNode.data;
	}

	/**
	* 遍历队列
	*/
	public void traverse() {
		Node temp = front.next;
		while (temp != null) {
			System.out.print(temp.data + "\t");
			temp = temp.next;
		}
	}

	static class Node {
		public int data;
		public Node next;
		public Node(int data) {
			this.data = data;
		}
	}
	//测试main方法
	public static void main(String[] args) {
		LinkQueue linkQueue = new LinkQueue();
		linkQueue.push(1);
		linkQueue.push(2);
		linkQueue.push(3);
		System.out.println("第一个出队的元素为 :" + linkQueue.pull()); 
		System.out.println("队列中的元素为:");
		linkQueue.traverse();
	}
}

1.4 基于jdk的集合类实现队列

其实jdk中本身已经提供了List,其功能还是比较强大的,这里我们基于List来自己实现一个!

/**
* 使用集合实现队列功能,使用int数组保存数据特点:先进先出,后进后出 
*/
public class QueueTest2 {
	public static void main(String[] args){ 
		//测试队列
		System.out.println("测试队列: "); 
		Queue queue = new Queue();
		queue.in(1);
		queue.in(2);
		queue.in(3);
		System.out.println(queue.out());
		System.out.println(queue.out());
		queue.in(4);
		System.out.println(queue.out());
		System.out.println(queue.out());
		queue.in(5);
		System.out.println(queue.out());

	}
}

//使用集合定义一个队列 
class Queue {
	List<Integer> list = new ArrayList<Integer>(); 
	//下标
	int index = 0;  
	//入队
	public void in(int n){
		list.add(n);
		index++;
	}

	//出队
	public int out(){
		if(!list.isEmpty()){
			index--;
			return list.remove(0);
		}
		return -1;
	}
}

2. 环形队列

在我们上面基于数组实现的队列中,假如头和尾都到了末尾,接下来还有新元素来该怎么办呢?虽然这时候个空间是空的,但是无法插入新元素,为此,我们将其设计成环形结构。

环形队列,顾名思义即让普通队列首尾相连,形成一个环形。当rear指向尾元素后,当队列有元素出队时,可以继续向队列中添加元素。这里使用 rear指针指向最后一个节点的后一个元素,即会占用一个位置用来表示队列已满。

  • 初始化队列时: front = rear = 0
  • 队列满时: ( rear +1 ) % maxSize == front (其中maxSize为初始化队列时,设置的队列最大元素个数)
    这里不能使用 rear = maxSize-1作为判断队满的条件,因使用环形队列方式实现,当第一次队满时,rear = maxSize -1,执行出队操作后原队头位置空出,此时继续执行入队操作,则 rear 向后移动一个位置,则rear = 0, 而此时队列也是已满状态。所以只要 rear 向前移动一个位置就等于front时,就是队满的情况。
  • 队列为空时: front == rear。

下面再以图解的方式讲解一下环形队列的入队出队以及队满情况。
在这里插入图片描述
此时front = rear = 0,队列为空。当第一次执行queue.add(1)后,环形队列元素如下图所示:
在这里插入图片描述
当依次执行queue.add(2);queue.add(3);queue.add(4);queue.add(5);后,达到(rear+1)%maxSize=front(即 rear=5)条件,队列已满不能添加新元素。此时环形队列元素情况如下图:
在这里插入图片描述
所以这种情况会浪费一个空间来作为判满的条件。

下面执行出队操作,当第一次执行出队操作queue.pop()方法后,环形队列元素情况如下图所示:
在这里插入图片描述
此时 (rear+1)%maxSize = 0 不等于 front=1,所以可以继续向队列中添加元素,也就不会出现假溢出的情况。当执行入队(例queue.add(6))操作后,rear = (rear+1)%maxSize 即rear=0,以此来生成环形队列。此时队列元素情况,如下图所示:
在这里插入图片描述
在这种场景下,环形队列有效元素个数该怎么计算呢,如果不是环形队列,则有效元素个数size = rear - front。而使用环形实现后,会出现rear<front的情况。

所以要使用(rear-front+maxSize)%maxSize的方式计算有效元素个数。(或者在内部定义一个size属性,当元素入队时size++,当出队时size–)。

因此在打印队列中元素时,从front位置开始至 front+size位置结束来循环打印有效元素。

如果不实用环形队列方式实现队列,则会出现“假溢出”情况(即队列满后,将全部元素出队却不能继续添加元素的 情况)。而环形队列会在队头元素出队后,将队尾指针rear重新分配为0,以达到循环使用队列空间的目的。

基于数组实现的例子:

public class CycleQueue {
	private int maxSize;
	private int data[];
	private int front;
	// 这里rear指向最后一个数据的后面一个位置,即队列中有一个为空占位 
	private int rear;

	public CycleQueue(int maxSize) {
		this.maxSize = maxSize;
		data = new int[maxSize];
		front = 0;
		rear = 0;
	}

	/**
	* 判断队列是否已满
	* 因是循环队列,所以rear值可能小于front,所以不能使用 rear == maxSize -1来判断 
	*/
	public boolean isFull() {
		return (rear + 1) % maxSize == front;
	}

	public boolean isEmpty() {
		return rear == front;
	}

	public void add(int n) {
		if (isFull()) {
			System.out.println("队列已满,不能添加"); return;
		}
		data[rear] = n;
		rear = (rear + 1) % maxSize;
	}

	public void head() {
		if (isEmpty()) {
			throw new RuntimeException("队列为空");
		}
		System.out.println("head=" + data[front]);
	}

	public int pop() {
		if (isEmpty()) {
			throw new RuntimeException("队列为空");
		}
		int value = data[front];
		front = (front + 1) % maxSize;
		return value;
	}

	public void print() {
		if (isEmpty()) {
			System.out.println("队列为空"); return;
		}
		for (int i = front; i < front + size(); i++) {
			System.out.printf("array" + (i % maxSize) + "=%d", data[i % maxSize]);
		}
	}

	/**
	* 因是循环队列,所以会出现rear<front情况,这里需要+maxSize
	*/
	public int size() {
		return (rear - front + maxSize) % maxSize;
	}

	public static void main(String[] args) {
		CycleQueue cycleQueue = new CycleQueue(3);
		cycleQueue.add(1);
		cycleQueue.add(2);
		cycleQueue.pop(
		cycleQueue.add(3);
		cycleQueue.print();
	}
}

3. 栈实现队列和队列实现栈

栈的特点是后进先出,队的特点是先进先出。两个栈将底部拼接到一起就能实现队列的效果,通过队列也能实现栈 的功能。在很多地方能看到让你通过两个栈实现队列的题目,也有很多地方是两个队列实现栈的题目,我们就干脆一次看一下如何做。这正好对应LeetCode232和225两道题。

3.1 用栈实现队列

LeetCode232 先看题意

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:
你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

这个题的思路是:将一个栈当作输入栈,用于压入 push 传入的数据;另一个栈当作输出栈,用于pop 和 peek 操作。

每次pop 或 peek 时,若输出栈为空则将输入栈的全部数据依次弹出并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序。

代码难度不算大:

class MyQueue {
	Deque<Integer> inStack;
	Deque<Integer> outStack;
	public MyQueue() {
		inStack = new LinkedList<Integer>();
		outStack = new LinkedList<Integer>();
	}
	public void push(int x) {
		inStack.push(x);
	}
	public int pop() {
		if (outStack.isEmpty()) {
			in2out();
		}
		return outStack.pop();
	}
	public int peek() {
		if (outStack.isEmpty()) {
			in2out();
		}
		return outStack.peek();
	}
	public boolean empty() {
		return inStack.isEmpty() && outStack.isEmpty();
	}
	private void in2out() {
		while (!inStack.isEmpty()) {
			outStack.push(inStack.pop());
		}
	}
}

3.2 用队列实现栈

leetcode225 先看题意:

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

分析:这个问题首先想到的是使用两个队列来实现。为了满足栈的特性,即最后入栈的元素最先出栈,在使用队列实现栈时,应满足队列前端的元素是最后入栈的元素。可以使用两个队列实现栈的操作,其中 queue1用于存储栈内的元素,queue2作为入栈操作的辅助队列。

入栈操作时,首先将元素入队到 queue2,然后将 queue1的全部元素依次出队并入队到queue2,此时queue2的 前端的元素即为新入栈的元素,再将 queue1和queue2互换,则 queue1的元素即为栈内的元素,queue1的前端和后端分别对应栈顶和栈底。

由于每次入栈操作都确保queue1的前端元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以简单实现。出栈操作只需要移除queue1的前端元素并返回即可,获得栈顶元素操作只需要获得 queue1的前端元素并返回即可(不移除元素)。

由于 queue1用于存储栈内的元素,判断栈是否为空时,只需要判断 queue1是否为空即可。

class MyStack {
	Queue<Integer> queue1;
	Queue<Integer> queue2;
	public MyStack() {
		queue1 = new LinkedList<Integer>();
		queue2 = new LinkedList<Integer>();
	}
	public void push(int x) {
		queue2.offer(x);
		while (!queue1.isEmpty()) {
			queue2.offer(queue1.poll());
		}
		Queue<Integer> temp = queue1;
		queue1 = queue2;
		queue2 = temp;
	}

	public int pop() {
		return queue1.poll();
	}
	public int top() {
		return queue1.peek();
	}
	public boolean empty() {
		return queue1.isEmpty();
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值