坐在馬桶上看演算法(5):佇列

啊哈磊發表於2016-05-09

新學期開始了,小哈是小哼的新同桌(小哈是個小美女哦~),小哼向小哈詢問QQ號,小哈當然不會直接告訴小哼啦,原因嘛你懂的。所以小哈給了小哼一串加密過的數字,同時小哈也告訴了小哼解密規則。

規則是這樣的:首先將第1個數刪除,緊接著將第2個數放到這串數的末尾,再將第3個數刪除並將第4個數再放到這串數的末尾,再將第5個數刪除……直到剩下最後一個數,將最後一個數也刪除。按照剛才刪除的順序,把這些刪除的數連在一起就是小哈的QQ啦。現在你來幫幫小哼吧。小哈給小哼加密過的一串數是“6 3 1 75 8 9 2 4”。

103006pjh63j66hu65bss8.png

OK,現在輪到你動手的時候了。快去找出9張便籤或小紙片,將“6 3 1 75 8 9 2 4”這9個數分別寫在9張便籤上,模擬一下解密過程。如果你沒有理解錯解密規則的話,解密後小哈的QQ號應該是“6 1 5 94 7 2 8 3”。

其實解密的過程就像是將這些數“排隊”。每次從最前面拿兩個,第1個扔掉,第2個放到尾部。具體過程是這樣的:剛開始這串數是“6 3 1 75 8 9 2 4”,首先刪除6並將3放到這串數的末尾,這串數更新為“1 7 5 89 2 4 3”。接下來刪除1並將7放到末尾,即更新為“5 8 9 24 3 7”。再刪除5並將8放到末尾即“9 2 4 3 7 8”,刪除9並將2放到末尾即“4 3 7 8 2”,刪除4並將3放到末尾即“7 8 2 3”,刪除7並將8放到末尾即“2 3 8”,刪除2並將3放到末尾即“8 3”,刪除8並將3放到末尾即“3”,最後刪除3。因此被刪除的順序是“6 1 5 9 4 7 2 8 3”,這就是小哈的QQ號碼了,你可以加她試試看^_^。

既然現在已經搞清楚瞭解密法則,不妨自己嘗試一下去程式設計,我相信你一定可以寫出來的。

首先需要一個陣列來儲存這一串數即intq[101]。並初始化這個陣列即intq[101]={0,6,3,1,7,5,8,9,2,4};(此處初始化是我多寫了一個0,用來填充q[0],因為我比較喜歡從q[1]開始用,對陣列初始化不是很理解的同學可以去看下我的上一本書《啊哈C!思考快你一步》)接下來就是模擬解密的過程了。

解密的第一步是將第一個數刪除,你可以想一下如何在陣列中刪除一個數呢?最簡單的方法是將所有後面的數都往前面挪動一位,將前面的數覆蓋。就好比我們在排隊買票,最前面的人買好離開了,後面所有的人就需要全部向前面走一步,補上之前的空位,但是這樣的做法很耗費時間。

102428gtp00uypprjufj39.png

在這裡,我將引入兩個整型變數head和tail。head用來記錄佇列的隊首(即第一位),tail用來記錄佇列的隊尾(即最後一位)的下一個位置。你可能會問為什麼tail不直接記錄隊尾,卻要記錄隊尾的下一個位置呢?這是因為當佇列當中只剩下一個元素時,隊首和隊尾重合會帶來一些麻煩。我們這裡規定隊首和隊尾重合時,佇列為空。

現在有9個數,9個數全部放入佇列之後head=1;tail=10;此時head和tail之間的數就是目前佇列中“有效”的數。如果要刪除一個數的話,就將head++就OK了,這樣仍然可以保持head和tail之間的數為目前佇列中“有效”的數。這樣做雖然浪費了一個空間,卻節省了大量的時間,這是非常划算的。新增加一個數也很簡單,把需要增加的數放到隊尾即q[tail]之後再tail++就歐克啦。

102428a63ucg6jcnrxoucz.png

我們來小結一下,在隊首刪除一個數的操作是head++;

  • 在隊尾增加一個數(假設這個數是x)的操作是q[tail]=x;tail++;
  • 整個解密過程,請看下面這個霸氣外漏的圖。
102428ndqkip9frf2ks2dd.png

最後的輸出就是6 1 5 94 7 2 8 3,程式碼實現如下。

怎麼樣上面的程式碼執行成功沒有?現在我們再來總結一下佇列的概念。佇列是一種特殊的線性結構,它只允許在佇列的首部(head)進行刪除操作稱之為“出隊”,而在佇列的尾部(tail)進行插入操作稱之為“入隊”。當佇列中沒有元素時(即head==tail),稱為空佇列。在我們的日常生活中有很多情況都符合佇列的特性。比如我們之前提到過的買票,每個排隊買票的視窗就是一個佇列。在這個佇列當中,新來的人總是站在佇列的最後面,來的越早的人越靠前也就越早能買到票,就是先來的人先服務,我們稱為“先進先出”(First InFirst Out,FIFO)原則。

佇列將是我們今後學習廣度優先搜尋以及佇列優化的Bellman-Ford最短路演算法的核心資料結構。所以現在將佇列的三個基本元素(一個陣列,兩個變數)封裝為一個結構體型別,如下。

上面我們定義了一個結構體型別,我們通常將其放在main函式的外面,請注意結構體的定義末尾有個;號。struct是結構體的關鍵字,queue是我們為這個結構體起的名字。這個結構體有三個成員分別是:整型陣列data、整型head和整型tail。這樣我們就可以把這三個部分放在一起作為一個整體來對待。你可以這麼理解:我們定義了一個新的資料型別,這個新型別非常強大,用這個新型別定義出的每一個變數可以同時儲存一個整型陣列和兩個整數。

102907p6qieaify6wt6wy6.png

有了新的結構體型別,如何定義結構體變數呢?很簡單,這與我們之前定義變數的方式是一樣,如下。

請注意struct queue需要整體使用,不能直接寫queue q;這樣我們就定義了一個結構體變數q。這個結構體變數q就可以滿足佇列的所有操作了。那又該如何訪問結構體變數的內部成員呢?可以使用.號,它叫做成員運算子或者點號運算子,如下:

好了,下面這段程式碼就是使用結構體來實現的佇列操作。

上面的這種寫法看起來雖然冗餘了一些,但是可以加強你對佇列這個演算法的理解。C++的STL庫已經有佇列的實現,有興趣的同學可以參看相關材料。佇列的起源已經無法追溯。在還沒有數字計算機之前,數學應用中就已經有對佇列的記載了。

我們生活中佇列的例子也比比皆是,比如排隊買票,又或者吃飯時候用來排隊等候的叫號機,又或者撥打銀行客服選擇人工服務時,每次都會被提示“客服人員正忙,請耐心等待”,因為客服人員太少了,撥打電話的客戶需要按照打進的時間順序進行等候等等。這裡表揚一下肯德基的宅急送,沒有做廣告的嫌疑啊,每次一打就通,基本不需要等待。但是我每次打銀行的客服(具體是哪家銀行就不點名了)基本都要等待很長時間,總是告訴我“正在轉接,請稍後”,嘟嘟嘟三聲後就變成“客服正忙,請耐心等待!”然後就給我放很難聽的曲子。看來錢在誰那裡誰就是老大啊……

相關文章