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