讀古今文學網 > 算法神探:一部谷歌首席工程師寫的CS小說 > 5 對一艘走私船的二分搜索 >

5 對一艘走私船的二分搜索

Usb港看起來就像是一個漁村。遠遠地看去,在碼頭的盡頭矗立著一群破舊的建築。每當有船隻抵港時,這裡才會有一些零星的活動,平時這個小鎮顯得非常寧靜。

Frank直接向Crab』s Pinch走去,那是一個漁夫酒吧,以蛤礪海鮮濃湯和每週三晚上的船夫號子大賽而出名。如果幸運的話,今天他的一個老朋友將會出現這裡。畢竟,Crab』s Pinch是Usb港唯一一個可以去的地方。現在,Frank選擇後排角落裡的一個桌子坐下了,點了一份海鮮濃湯等待著。

不久前一個名叫Mavis的走私犯來過這個潮濕的小酒吧。由於天生小心謹慎,Mavis從來沒有被抓到過。不過眾所周知的是,她曾經不惜燒燬她的船以銷毀證據。但Frank與她私交甚好,至少在Frank離開警隊後,他們會交換一些零碎的信息。

Frank對著他的濃湯度過了漫長的一個小時後,終於看到了Mavis,他推開碗向Mavis打了一聲招呼。Mavis在門口猶豫了一會兒,擠過人群進入酒吧。

「Mavis,」當她走到這個角落後,Frank說道,「你好嗎?」

「十分鐘之前比現在好多了!」她生氣地啐出這幾個字。

Frank還沒來得及問任何問題,Notation警官就大步跨進了門,她舉著手大聲說道:「女士們先生們,大家請注意!兩天前的晚上有一輛馬車路過這裡。」

Frank低聲咒罵著,他受夠了她的這種官腔。

「我從天沒亮就跑來這,本希望能有一碗熱乎乎的海鮮湯和幾分鐘的安靜,卻看到了警察在找什麼笨蛋馬車!」Mavis抱怨道。

Frank笑了笑:「所以在她走掉之前,你都不能卸貨,對吧?」

Mavis瞪了Frank一眼,但也沒有什麼可以反駁的。Usb港從來就沒有人是靠捕漁和航運來賺錢的。這個港口對於罪犯來說的確很有吸引力,他們可以把心思都花在運送貨品上,而無須應付那些「多管閒事」的政府官員。Frank敢用他一個月的租金打賭,在這個碼頭沒有一條船是不走私的。

「你知道什麼有關那輛馬車的事情嗎?」Frank輕聲說。

Mavis聳聳肩:「這裡的碼頭上總有馬車。這裡是一個港口,Frank,人們要運送貨物。」

「這是一輛特殊的馬車,」Frank追問,「馬車上有一串獨立的可以存放動物的棚,就像一個裝在輪子上的巨大的數組。」

「聽起很高級的樣子,」Mavis說,「可我從沒聽說過有什麼動物被運送。倒是聽說過關於運送一些箱子或者兩隻小烏龜的傳聞,但沒什麼東西大到需要用棚來運送。你確定它經過這裡?」

Frank點點頭。那個馬車的味道就像是廁所裡的魚味空氣清新劑,有時候聞起來就像Usb港的味道一樣差勁。

「那這幾天有船離開過這裡嗎?」Frank問。這些小偷把偷來的文件運送到這麼遠的地方,他們不會一直傻等著的。

「只有Retry Loop這艘船離開過,」Mavis說,「我只能告訴你這些,因為這是公開的。但是我並不知道它裝了些什麼,我也不關心。」

「那你知道它什麼時候回來的嗎?」Frank問。

「19個小時以前回到港口的,」Mavis回答,「我還是不知道它裝了些什麼。」

Frank大笑著說:「看來我應該去鎮上逛一逛了。」

Mavis敷衍地笑了笑後向服務員打了個手勢。

Frank沿著碼頭走了不到20米,Notation警官就跑過去跟上了他。

「Runtime先生,我在調查一個案子,」她開口道,「如果你有關於……」

Frank停了下來,使得她也猛地停住腳步。「Notation警官,你在調查什麼?」Frank問道。

Notation嘴唇動了幾下,沒有發出任何聲音,但此時她的脖子突然變得通紅。

Frank又問:「警長並不知道你在這裡,對嗎?你在這裡調查並沒有得到警長的許可。」

「你是什麼意思?」Notation警官反駁道,但是Frank打斷了她。

「別裝了,」Frank說,「你單獨來到這裡這一事實就我的證據。你在擅自進行調查。可問題是你為什麼要調查此案呢?」

此刻,Notation警官變得越發面紅耳赤。

「這不是你該關心的事。」她說。

「警長能來找我,就說明他已經不相信自己的人了。」Frank冷靜地回答。

「警長竟然聘請了你這種過氣的偵探?」

「是的,因為他相信我。」

Notation警官板起臉,眼裡露出怒氣。不一會兒,Frank甚至覺得她也許會用她的警棍來結束這次談話。但是她很快就消氣了,就像她來氣時一樣快。

「我需要找到那些文件,」她傷心地說,「這是我的錯——那晚是我值班。」

「明白了。」Frank若有所思地說。

「我必須找到那些文件,」Notation警官又重複了一遍,聽起來有點激動,「我剛到警隊才幾個月,並且……」

Frank打斷了她,並給了她一個安慰的微笑,他料到是這樣的。菜鳥們很少能夠處理好他們犯下的第一個錯誤,而Notation看起來又比大部分人遇到的麻煩更大。「我們正在尋找Retry Loop,」他說,「Crannock夫婦的馬車在那個搶劫的夜晚卸下了什麼東西,船在19個小時之前已經回來了。」

當然,Frank並不相信她,不過他希望能和她走近一點兒,密切關注她。她所掌握的Crannock夫婦的情況比她寫在報告裡的東西要多得多。有一些東西在她的報告中沒有提及,Frank必須知道她還知道些什麼。

「我們最好馬上開始,」Notation苦惱地看著碼頭說道,「有好多船都需要檢查。我們是從頭開始嗎?」港口大部分的船都屬於走私犯船,它們很難被區分出來,這些船的名字可能需要挨個去問。

「我們有更好的方法,」Frank解釋道,「這裡的海關長是個排序狂,他堅持要把港口停泊的船隻按照它們到港時間排序。新抵達的船會被安排到一個最接近小鎮的船位,這樣可以讓船員方便地裝載和卸貨,如果又有新抵達的船,就讓所有的船都往後調整,讓出最前方的位置。」

「真是可笑,」Notation說道,「多浪費力氣!他為什麼要這樣做?」

Frank笑了笑說:「他聲稱這是為了效率,但任何在Usb港待了一個星期的人都知道真相。這個海關長受不了腐爛的魚臭味。港口裡那些貨物沒有全部售完的船,唉……『香氣四溢』。海關長的目的就是要讓在港口停留時間長一些的船隻遠離他的辦公室。」

Notation警官瞪著他問道:「你是認真的?」

Frank又笑道:「是的,如果你巡邏過,你也會獲得這種有用的信息。現在的關鍵是我們知道了這裡的船是按到港的時間順序排列的,並且Retry Loop是19小時前抵達的,所以我們就可以簡單地做一個二分搜索。

「我們的目標值是19,我們的算法是二分搜索。現在搜索空間是整列船隻,所以我們已經有了上界和下界,如果我們使用閉合區間,那麼我們的下界是第一艘船,上界是最後一艘船。如果Retry Loop在這裡,很明顯它不會在第一艘船之前,也不會在最後一艘船之後。

「現在我們從中間的那艘船開始,詢問它是何時到港的,如果它的到港時間不足19個小時,那它肯定在Retry Loop之前。這樣可以將我們的搜索分為兩塊,然後……」

「如果它是19個小時以前抵達的,則它一定在Retry Loop之後,」Notation搶在Frank之前說,「我知道二分搜索,我的警用算法課的期末考試就在兩個半月之前。」

確定搜索算法後,他們倆就動身去找Retry Loop。中間的船是一艘聞起來有股怪香蕉味的黃色帆船,它是17個小時前抵達的。

這意味著他們可以排除前面一半的船隻了,包括中間這艘。Frank將下界調整為黃色帆船之後的那艘船。

搜索空間縮小後,他們選擇了一個新的中點。他們花了好一段時間才讓這個船長相信他們不是海關的臥底。十分鐘之後,Notation將她的徽章推到了船長的眼前,船長的語氣立即變得憤怒而抱怨,他說他的船Corrupt Packet已經被困在這個港口22個小時了,要求他們代表他和海關長談談。

因為目標是19個小時,所以他們知道Retry Loop是在Corrupt Packet之前抵達。他們又一次改變了界限,所以現在Corrupt Packet左邊的船是新的上界。

只剩下兩艘船在搜索範圍內了,搜索即將結束。如果這兩艘船都不是Retry Loop,他們就能確定它已經離開了港口,因為一旦搜索空間沒有更多的元素,他們就可以排除整個搜索空間。

因為現在只剩下兩艘船,他們可以選擇其中任意一個作為中點,根據直覺,Frank選擇了早些抵達的船,也就是它們中的下界。與一個在碼頭閒逛的水手進行簡單對話後,他們確定這艘船就是Retry Loop,它已經抵達19個小時了。

「現在怎麼辦?」他們看著那艘船,Notation問道。

「我們要用你那枚閃亮的徽章。」Frank回答道。

警用算法導論:二分搜索Ⅰ

節選自Drecker教授講義

二分搜索算法用於高效地在有序數組A中查找一個目標值v。和線性搜索不同,二分搜索利用數據結構中的信息讓搜索更高效。高效算法的關鍵是信息。在下面的例子中,我們就要使用數組是按照升序排序的這個信息:

對於一對索引i和j,如果i<j,則有A[i]≤a[j]

這看起來並不是很多的信息,但是它足夠讓我們的搜索更加高效。

二分搜索算法的工作原理是:不斷地將搜索空間分成兩半,並且把搜索空間限制在其中的一半中。這個算法通過改變上下界限有效地限制了搜索空間。上界(IndexHigh)標記了搜索空間有效數組中最高的索引,下界(IndexLow)標記了搜索空間有效數組中最低的索引。通過這個算法,如果目標值在這個數組中,就可以保證:

在搜索的每一步中,我們只需依次判定上界和下界中間的值:

接下來,我們將中間值A[IndexMid]和目標值v進行比較。如果中間值小於目標值(A[IndexMid]<v),我們就知道目標值一定介於這個中間值之後。這樣我們可以將IndexLow改為IndexMid+1來使搜索空間又變成原來的一半。

如果中間值比目標值大(A[IndexMid]>v),我們就知道目標一定位於中間值之前,於是我們將IndexHigh改為IndexMid-1來使得搜索空間變成原來的一半。

當然,如果我們發現A[IndexMid]等於v,我們可以直接結束搜索,找到目標值。

現在我們就來使用二分搜索在下面這個已排序的數組中尋找到15。虛線框出的方塊是算法當前需要判定的值,而被陰影遮住的部分則是在搜索中被排除的部分。

第一個被判定的中點的值是11,比我們的目標值15小。因為我們知道這個數組是按照升序排列的,所以可以排除中點及其之前的所有部分。我們將下界索引移動到適當的地方(IndexLow=IndexMid+1)。

在第二次比較之後,我們發現中點值是52,比目標值大。我們可以排除中點和它之後的所有部分。此時需要移動我們上界(IndexHigh=IndexMid-1)。

請注意,通過這幾次的操作,此時雖然下界已經是目標值了(v=15),但是我們仍需要繼續搜索,直到中間值指向目標值。這是因為二分搜索是對中間值進行判定,而不會判定上界和下界是否是目標值。

如果目標值不在數組中會發生什麼呢?在搜索過程中,上下界之間的距離會越來越近,直到它們之間沒有任何還未檢查過的值。因為我們總是將其中一個界限移動到中間值的另一邊,所以我們可以在IndexHigh<Indexlow的時候停下來,這個時候就可以保證目標值不在數組中了。