菜鸟进阶(三)队列

一、队列的概念:

  队列(简称作队,Queue)也是一种特殊的线性表,队列的数据元素以及数据元素间的逻辑关系和线性表完全相同,其差别是线性表允许在任意位置插入和删除,而队列只允许在其一端进行插入操作在其另一端进行删除操作

队列中允许进行插入操作的一端称为队尾,允许进行删除操作的一端称为队头。队列的插入操作通常称作入队列,队列的删除操作通常称作出队列

下图是一个依次向队列中插入数据元素a0,a1,...,an-1后的示意图:

上图中,a0是当前 队头数据元素,an-1是当前 队尾数据元素。

为了避免当只有一个元素时,对头和队尾重合使得处理变得麻烦,所以引入两个指针:front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样的话,当front指针等于rear时,此队列不是还剩一个元素,而是空队列。

二、队列的抽象数据类型:

数据集合:

  队列的数据集合可以表示为a0,a1,…,an-1,每个数据元素的数据类型可以是任意的类型。

操作集合:

(1)入队列append(obj):把数据元素obj插入队尾。

(2)出队列delete():把队头数据元素删除并由函数返回。

(3)取队头数据元素getFront():取队头数据元素并由函数返回。

(4)非空否isEmpty():非空否。若队列非空,则函数返回false,否则函数返回true。

三、循环顺序队列:

线性表有顺序存储和链式存储,队列是一种特殊的线性表,同样也存在这两种存储方式。我们先来看一下队列的顺序存储。

1、顺序队列的“假溢出”:



上图中,front指针指向队头元素,rear指针指向队尾元素的下一个位置。图(d)中b、c、d出队后,front指针指向元素e,rear指针在数组外面。假设这个队列的总个数不超过5个,但目前如果接着入队的话,因数组末尾元素已经被占用,再向后加就会产生数组越界的错误,可实际上队列在下标为0、1、2、3、4的地方还是空闲的,我们把这种现象叫做“假溢出”。

2、循环顺序队列:

    所以解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种逻辑上首尾相连的顺序存储结构称为循环队列

如何判断循环队列究竟是空的还是满的:

  现在问题又来了,我们之前说,空队列时,front指针等于rear指针,那么现在循环队列满的时候,也是front等于rear,那么如何判断循环队列究竟是空的还是满的?有如下办法:

  • 办法1:设置一个标志位flag。初始时置flag=0;每当入队列操作成功就置flag=1;每当出队列操作成功就置flag=0。则队列空的判断条件为:rear == front && tag==0;队列满的判断条件为:rear = = front && tag= =1。

  • 办法2:保留一个元素的存储空间。此时,队列满时的判断条件为  (rear + 1) % maxSize == front;队列空的判断条件还是front == rear。

  • 办法3:设计一个计数器count,统计队列中的元素个数。此时,队列满的判断条件为:count > 0 && rear == front ;队列空的判断条件为count == 0。

我们在接下来的代码中采用方法3来实现。

3、代码实现:(循环顺序队列的创建)

/**
 * 使用环形数组,解决上面数组使用一次就不能用的问题
 *将这个数组使用算法,改进成一个环形的队列,使用取模%的方式
 * @author syw
 *思路如下
 *1.front变量的含义调整一下,将front不再指向队头的前一个元素,而是指向队头,既arr[front]为队头,front的初始值为0
 *2.rear也调整一下,将rear不再指向最后一个元素,而是最后一个元素的后一个位置。因为希望空出一个空间作为约定,rear的初始值也为0
 *注意:预留的位置是动态变化的,始终在rear后面,预留空间的目的是为了区别队空和队满
 *3.当队列满时,(rear+1)%maxSize==front,这里取模是为了找到rear的真实位置,因为rear随着取数据,插入数据。可能在front的前面。
 *注意maxSize是数组的长度,从0到maxSize-1,一共maxSize个区域。
 * 例子:当没有取数据的时候front为0,此时实际数据最多只能到arr[maxSize-2],rear此时指向arr[maxSize-1],也就是预留的空间,此时(rear+1)%maxSize=0=front;
 * 当取出一个数据的时候,此时front为1,rear不变,还是maxSize-1,再插入一个数据 到(rear+1)%maxSize,则此时rear指向0(预留空间的位置),插入的数据在arr[maxSize-1],也就是最后一个数据
 * 此时 (rear+1)%maxSize=1=front(满)
 *4.当队列空时候依然是 rear==front
 *5.队列中有效数据的个数  (rear+maxSize-front)%maxSize,例如maxSize为10,front=0,rear=4,此时明显有4个数据既arr[0],arr[1],arr[2],arr[3],
 *因为rear指向最后一个元素的下一个元素,所以实际有三个   此时 (4+10-0)%10=4,例如 maxSize为10,front为8,rear为1(实际到a[0],因为rear指向最后一个元素的下一个元素),
 *此时(1+10-8)%10=3,也是三个元素,a[8],a[9],a[0]
 *6.在原来的队列上修改变成环形队列
 */
public class RoundQueue {
	public static void main(String[] args) {
		//1.创建一个队列
		RoundArrayQueue arrayQueue = new RoundArrayQueue(4);//这里设置的为4,但是有效数据最多为3个
		char key=' ' ;//接受用户的输入
		Scanner sc=new Scanner(System.in);
		boolean loop=true;
		while(loop) {
			System.out.println("s(show):显示队列");
			System.out.println("e(exit):退出程序");
			System.out.println("a(add):添加数据到队列");
			System.out.println("g(get):从队列中取出数据");
			System.out.println("h(head):查看队列头部的数据");
			key =sc.next().charAt(0);//接收一个字符
			switch(key) {
			case 's':
				arrayQueue.showQueue();
				break;
			case 'a':
				System.out.println("输入一个数");
				int val=sc.nextInt();
				arrayQueue.addQueue(val);
				break;
			case 'g':
				try {
					int res=arrayQueue.getQueue();//注意这里取出数据只能一个一个个取出
					System.out.println("取出的数据是"+res);
				}catch (Exception e) {
					System.out.println(e.getMessage());
				}
				break;
			case 'e':
				sc.close();
				loop=false;
				break;
			case 'h':
				try {
					int res=arrayQueue.headVal();
					System.out.println("队列头部数据是\t"+res);
				}catch(Exception e) {
					System.out.println(e.getMessage());
				}
				break;
			default:
				break;
			}
		}
		System.out.println("程序退出");
	}
}
class RoundArrayQueue{
	private int maxSize;//表示数组的最大容量
	private int front ;//队列头 初始化为0
	private int rear;//队列尾部 初始化为0
	private int[] arr;//用于存放数据,模拟队列
	//1.创建队列的构造器
	public RoundArrayQueue(int arrMaxSize) {
		maxSize=arrMaxSize;
		arr=new int[maxSize]; //这里对数组进行大小初始化,如果不初始化不能存数据
		front=0; //指向队列头部
		rear=0;//指向队列最后一个元素的下一个元素。
	}
	//2.判断队列是否满
	public boolean isFull() {
		return (rear+1)%maxSize==front;
	}
	//3.判断队列是否为空
	public boolean isEmpty() {
		return rear==front;
	}
	//4,添加数据到队列
	public void addQueue(int n) {
		//判断队列是否满了
		if(isFull()) {
			System.out.println("队列满了,不能加入");
			return ;
		}
		//如果不满,因为rear的含义是最后一个存在元素的向下一个元素,也就是待插入的位置
		arr[rear]=n;
		//将rear后移,这里不能直接后移,如果此时在最后一个,直接后移会下标越界,因为是循环队列,通过取模可以跑到前面来
		rear=(rear+1)%maxSize;
	}
	//5,出队列,获取数据
	public int getQueue() {
		//判断队列是否为空
		if(isEmpty()) {
			//抛异常
			throw new RuntimeException("队列空,不能取");//throw会自动return
		}
		//不为空,取数据
		int temp=arr[front]; //先把front对应的值保存到一个临时变量
		front=(front+1)%maxSize; //front后移,注意这里front也不能直接++,因为可能会是front从上面,循环到下面
		return temp;
	}
	//6,显示队列的所有数据
	public void showQueue() {
		//遍历
		if(isEmpty()) {
			System.out.println("队列为空,没有数据");
			return ;
		}
		//从front开始,到有效元素个数,否则无法遍历
		int vallength=(rear+maxSize-front)%maxSize;//有效元素个数
		for(int i=front;i<front+vallength;i++) {
			//注意,此时下标为i%maxSize
			System.out.println("arr["+i%maxSize+"]="+arr[i%maxSize]);
		}
	}
	//7,显示队头,注意不是去除数据
	public int headVal() {
		if(isEmpty()) {
			throw new RuntimeException("队列空");
		}
		return arr[front];
	}
}

四、链式队列:

    链式队列其实就是特殊的单链表,只不过它只能尾进头出而已。链式队列的存储结构如下图所示:


1、链式队列的实现:

/**
 * 链队列的相关操作的实现
 */
public class LinkQueueImpl implements  LinkQueue{
    public Node front;//队头
    public Node end; //对尾
    public int count;//队列长度
    public LinkQueueImpl() {
        init();
    }
    //初始化队列
    public void init(){
        front=end=null;
        count=0;
    }
    @Override
    public void appendLinkQueue(Object object) throws Exception {
        //新的节点
        Node node=new Node(object,null);
        //原队尾的指针指向新的节点
        if(end!=null){
            end.setNext(node);
        }
        //尾指针指向新的节点
        end=node;
        //说明插入的是第一个节点
        if(front==null){
            front=node;
        }
        //个数增加
        count++;
    }
    @Override
    public Object popLinkQueue() throws Exception {
        if(!isEmpty()){
            //保存出队节点
            Node node=front;
            //出队
            front=front.getNext();
            count--;
            //如果出队之后数目为0
            if (count==0){
                end=null;
            }
            return node.getData();
        }else{
            throw new Exception("队列为空");
        }
    }
    @Override
    public Object getFront() throws Exception {
        if(!isEmpty()){
            return front.getData();
        }else{
            return null;
        }
    }
    @Override
    public Boolean isEmpty() {
        return count==0;
    }
    @Override
    public void cleanLinkQueue() {
       front=end=null;
       count=0;
    }
    @Override
    public void showLinkQueue() {
        //引入一个辅助节点
        Node temp=front;
        //遍历
        while (temp!=null){
            System.out.println(temp.getData());
            temp=temp.getNext();
        }
    }
}
class TestLinkQueue{
    public static void main(String[] args) throws Exception {
        LinkQueueImpl queue=new LinkQueueImpl();
        queue.appendLinkQueue("123");
        queue.appendLinkQueue("456");
        queue.appendLinkQueue("789");
        queue.showLinkQueue();
        System.out.println(queue.front+":"+queue.end+":"+queue.count);
        queue.popLinkQueue();
        System.out.println(queue.front+":"+queue.end+":"+queue.count);
        queue.cleanLinkQueue();
        System.out.println(queue.front+":"+queue.end+":"+queue.count);
    }
}
/**
 * 定义队列中的节点
 */
class Node{
    //数据域
    private Object data;
    //指针域
    private Node next;
    //头结点的创建方式
    public Node(Object data) {
        this.data = data;
    }
    //非头结点的构造
    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }
    //获取当前数据域的值
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public Node getNext() {
        return next;
    }
    public void setNext(Node next) {
        this.next = next;
    }
    @Override
    public String toString() {
        return "Node{" +
                "data=" + data +
                '}';
    }
}
/**
 * 声明队列中的方法
 */
interface LinkQueue{
    //入队
    public void appendLinkQueue(Object object) throws Exception;
    //出队
    public Object popLinkQueue()throws Exception;
    //获取队头元素
    public Object getFront()throws Exception;
    //判断队列是否为空
    public Boolean isEmpty();
    //清空队列
    public void cleanLinkQueue();
    //遍历队列
    public  void showLinkQueue();
}

2、循环队列和链式队列的比较:

(1)从时间上看,它们的基本操作都是常数时间,即O(1)的。不过循环队列是事先申请好空间,使用期间不释放;而链式队列,每次申请和释放结点也会存在一定的时间开销,如果入栈和出栈比较频繁,则两者还是有细微的差别。

(2)从空间上看,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链式队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链式队列更加灵活。

总结:总的来说,在可以确定队列长度的最大值的情况下,建议用循环队列,如果你无法估计队列的长度,那就用链式队列。

五、优先级队列:

  优先级队列是带有优先级的队列。

    用顺序存储结构实现的优先级队列称作顺序优先级队列

    用链式存储结构存储的优先级队列称作链式优先级队列

顺序优先级队列顺序循环队列相比主要有两点不同

(1)对于顺序优先级队列来说,出队列操作不是把队头数据元素出队列,而是把队列中优先级最高的数据元素出队列。(入队操作没区别)

(2)对于顺序优先级队列来说,数据元素由两部分组成,一部分是原先意义上的数据元素,另一部分是优先级。通常设计优先级为int类型的数值,并规定数值越小优先级越高

 1、顺序优先队列的实现:

设计顺序优先级队列分为两个类:

  数据元素类

  优先级队列类

代码实现:

/**
 * 数组实现的优先队列
 */
public class PrioritySequenceQueue implements  PriorityQueue{
    static final int defaultSize = 10; //默认队列长度
    public int front; //队头
    public int end;//队尾
    public int count; //队列中的元素数量
    public int maxSize; //队列最大长度
    public Element[] queue; //队列
    public PrioritySequenceQueue() {
        init(defaultSize);
    }
    public PrioritySequenceQueue(int maxSize) {
        init(maxSize);
    }
    public void init(int size){
        front=end=0;
        count=0;
        maxSize=size;
        queue=new Element[size];
    }
    @Override
    public void EnQueue(Element element) throws Exception {
        if(count>=maxSize){
            throw new Exception("出错");
        }else{
            queue[end]=element;
            end++;
            count++;
        }
    }
    @Override
    public Object OutQueue() throws Exception {
        if(isEmpty()){
            throw new Exception("出错");
        }else{
            //假设第一个元素的优先级最高(越小越高)
            Element min=queue[0];
            int minIndex=0;
            for(int i=0;i<count;i++){
                if(queue[i].getPriority()<min.getPriority()){
                    //更新优先级最高的元素
                    min=queue[i];
                    minIndex=i;
                }
            }
            //优先级最高的元素后面的元素后移
            for (int i=minIndex+1;i<count;i++){
                //从minIndex开始,每个元素都指向后面一个元素
                queue[i-1]=queue[i];
            }
            end--;
            count--;
            return min;
        }
    }
    @Override
    public Object getFrontElement() throws Exception {
        if(isEmpty()){
            throw new Exception("出错");
        }else {
            //假设第一个元素为优先级最高的元素
            Element min=queue[0];
            int minIndex=0;
            for(int i=0;i<count;i++){
                if(queue[i].getPriority()<min.getPriority()){
                    //替换
                    min=queue[i];
                    minIndex=i;
                }
            }
            return min;
        }
    }
    @Override
    public boolean isEmpty() {
        return count==0;
    }
}
interface  PriorityQueue{
    //入队
    public void EnQueue(Element element) throws Exception;
    //出队,默认取出优先级最高的元素
    public Object OutQueue() throws Exception;
    //查看优先级最高的元素
    public Object getFrontElement() throws Exception;
    //判断队列是否为空
    public boolean isEmpty();
}
/**
 * 优先队列的元素节点
 */
class Element{
    private Object element;//数据
    private int priority;//优先级
    public Element(Object element, int priority) {
        this.element = element;
        this.priority = priority;
    }
    public Object getElement() {
        return element;
    }
    public void setElement(Object element) {
        this.element = element;
    }
    public int getPriority() {
        return priority;
    }
    public void setPriority(int priority) {
        this.priority = priority;
    }
}

六、队列的实际应用:

使用顺序循环队列和多线程实现一个排队买票的例子。而且,我们只允许这个队伍中同时排队的只有10个人,那就需要用到队列了。

    生产者(等候买票)

    消费者 (买票离开)








全部评论