JAVA数据结构
Java数据结构(4)——队列和栈
1.栈
栈可以看做是一种特殊的线性表,访问、插入和删除其中的元素只能在栈尾(栈顶)进行。也就是说,栈内的元素遵循先进后出的规则。由于栈只允许在栈顶进行插入与删除的操作,所以用数组线性表来实现比用链表来实现效率更高。通常情况,有两种方法可以来设计栈。
- 继承:通过扩展数组线性表ArrayList来定义栈类。
- 组合:讲数组线性表定义为栈类种的数据域。
上述两种方法相比之下,组合的方法可能更适合于栈类的创建,因为它可以定义一个全新的栈类而不需要继承数组线性表中不必要和不合适的方法。下表将说明栈中的一些基本方法,并通过组合方法实现它们。
方法名 | 方法 |
---|---|
MyStack() | 构造函数 |
int getSize() | 返回这个栈的元素个数 |
E peek() | 返回这个栈的栈顶元素 |
E pop() | 返回并删除这个栈的栈顶元素 |
void push() | 添加新元素至栈顶 |
下面将给出上述方法以及栈类的具体实现:
public class MyStack<E>{
private MyArrayList<E> List=new MyArrayList<E>();//数组线性表的构建在数据结构(1)中已经给出
/*上述方法展现的就是通过组合方法将数组线性表融入到栈类中,并通过数组线性表来创建栈类*/
public Mystack(){}
public int getSize(){
return List.size();
}
public E peek(){
return List.get(getSize()-1);
}
public void push(E e){
list.add(e);
}
public E pop(){
E e=List.get(getSize()-1);
List.remove(getSize()-1);
return e;
}
}
对于一个栈来说,push(e)方法将一个元素添加至栈顶,而pop()方法将栈顶元素从栈中删除并返回钙元素。很容易看出来,push()和pop()方法的时间复杂度是O(1)。
2.队列
队列也可以看做是一种特殊类型的线性表,它与前文中所提到的栈不同,队列的特点在于其存存储元素和提取元素的方式是先进先出的,也就是说元素只能从队列的末端插入,从开始端访问和删除。对于队列来说,删除元素的操作是在线性表的起始位置开始的,所以用链表实现队列的效率更高,因此本文还是通过组合的方法,利用链表实现队列。其中链表的实现方法已再数据结构(1)中给出,在此不再赘述。队列的具体方法如下表所示。
方法名 | 方法 |
---|---|
Myqueue() | 构造队列 |
void enqueue(E e) | 向队列添加一个元素 |
E dequeue() | 删除并返回队列中的一个元素 |
int getSize() | 返回队列元素个数 |
下面将给出队列的实现方法:
public class MyQueue<E>{
private MyLinkedList<E> list=new MyLinkedList<E>();
public MyQueue(){};
public void enqueue(){
list.addLast(E);//添加链表尾节点元素
}
public E dequeue(){
return list.removeFirst();//删除并返回链表头节点元素
}
public int getSize(){
return list.size();
}
}
对于一个队列来说enqueue(e)方法将一个元素添加到队尾,而dequeue()方法从队头删除并返回元素。很容易看出enqueue和dequeue的方法的时间复杂度尾O(1)。此外上一文中并未给出addLast()和removeFirst()方法,在此本文将给出这两种方法的具体实现,仅供参考。
/*addLast方法*/
public void addLast(E e){
Node<E> newNode=new Node<E>();
if(tail==null){
head=tail=newNode;
}else{
tail.next=newNode;
tail=tail.next;
}
size++;
}
/*removeFirst方法*/
public E removeFirst(){
if(size==0) return null;
else if(size==1){
Node<>
}
}
2.1优先队列
普通队列是一种先进先出的数据结构,元素在队尾追加,而从队列头删除。在优先队列中,元素被赋予了优先级。当元素被访问时,具有最高优先级的元素最先删除。优先队列具有最高进先出的行为特征。通常情况下,我们可以使用堆实现优先队列,其中根节点是队列中具有最高优先级的对象。(堆的内容详见数据结构(3))优先队列包含的方法如下。
方法名 | 功能 |
---|---|
void enqueue(E element) | 向队列中添加一个元素 |
E dequeue() | 从队列中删除一个元素 |
int getSize() | 返回队列中的元素个数 |
具体实现代码如下所示,其中堆实现的代码以及各方法实现在数据结构(3)中具体给出。
class PriorityQueue<E extends Comparable<E>>{
private heap<E> heap=new heap<E>();
public void enqueue(E element){
heap.insert(element);
}//插入元素
public E dequeue(){
return heap.remove();
}//删除元素
public int getSize(){
return heap.getSize();
}//返回队列大小
}
2.1阻塞队列
阻塞队列(BlockingQueue)是Concurrent包中新增的数据结构,BlockingQueue很好的解决了多线程中如何高效安全传输数据的问题。其功能是在试图向一个满队列添加元素或者从空队列删除元素时会导致线程阻塞,并且提供同步的put和take方法向队列头添加元素,以及从队列尾删除元素。
多线程环境中,通过队列可以很容易实现数据共享。在concurrent包发布以前,在多线程的环境下我们往往要自己去调试线程之间数据处理速度不匹配的情况,运用锁和条件同步各线程。而BlockingQueue解决了这一问题。以消费者和生产者经典模型为例子,阻塞队列两个常见的阻塞场景如下:
- 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞,直到有数据放入队列。
- 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞,直到队列中有空的位置,线程被自动唤醒。
下表将展示阻塞队列接口中所包含的主要方法。
方法名 | 方法 |
---|---|
void put(E element) | 在队尾插入一个元素,若队列满则等待 |
E take() | 返回并删除队列头,若队列空则等待 |
void put(E element) | 在队尾插入一个元素,若队满则等待 |
boolean offer(E e,long timeout,TimeUnit unit) | 设定等待时间,如果在制定的时间内还不能往队列中加入BlockingQueue,则返回失败 |
E poll(long timeout,TimeUnit unit) | 从BlockingQueue取出一个队首的对象,如果在制定时间内,队列一旦有数据可取则返回队列中的数据;否则直到超时还没有数据可取则返回失败 |
常见的BlockingQueue有5种,具体包括:ArrayBlockingQueue, LinkedBlockingQueue, DelayQueue, PriorityBlockingQueue, SynchronousQueue。
- ArrayBlockingQueue
基于数组的阻塞队列实现,在该种队列内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标志队列的头部和尾部在数组中的位置。 - LinkedBlockingQueue
基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列,当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有队列缓冲区达到最大值缓存容量时,才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。
ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产消费者问题,使用这两个类足够使用。
下面代码是经典的生产者与消费者案例,示范了如何使用BlockingQueue:
import java.util.concurrent.*;
import java.util.Random;
public class BlockingTest{
/*创建基于数组的阻塞队列,其容量为2*/
public static ArrayBlockingQueue<Integer> buffer=new ArrayBlockingQueue<Integer>(2);
/*创建生产者任务类*/
private static class producerTask implements Runnable{
/*任务是否执行的标志*/
boolean isrunning=true;
public void run(){
Random rand=new Random();
try{
int i=1;
while(isrunning){
System.out.println("Producer writes"+i);
buffer.put(i++);
Thread.sleep(rand.nextInt(1000));
/*若超过2秒没有输入数据则输出显示失败*/
if(!buffer.offer(i,2,TimeUnit.SECONDS)){
System.out.println("Input data failure!");
}
}
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
public void stop(){
isrunning=false;
}
}
/*创建消费者任务类*/
private static class consumerTask implements Runnable{
boolean isrunning=true;
public void run(){
Random rand=new Random();
try{
while(isrunning){
Integer data=buffer.poll(2,TimeUnit.SECONDS);
/*若超过2秒没有数据写入则停止写入数据*/
if(null!=data){
System.out.println("\t\t\tConsumer takes"+data);
Thread.sleep(rand.nextInt(1000));
}else{
isrunning=false;
}
}
}catch(InterruptedException ex){
ex.printStackTrace();
Thread.currentThread().interrupt();
}finally{
System.out.println("Exit!");
}
}
}
public static void main(String[] args){
/*创建线程池*/
ExecutorService executor=Executors.newFixedThreadPool(2);
producerTask pTask=new producerTask();
consumerTask cTask=new consumerTask();
try{
/*启动线程*/
executor.execute(pTask);
executor.execute(cTask);
/*执行10秒*/
Thread.sleep(10*1000);
pTask.stop();
executor.shutdown();
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
}
BlockingQueue不光实现了一个完整队列所具有的基本功能,同时在多线程环境下还可以自动管理了多线键的自动等待与唤醒功能,这一点功能颇为实用。