• 队列的实现与应用


    队列是一种线性集合,其元素一端加入,从另一端删除,因此我们说队列元素是按先进先出(FIFO)方式处理。

    队列的处理过程:通常队列会画成水平,其中一端作为队列的前端(front)也称队首(head),另一端作为队列的末端(rear)也称队尾(tail).元素都是从队列末端进入,从队列前端退出.

    因而在队列中,其处理过程可在队列的两端进行,而在栈中,其处理过程只在栈的一端进行,但两者也有相似之处,与栈类似,队列中也没有操作能让用户“抵达”队列中部,同样也没有操作允许用户重组或删除多个元素。(不过这些操作都可以再链表中实现)

    下面我们定义一个泛型QueueADT接口来表示队列的各种操作(队列的首要作用是保存顺序,而栈则相反是用来颠倒顺序)

    package xidian.sl.queue;
    
    import xidian.sl.stack.EmptyCollectionException;
    
    /**
     * 栈的首要作用是颠倒顺序,而队列的首要作用是保持顺序
     * */
    public interface QueueADT<T> {
        /*向队列末尾添加一个元素*/
        public void enqueue(T element);
        
        /*从队列前端删除一个元素*/
        public T dequeue() throws EmptyCollectionException;
        
        /*考察队列前端的那个元素*/
        public T first();
        
        /*判断队列是否为空*/
        public boolean isEmpty();
        
        /*判定队列中的元素个数*/
        public int size();
        
        /*返回队列的字符串表示*/
        public String toString();
    }

    与栈的实现一样,我们这里也提供两种实现方法:链表与数组的实现

    1.链表实现队列:

    队列与栈的区别在于,我们必须要操作链表的两端。因此,除了一个指向链表首元素的引用外,还需要跟踪另一个指向链表末元素的引用。再增加一个整形变量count来跟踪队列中的元素个数。

    综合考虑,我们使用末端入列,前端出列

    package xidian.sl.queue;
    
    import xidian.sl.stack.EmptyCollectionException;
    import xidian.sl.stack.LinearNode;
    
    public class LinkedQueue<T> implements QueueADT<T> {
        //跟踪队列中的元素个数
        private int count;
        //指向首元素末元素的引用
        private LinearNode<T> front, rear;
        
        public LinkedQueue(){
            count = 0;
            front = rear = null;
        }
        
        /**
         * 实现dequeue操作时,确保至少存在一个可返回的元素,如果没有,就要抛出异常
         * @throws EmptyCollectionException 
         * */
        public T dequeue() throws EmptyCollectionException {
            if(isEmpty()){
                throw new EmptyCollectionException("queue");
            }
            
            T result = front.getElement();
            front = front.getNext();
            count--;
            //如果此时队列为空,则要将rear引用设置为null,front也为null,但由于front设置为链表的next引用,已经有处理
            if(isEmpty()){
                rear = null;
            }
            return result;
        }
    
        /**
         * enqueue操作要求将新元素放到链表的末端
         * 一般情况下,将当前某元素的next引用设置为指向这个新元素,并重新把rear引用设置为指向这个新添加的末元素,但是,如果队列
         * 目前为空,则front引用也要设置为指向这个新元素
         * */
        public void enqueue(T element) {
            LinearNode<T> node = new LinearNode<T>(element);
            
            if(isEmpty()){
                front = node;
            }else{
                rear.setNext(node);
            }
            rear = node;
            count++;
        }
    
        @Override
        public T first() {
            T result = front.getElement();
            return result;
        }
    
        @Override
        public boolean isEmpty() {
            return count == 0 ? true : false;
        }
    
        @Override
        public int size() {
            return count;
        }
    
    }

    这里使用到的LinearNode类与在栈中使用到的是一样的:

    package xidian.sl.stack;
    
    /**
     * 节点类,含有另个引用,一个指向链表的下一个LinearNode<T>节点,
     * 另一个指定本节点中存储的元素 
     * */
    public class LinearNode<T> {
        /*指向下一个节点*/
        private LinearNode<T> next;
        /*本节点存储的元素*/
        private T element;
        
        /*创建一个空的节点*/
        public LinearNode(){
            next = null;
            element = null;
        }
        
        /*创建一个存储了特殊元素的节点*/
        public LinearNode(T elem){
            next = null;
            element = elem;
        }
        
        /*返回下一个节点*/
        public LinearNode<T> getNext(){
            return next;
        }
        
        /*设置下一个节点*/
        public void setNext(LinearNode<T> node){
            next = node;
        }
    
        /*获得当前节点存储的元素*/
        public T getElement() {
            return element;
        }
        
        /*设置当前节点存储的元素*/
        public void setElement(T element) {
            this.element = element;
        }
        
        
        
    }

    2.数组实现队列:

    固定数组的实现在栈中是很高效的,是因为所有的操作(增删等)都是在集合的一端进行的,因而也是在数组的一端进行的,但在队列的实现中则不是这样,因为我们是在两端对队列进行操作的,因此固定数组的实现效率不高。

    分析:1.将队列的首元素总是存储在数组的索引0处,由于队列处理会影响到集合的两端,因此从队列中删除元素的时候,该策略要求移动元素,这样操作的效率就很低

    2.如果将队列的末端总是存储在数组索引0处,当一个元素要入列时,是在末端进行操作的,这就意味着,每个enqueue操作都会使得队列中的所有元素在数组中移动一位,效率仍然很低。

    3.如果不固定在哪一端,当元素出列时,队列的前端在数组中前移,当元素入列时,队列的末端也在数组中前移,当队列的末端到达了数组的末端将出现难题,此时加大数组容量是不切合实际的,因为这样将不能有效的利用数组的空闲空间。

    因此我们的解决方法是将数组看作是环形(环形数组)的,这样就可以除去在队列的数组实现,环形数组并不是一种新的结构,它只是一种把数组用来存储队列的方法,环形数组即数组的最后一个索引后面跟的是第一个索引。

    实现方案:用两个整数值来表示队列的前端和末端,当添加和删除元素时,这些值会改变。注意,front的值表示的是队列首元素存储的位置,rear的值表示的是数组的下一个可用单元(不是最后一个元素的存储位置),而由于rear的值不在表示队列的元素数目,因此我们需要使用一个单独的整数值来跟踪元素计数。

    由于使用环形数组,当队列的末端到达数组的末端时,它将“环绕”到数组的前端,因此,队列的元素可以跨越数组的末端。

    package xidian.sl.queue;
    
    import xidian.sl.stack.EmptyCollectionException;
    
    public class CircularArrayQueue<T> implements QueueADT<T>{
        //数组的默认容量大小
        private final int DEFAULT_CAPACITY = 100;
        private int front, rear, count;
        private T[] queue;
        
        @SuppressWarnings("unchecked")
        public CircularArrayQueue(){
            front = rear = count = 0;
            queue = (T[]) new Object[DEFAULT_CAPACITY];
        }
        
        @SuppressWarnings("unchecked")
        public CircularArrayQueue(int initialCapacity){
            front = rear = count = 0;
            queue = (T[]) new Object[initialCapacity];
        }
        
        /**
         * 一个元素出列后,front的值要递减,进行足够的dequeue操作后,front的值将到达数组的最后一个索引处,当最大索引处的元素被
         * 删除后,front的值必须设置为0而不是递减,在enqueue操作中用于设置rear值的计算,也可以用来设置dequeue操作的front值
         * */
        public T dequeue() throws EmptyCollectionException {
            if(isEmpty()){
                throw new EmptyCollectionException("queue");
            }
            T result = queue[front];
            queue[rear] = null;
            front = (front + 1) % queue.length;
            count--;
            return result;
        }
    
        /**
         * enqueue操作:通常,一个元素入列后,rear的值要递增,但当enqueue操作填充了数组的最后一个单元时,
         * rear必须设为0,表面下一个元素应该存储在索引0处,下面给出计算rear值的公式:
         * rear = (rear + 1) % queue.length;(queue是存储队列的数组名)
         * */
        public void enqueue(T element) {
            //首先查看容量,必要时进行扩容
            if(size() == queue.length){
                expandCapacity();
            }
            queue[rear] = element;
            rear = (rear + 1) % queue.length;
            count++;
        }
    
        @Override
        public T first() throws EmptyCollectionException {
            if(isEmpty()){
                throw new EmptyCollectionException("queue");
            }
            return queue[front];
        }
    
        @Override
        public boolean isEmpty() {
            return count == 0 ? true : false;
        }
    
        @Override
        public int size() {
            return count;
        }
        
        /**
         * 当数组中的所有单元都已填充,就需要进行扩容,
         * 注意:已有数组的元素必须按其在队列中的正确顺序(而不是它们在数组中的顺序)复制到新数组中
         * */
        @SuppressWarnings("unchecked")
        private void expandCapacity(){
            //增加为原容量的2倍
            T[] larger = (T[]) new Object[queue.length*2];
            //新数组中从索引0处开始按队列的正确顺序进行填充元素
            for(int scan = 0; scan < count; scan++){
                larger[scan] = queue[front];
                front = (front + 1) % queue.length;
            }
            //重新定位front,rear,queue
            front = 0;
            rear = count;
            queue = larger;
        }
    }


    队列的应用实例:

    1.代码密钥:凯撒加密法是一种简单的消息编码方式,它是按照字母表将消息中的每个字母移动常量的k位,但这种方式极易破解,因为字母的移动只有26种可能。

    因此我们使用重复密钥:这是不是将每个字母移动常数位,而是利用一个密钥值列表,将各个字母移动不同的位数。如果消息比密钥值长,可以从头再使用这个密钥值列表;

    package xidian.sl.queue;
    
    import xidian.sl.stack.EmptyCollectionException;
    
    public class Codes {
        public static void main(String[] args) throws EmptyCollectionException {
            //消息的密钥
            int[] key = {5, 12, -3, 8, -9, 4, 10};
            Integer keyValue;
            String enCoded = "", deCoded = "";
            //待加密的字符串
            String message = "All programmers are playWrights and all computers are lousy actors";
            //用于存储密钥的队列
            CircularArrayQueue<Integer> keyQueue1 = new CircularArrayQueue<Integer>();
            CircularArrayQueue<Integer> keyQueue2 = new CircularArrayQueue<Integer>();
            
            //两个队列分别存储一份密钥,模拟消息编码者使用一份密钥,消息解码者使用一份密钥
            for(int scan = 0; scan < key.length; scan++){
                keyQueue1.enqueue(new Integer(key[scan]));
                keyQueue2.enqueue(new Integer(key[scan]));
            }
            //利用队列存储密钥使得密钥重复很容易,只要在用到每个密钥值后将其放回到队列即可
            for(int scan = 0; scan < message.length(); scan++){
                //取一个密钥
                keyValue = keyQueue1.dequeue();
                //会将该字符移动Unicode字符集的另外一个位置
                enCoded += (char)((int)message.charAt(scan) + keyValue.intValue());
                //将密钥重新存储到队列中
                keyQueue1.enqueue(keyValue);
            }
            System.out.println("Encoded Message:\n"+enCoded+"\n");
            
            for(int scan = 0; scan < enCoded.length(); scan++){
                keyValue = keyQueue2.dequeue();
                deCoded += (char)((int)enCoded.charAt(scan) - keyValue.intValue());
                keyQueue2.enqueue(keyValue);
            }
            System.out.println("Decoded Message:\n"+deCoded);
            
        }
    }

    运行结果:

    2.利用队列的保存顺序特性,模拟售票口

    考虑去银行办业务:一般来说,服务窗口越多,队走的越快,银行经理希望顾客满意,但又不希望雇佣过多的员工。

    我们模拟的服务窗口有如下假设:

    1.只排一队,并且先到的人先得到服务(这是一个队列)

    2.平均每隔15秒就会来一位顾客

    3.如果有空闲的窗口,在顾客抵达之时就会马上处理

    4.从顾客来到窗口到处理完顾客请求,这个平均需要120秒

    以下就来模拟高峰期银行开多少个窗口最为合适:

    先模拟一个顾客类:

    package xidian.sl.queue;
    
    public class Costomer {
        //arrivalTime跟踪顾客抵达售票口的时间,departureTime跟踪顾客买票后离开售票口的时间
        private int arrivalTime, departureTime;
        
        public Costomer(int arrives){
            arrivalTime = arrives;
            departureTime = 0;
        }
    
        public int getArrivalTime() {
            return arrivalTime;
        }
    
        public void setArrivalTime(int arrivalTime) {
            this.arrivalTime = arrivalTime;
        }
    
        public int getDepartureTime() {
            return departureTime;
        }
    
        public void setDepartureTime(int departureTime) {
            this.departureTime = departureTime;
        }
        //顾客买票所花的总时间就是离开时间-抵达时间
        public int totalTime(){
            return (departureTime-arrivalTime);
        }
        
    }

    模拟类:

    package xidian.sl.queue;
    
    import xidian.sl.stack.EmptyCollectionException;
    
    
    public class TicketCounter {
        //接受服务的时间
        private static int PROCESS = 120;
        //最多窗口数
        private static int MAX_CASHIERS = 10;
        //顾客的数量
        private static int NUM_CUSTOMERS = 100;
        
        public static void main(String[] args) throws EmptyCollectionException {
            Costomer costomer;
            //存储顾客的队列
            LinkedQueue<Costomer> costomerQueue = new LinkedQueue<Costomer>();
            int[] cashierTime = new int[MAX_CASHIERS];
            int totalTime, averageTime, departs;
            //该循环决定了每遍模拟时用了多少个售票口
            for(int cashiers = 0; cashiers < MAX_CASHIERS; cashiers++){
                //将售票口的服务时间初始化为 0
                for(int count = 0; count < cashiers; count++){
                    cashierTime[count] = 0;
                }
                //往costomerQueue存储顾客,模拟每隔15分钟来一个顾客
                for(int count = 1; count <= NUM_CUSTOMERS; count++){
                    costomerQueue.enqueue(new Costomer(count*15));
                }
                //初始化总的服务时间为0
                totalTime = 0;
                //开始服务
                while(!(costomerQueue.isEmpty())){
                    for(int count = 0; count <= cashiers; count++){
                        if(!(costomerQueue.isEmpty())){
                            //取出一位顾客
                            costomer = costomerQueue.dequeue();
                            //顾客来的时间与售票口的服务时间相比
                            if(costomer.getArrivalTime() > cashierTime[count]){
                                //表示空闲,可以进行服务
                                departs = costomer.getArrivalTime() + PROCESS;
                            }else{
                                //无空闲则需排队等待
                                departs = cashierTime[count] + PROCESS;
                            }
                            //保存用户的离开时间
                            costomer.setDepartureTime(departs);
                            //设置该售票口的服务时间
                            cashierTime[count] = departs;
                            //计算总的服务时间
                            totalTime += costomer.totalTime();
                        }
                    }
                }
                
                averageTime = totalTime / NUM_CUSTOMERS;
                System.out.println("售票口数量: "+(cashiers+1));
                System.out.println("平均时间: "+averageTime+"\n");
                
            }
        }
        
    }

    模拟结果:

    由模拟结果可知最合适是开8个窗口

  • 相关阅读:
    让所有IE支持HTML5的解决方案
    MVC3实现多个按钮提交
    水平垂直居中
    模块化开发之sea.js实现原理总结
    express常见获取参数的方法
    使用nodemon提高nodejs调试效率
    Vue2 实践揭秘 错误列表
    全局安装 Vue cli3 和 继续使用 Vue-cli2.x
    使用VUECLI3
    npm ERR! code Z_BUF_ERROR
  • 原文地址:https://www.cnblogs.com/shenliang123/p/2913552.html
Copyright © 2020-2023  润新知