数据结构与算法之队列

(一) 队列

1.定义

队列也是一种操作受限的线性表数据结构, 它只允许在表的前端(队首)进行删除操作,而在表的后端(队尾)进行插入操作. 队列是一种先进先出的数据结构(First In First Out: FIFO). 相比较数组, 栈对应的操作是数组的子集.

2.图解
  • 队列的存储结构: front队首指针等于tail队尾指针, 队列为空.
    在这里插入图片描述
  • 队列依次入队添加元素 A、B、C, 每次入队 tail++
    在这里插入图片描述
  • 队列出队移除元素, front++
    在这里插入图片描述
  • 假溢出: 在顺序队列中,当队尾指针已经到数组的上界,不能再有入队操作,否则会造成数组越界而遭致程序出错, 也不能扩容, 因为数组中还有大量空间未被占用。 在这里插入图片描述
  • 循环队列: 解决顺序队列假溢出问题, 循环队列仍然是基于数组实现.
    在这里插入图片描述
    循环队列中的front指针或tail指针指向队尾时,只要队首还存在未使用的空间, 那么指针可以循环到队首0索引处. 但此时,
    front == tail 既可以表示队列为空, 又可以表示队列已满.

    解决方案:
  1. 设标志位法: 设置一个标志量flag: 入队操作设置flag=1, 出队操作设置flag=0.
    当front == tail && flag == 1, 队列已满; 当front == tail && flag == 0, 队列为空.
  2. 预存长度法: 设置一个计数器size记录队列中元素个数.
  3. 空闲单元法: 人为浪费一个单元, 如上图所示. 当 front == tail, 队列为空; 当front == (tail + 1) % size, 队列已满.
    (size 为数组长度, 取余操作时保证队尾指针tail在数组长度内), 下文的LoopQueue循环队列采用此方法实现.

(二) 队列的基本实现

基于<<数据结构与算法之数组>>中的动态数组实现

1.新建接口Queue
/**
 * 自定队列数据结构接口
 * 
 * @author Administrator
 *
 */
public interface Queue<E> {

	/**
	 * 入队: 向队列(队尾)中添加元素
	 * 
	 * @param e
	 */
	void enqueue(E e);
	
	/**
	 * 出队: 从队列(队首)中取出一个元素
	 * 
	 * @return
	 */
	E dequeue();
	
	/**
	 * 查看队首元素
	 * 
	 * @return
	 */
	E getFront();
	
	/**
	 * 获取队列中有效元素的个数
	 * 
	 * @return
	 */
	int getSize();
	
	/**
	 * 返回队列中有效元素是否为空
	 * 
	 * @return
	 */
	boolean isEmpty();
}
2.自定义ArrayQueue数组队列
/**
 * 自定义数组(顺序)队列实现Queue接口
 * 
 * @author Administrator
 *
 * @param <E>
 */
public class ArrayQueue<E> implements Queue<E> {

	private Array<E> array;

	public ArrayQueue() {
		array = new Array<>();
	}

	public ArrayQueue(int capacity) {
		array = new Array<>(capacity);
	}

	/**
	 * 入队: 向队列(队尾)添加一个元素
	 */
	@Override
	public void enqueue(E e) {
		array.addLast(e);
	}

	/**
	 * 出队: 从队列(队首)取出一个元素
	 */
	@Override
	public E dequeue() {
		return array.removeFirst();
	}

	/**
	 * 查看队首元素
	 */
	@Override
	public E getFront() {
		return array.getFirst();
	}

	/**
	 * 查看队列中有效元素的个数
	 */
	@Override
	public int getSize() {
		return array.getSize();
	}

	/**
	 * 返回队列中有效元素是否为空
	 */
	@Override
	public boolean isEmpty() {
		return array.isEmpty();
	}

	@Override
	public String toString() {
		StringBuilder res = new StringBuilder();
		res.append("Queue: ");
		res.append("front [");
		for (int i = 0; i < array.getSize(); i++) {
			res.append(array.get(i));
			if (i != array.getSize() - 1) {
				res.append(", ");
			}
		}
		res.append("] tail");
		return res.toString();
	}
}

测试

public static void main(String[] args) {
	ArrayQueue<Integer> queue = new ArrayQueue<>();
	// 向队列中添加元素: 0-9
	for (int i = 0; i < 10; i++) {
		queue.enqueue(i);
		System.out.println(queue);
		
		// 每添加三个元素, 移除队列队首元素
		if(i % 3 == 2) {
			queue.dequeue();
			System.out.println(queue);
		}
	}
}

front: 代表队列队首; tail: 代表队列队尾.
在这里插入图片描述
数组队列的时间复杂度分析

函数时间复杂度分析
enqueue(e)O(1)直接往size索引处赋值,此操作消耗的时间与数据规模无关系的, 在常数时间内完成
dequeue()O(n)从队列(队首)中取出一个元素, 所有元素向后移一个单位, 与数据规模呈线性关系
getFront()O(1)直接获取0索引处的值,此操作消耗的时间与数据规模无关系的, 在常数时间内完成
getSize()O(1)直接获取size的值,此操作消耗的时间与数据规模无关系的, 在常数时间内完成
isEmpty()O(1)直接判断size==0,此操作消耗的时间与数据规模无关系的, 在常数时间内完成

数组队列的dequeue()操作, 从队列(队首)中取出一个元素, 所有元素向后移一个单位. 它的时间复杂度是 O(n) 级别的. 当数组队列中数据量很大情况下, 是稍微不合理的. 那么dequeue()操作的时间复杂度有没有可能是 O(1) 级别的?
循环队列 的出现很好的解决了这个问题.

3.自定义LoopQueue循环队列
  • 创建LoopQueue类实现Queue接口
public class LoopQueue<E> implements Queue<E> {

	/**
	 * 存储数据的源数组
	 */
	private E[] data;
	
	/**
	 * 队首指针
	 */
	private int front;
	
	/**
	 * 队尾指针
	 */
	private int tail;

	/**
	 * 无参数的构造函数, 默认数组的容量capacity=10
	 */
	public LoopQueue() {
		this(10);
	}

	/**
	 * 构造函数, 创建capacity + 1容量的数组. 我们采用了牺牲一个元素空间,来区别队空或队满的方式, 
	 * 	但用户使用LoopQueue类, 无需关注底层实现, 为给用户带来更好的体验, 因此我们补全了一个单位的空间
	 * 
	 * @param capacity
	 */
	@SuppressWarnings("unchecked")
	public LoopQueue(int capacity) {
		data = (E[]) new Object[capacity + 1];
		front = 0;
		tail = 0;
	}

	@Override
	public void enqueue(E e) {
	}

	@Override
	public E dequeue() {
		return null;
	}

	@Override
	public E getFront() {
		return null;
	}

	@Override
	public int getSize() {
		return 0;
	}

	@Override
	public boolean isEmpty() {
		return false;
	}
	
}
  • 完善isEmpty()方法: 返回队列中有效元素是否为空
/**
 * 返回队列中有效元素是否为空: 队尾指针数 等于 队首指针数
 */
@Override
public boolean isEmpty() {
	return tail == front;
}
  • 完善getSize()方法: 获取队列中有效元素的个数
/**
 * 获取队列中有效元素的个数: 分两种情况
 * 		1. front队首指针数 小于 tail队尾指针数, 相当于数组队列, 有效元素的个数 = 队尾指针数 - 队首指针数
 * 		2. front队首指针数 大于 tail队尾指针数, 循环队列, 有效元素的个数 = 队尾指针数 + 队列容量与队首指针数之差
 */
@Override
public int getSize() {
	return front > tail ? tail + (data.length - front) : tail - front;
}
  • 完善getFront()方法: 查看队首元素
@Override
public E getFront() {
	return data[front];
}
  • 新增getCapacity(): 获取循环队列的真实容量
/**
 * 获取循环队列的真实容量: 当前数组长度-补全的一个单位的长度
 * 
 * @return
 */
@Override
public int getCapacity() {
	return data.length - 1;
}
  • 新增getCapacity(): 获取循环队列的真实容量
/**
 * 获取循环队列的真实容量: 当前数组长度-补全的一个单位的长度
 * 
 * @return
 */
@Override
public int getCapacity() {
	return data.length - 1;
}
  • 新增resize(capatity): 重置队列容量
/**
 *重置队列容量, 新建一个容量为newCapacity的队列赋值给data
 * 
 * @param capaccity
 */
@SuppressWarnings("unchecked")
private void resize(int capacity) {
	E[] newData = (E[]) new Object[capacity + 1];
	// 获取原队列有效元素个数
	int size = getSize();
	// 遍历循环队列, 赋值给新队列
	for (int i = 0; i < size; i++) {
		newData[i] = data[(i + front) % data.length];
	}
	data = newData;
	// 队首指针指向新队列0索引处
	front = 0;
	// 队尾指针指向 原队列有效元素个数 位置处
	tail = size;
}
  • 完善enqueue(e)方法: 入队, 向循环队列添加一个元素
@Override
public void enqueue(E e) {
	/*
	 * front == (tail + 1) % data.length: 代表循环队列已满, 扩容原容量的2倍
	 */
	if (front == (tail + 1) % data.length) {
		resize(2 * getCapacity());
	}
	data[tail] = e;
	// 取余操作: 保证队尾指针tail在数组长度内, 而不是简单队尾指针向后移一个单位
	tail = (tail + 1) % data.length;
}
  • 完善dequeue()方法: 出队, 从队列(队首)取出一个元素
@Override
public E dequeue() {
	if (isEmpty()) {
		throw new IllegalArgumentException("LoopQueue is empty.");
	}
	E e = data[front];
	data[front] = null;
	
	// 取余操作: 保证队首指针front在数组长度内, 而不是简单队首指针向后移一个单位
	front = (front + 1) % data.length;

	// 当队列的有效元素等于队列容量的四分之一时, 队列缩容至原容量的二分之一, 防止复杂度震荡
	if (getSize() == getCapacity() / 4 && getCapacity() / 2 != 0) {
		resize(getCapacity() / 2);
	}
	return e;
}

测试

public static void main(String[] args) {
	LoopQueue<Integer> queue = new LoopQueue<>();
	// 向队列中添加元素: 0-9
	for (int i = 0; i < 10; i++) {
		queue.enqueue(i);
		System.out.println(queue);
		
		// 每添加三个元素, 移除队列队首元素
		if(i % 3 == 2) {
			queue.dequeue();
			System.out.println(queue);
		}
	}
}

在这里插入图片描述

函数时间复杂度分析
dequeue()O(1)从循环队列中取出一个元素, 只需 (front+1)%data.length 操作, 此操作消耗的时间与数据规模无关系的, 在常数时间内完成
  • 数组队列和循环队列的比较
public static void main(String[] args) {
	int opCount = 200000;
	ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
	LoopQueue<Integer> loopQueue = new LoopQueue<>();
	System.out.println("ArrayQueue, time: " + testQueue(arrayQueue, opCount) + " s");
	System.out.println("LoopQueue, time: " + testQueue(loopQueue, opCount) + " s");
}

/**
 * 测试使用queue运行opCount个enqueue和dequeue操作所需要的的时间, 单位: 秒
 * 
 * @param queue
 * @param opCount
 * @return
 */
public static double testQueue(Queue<Integer> queue, int opCount) {
	long startTime = System.currentTimeMillis();
	for (int i = 0; i < opCount; i++) {
		queue.enqueue(i);
	}
	for (int i = 0; i < opCount; i++) {
		queue.dequeue();
	}
	long endTime = System.currentTimeMillis();
	return (endTime - startTime) / 1000.0;
	}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值