数据结构与算法(队列)

文章介绍了队列作为一种线性数据结构,其先进先出的特性以及在Java中的实现,如LinkedList和ArrayQueue。队列的基本操作包括入队和出队,文章通过示例代码展示了这些操作。此外,还讨论了基于数组和链表的队列实现,循环队列的概念以解决数组队列的空间问题,以及阻塞队列和并发队列在多线程环境中的重要性,特别是并发队列如何利用CAS实现高效线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

队列

队列是一种常用的线性数据结构,具有先进先出的特点。队列中元素的插入和删除操作分别在队列的两端进行,新元素被插入到队列的尾部,而原有元素则从队列的头部被删除。
队列中包含两个基本操作 :入队(enqueue)和出队(dequeue)。其中,入队操作会将一个元素添加到队列的尾部,而出队操作会从队列的头部删除一个元素,并返回该元素的值。此外,队列还支持其他常见操作,例如 获取队列长度、判断队列是否为空、清空队列等。
java中提供了多种不同类型的队列实现类,例如:LinkedList、ArrayDeque、PriorityQueue等。以下是一个使用Java中内置的LinkedList实现队列的代码:

public class QueueExample {
	public static void main(String[] args) {
		//创建一个队列对象
		Queue<String> queue = new LinkedList<>();
		//入队操作
		queue.offer("a");
		queue.offer("b");
		queue.offer("c");
		//出队操作
		String firstElement = queue.poll();
		System.out.println("出队元素:" + firstElement);
		//获取队列长度
		int queueSize = queue.size();
		System.out.println("队列长度:" + queueSize);
		//判断队列是否为空
		boolean isEmpty = queue.isEmpty();
		System.out.println("队列是否为空:" + isEmpty);
		//清空队列
		queue.clear();
	}
}

该代码创建了一个队列对象queue,并向其中插入三个元素。接着,通过调用poll方法从队列中删除第一个元素,并输出其值。然后获取队列长度、判断队列是否为空并清空队列等操作。需要注意的是,在使用队列时需要遵循先进先出的原则,以确保数据处理的正确性和公正性。

基于数组实现的队列

//用数组实现的队列
public class ArrayQueue{
	//数组:items,数组大小:n
	private String[] items;
	private int n = 0;
	//head表示队列下标,tail表示队尾下标
	private int head = 0;
	private int tail = 0;
//申请一个大小为capacity的数组
public ArrayQueue(int capacity){
	items = new String[capacity];
	n = capacity;
}

//入队
public boolean enqueue(String item){
	//如果tail == n表示队列已经满了
	if(tail == n){ 
	return false;
	}
	items[tail] = item;
	++tail;
	return true;
}
//出队
public String dequeue(){
	//如果head == tail表示队列为空
	if(head == tail){
	return null;
	}
	String ret = items[head];
	++head;
	return ret;
}
}

实现队列需要两个指针:一个是head指针指向队头,一个是tail指针指向队尾。如下图:当a、b、c依次入队之后,队列中的head指针指向下标为0的位置,tail指针指向下标为4的位置。
在这里插入图片描述
当调用两次出队操作之后,队列中head指针指向下标为2的位置,tail指针仍然指向下标为4的位置
在这里插入图片描述
随着不停的入队(tail后移)、出队(head后移)操作,head和tail都会持续往后移动。当tail移动到最右边,此时,数组就算还有空闲空间,也无法继续往队列中添加数据了。这个问题如何解决?

//入队操作,将item放入队尾
public boolean enqueue(String item){
//tail == n表示队列末尾没有空间了
if(tail == n){
//tail == n && head == 0,表示整个队列都占满了
	if(head == 0){
		return false;
	}
	//数据搬移
	for(int i = head; i < tail; ++i){
		items[i-head] = items[i];
	}
	//搬移完之后重新更新head和tail
	tail -= head;
	head = 0;
	}
	items[tail] = item;
	++tail;
	return true;
}

在出队时可以不用搬移数据。如果没有空闲空间,我们只需要在入队时,再集中触发一次数据的搬移操作。
当队列的tail指针移动到数组的最右边后,如果有新的数据入队,我们可以将head和tail之间的数据,整体搬移到数组中0到tail-head的位置。
在这里插入图片描述

基于链表实现的队列

也是需要两个指针:head指针和tail指针,它们分别指向链表的第一个结点和最后一个结点。
入队时,tail->next=new node,tail = tail->next:出队时,head = head->next.
在这里插入图片描述

循环队列

我们刚才用数组来实现队列时,在tail == n时,会有数据搬移操作,这样入队操作性能就会受到影响。因此就出现了循环队列来解决这一问题。
在这里插入图片描述
可以看到这个队列的大小为8,当前head = 4,tail = 7。当有一个新的元素a入队时,我们放到下标为7的位置。但这个时候,我们并不把tail更新为8,而是将其在环中后移一位,到下标为0的位置。当再有一个元素b入队时,我们将b放入下标为0的位置,然后tail加1更新为1。因此,在a,b依次入队后,循环队列中的元素会变成如下图的所示:
在这里插入图片描述
想要写出循环队列的代码最关键的是,确定好队空和队满的判定条件。
在用数组实现的非循环队列中,队满的判断条件是tail == n,队空的判断条件是head == tail。对于如何判断队空和队满,可以这样来想,队列为空的判断条件仍然是head == tail。但队列满的判断条件就稍微复杂一些。通过下图的队满来看
在这里插入图片描述
不难发现(3+1)%8=4,由于tail = 3,head = 4,n = 8可以得出公式(tail+1)%n=head。
因此,当队满时,tail指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。如下代码:

public class CircularQueue{
	//数组:items,数组大小:n
	private String[] items;
	private int n = 0;
	//head表示队头下标,tail表示队尾下标
	private int head = 0;
	private int tail = 0;
	
	//申请一个大小为capacity的数组
	public CircularQueue(int capacity){
		items = new String[capacity];
		n = capacity;
	}
	//入队
	public boolean equeue(String item){
		//队列满了
		if((tail + 1)%n = head){
		return false;
		} 
		items(tail) = item;
		tail = (tail + 1)%n;
		return ture;
	}
	//出队
	public String dequeue(){
	//如果head == tail表示队列为空
	if(head == tail){
		return null;
	}
	String ret = items[head];
	head = (head + 1)%n;
	return ret;
	}
}

阻塞队列

在这里插入图片描述
阻塞队列是在队列基础上增加阻塞操作。其实就是队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
就是“生产者-消费者模型”,可以有效地协调生产和消费的速度。当“生产者”生产数据的速度过快,“消费者”来不及消费时,存储数据的队列很快就会满。这个时候,生产者就阻塞等待,直到“消费者”消费了数据,“生产者”才会被唤醒继续“生产”。
不仅如此,还可以通过协调“生产者”和“消费者”的个数,来提高数据的处理效率。比如上面的例子,可以多配置几个“消费者”,来应对一个“生产者”。
在这里插入图片描述

并发队列

在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,那如何实现一个线程安全的队列?因此把实现线程安全的队列称为 :并发队列。最简单直接的实现方式是直接在enqueue()、dequeue()方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用CAS原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值