隊列的實現與應用


隊列是一種線性集合,其元素一端加入,從另一端刪除,因此我們說隊列元素是按先進先出(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個窗口

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM