做者:July。致謝:caopengcs、胡果果。
時間:二零一三年九月七日。
html
寫博的近三年,整理了太多太多的筆試面試題,如微軟面試100題系列,和眼下這個程序員編程藝術系列,真心以爲題目年年變,但解決問題的方法永遠都是那幾種,用心準備後,自會發現一切有跡可循。程序員
故爲更好的幫助人們找到工做,特准備在北京舉辦一系列面試&算法講座。時間定爲週末,每次一個上午或下午,受衆對象爲要找工做或換工做或對算法感興趣的朋友,費用前期暫願交就交,交多少全由本身決定。主講人:我和目前zoj排名第一的caopengcs博士。9月15日爲第1次講座:http://weibo.com/1580904460/A8N6oAFZ4?mod=weibotime。面試
OK,切入正題。上面說整理過不少筆試面試題,但好的筆試面試題真心難求,包括在編程藝術系列每一章的選題,越到後面越難挑,而本文寫兩個跟實際掛鉤的問題,它們來自此文http://blog.csdn.net/v_july_v/article/details/7974418 的第3.6題,和第87題,即算法
題目分析:本題來源於去年2012年百度的一套實習生筆試題中的系統設計題( 爲尊重願題,本章主要使用百度搜索引擎展開論述,而不是google等其它搜索引擎,但原理不會差太多。然脫離本題,平時搜的時候,鼓勵用...),題目比較開放,考察的目的在於看應聘者解決問題的思路是否清晰明確,其次即是看能考慮到多少細節。
Trie樹,即字典樹,又稱單詞查找樹或鍵樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計和排序大量的字符串(但不只限於字符串),因此常常被搜索引擎系統用於文本詞頻統計。它的優勢是:最大限度地減小無謂的字符串比較,查詢效率比哈希表高。sql
Trie的核心思想是空間換時間。利用字符串的公共前綴來下降查詢時間的開銷以達到提升效率的目的。
它有3個基本性質:
數據庫
當每一個搜索引擎輸入一個前綴時,下面它只會展現0~10個候選詞,但如果碰到那種候選詞不少的時候,如何取捨,哪些展現在前面,哪些展現在後面?這就是一個搜索熱度的問題。編程
如本題描述所說,在去年的這個時候,當我在搜索框內搜索「北京」時,它下面會提示以「北京」爲前綴的諸如「北京愛情故事」,「北京公交」,「北京醫院」,且「 北京愛情故事」展現在第一個:數據結構
爲什麼輸入「北京」,會首先提示「北京愛情故事」呢?由於去年的這個時候,正是《北京愛情故事》這部電影上映正火的時候(其上映日期爲2012年1月8日,火了至少一年),那個時候你們都一個勁的搜索這部電影的相關信息,當10我的中輸入「北京」後,其中有8我的會繼續敲入「愛情故事」(連起來就是「北京愛情故事」)的時候,搜索引擎對此固然不會無動於衷。nosql
也就是說,搜索引擎知道了這個時間段,你們都在瘋狂查找北京愛情故事,故當用戶輸入以「北京」爲前綴的時候,搜索引擎猜想用戶有80%的機率是要查找「北京愛情故事」,故把「北京愛情故事」在下面提示出來,並放在第一個位置上。ide
但爲什麼今年這個時候再次搜索「北京」的時候,它展現出來的詞不一樣了呢?
緣由在於隨着時間變化,人們對北京愛情故事這部影片的關注度逐漸降低,與此同時,又出現了新的熱詞,新的電影,故如今雖然一樣是輸入「北京」,後面提示的詞也相應跟着起了變化。那解決這個問題的辦法是什麼呢?如開頭所說:按期分析某段時間內的人們搜索的關鍵詞,統計出搜索次數比較多的熱詞,繼而當用戶輸入某個前綴時,優先展現熱詞。
故說白了,這個問題的第二個步驟即是統計熱詞,咱們把統計熱詞的方法稱爲TOP K算法,此算法的應用場景即是此文http://blog.csdn.net/v_july_v/article/details/7382693中的第2個問題,再次原文引用:
「尋找熱門查詢,300萬個查詢字符串中統計最熱門的10個查詢
原題:搜索引擎會經過日誌文件把用戶每次檢索使用的全部檢索串都記錄下來,每一個查詢串的長度爲1-255字節。假設目前有一千萬個記錄(這些查詢串的重複度比較高,雖然總數是1千萬,但若是除去重複後,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就是越熱門),請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。
解答:由上面第1題,咱們知道,數據大則劃爲小的,如一億個Ip求Top 10,可先%1000將ip分到1000個小文件中去,並保證一種ip只出如今一個文件中,再對每一個小文件中的ip進行hashmap計數統計並按數量排序,最後歸併或者最小堆依次處理每一個小文件的top10以獲得最後的結果。
但若是數據規模自己就比較小,能一次性裝入內存呢?好比這第2題,雖然有一千萬個Query,可是因爲重複度比較高,所以事實上只有300萬的Query,每一個Query255Byte,所以咱們能夠考慮把他們都放進內存中去(300萬個字符串假設沒有重複,都是最大長度,那麼最多佔用內存3M*1K/4=0.75G。因此能夠將全部字符串都存放在內存中進行處理),而如今只是須要一個合適的數據結構,在這裏,HashTable絕對是咱們優先的選擇。
因此咱們放棄分而治之/hash映射的步驟,直接上hash統計,而後排序。So,針對此類典型的TOP K問題,採起的對策每每是:hashmap + 堆。以下所示:
別忘了這篇文章中所述的堆排序思路:‘維護k個元素的最小堆,即用容量爲k的最小堆存儲最早遍歷到的k個數,並假設它們便是最大的k個數,建堆費時O(k),並調整堆(費時O(logk))後,有k1>k2>...kmin(kmin設爲小頂堆中最小元素)。繼續遍歷數列,每次遍歷一個元素x,與堆頂元素比較,若x>kmin,則更新堆(x入堆,用時logk),不然不更新堆。這樣下來,總費時O(k*logk+(n-k)*logk)=O(n*logk)。此方法得益於在堆中,查找等各項操做時間複雜度均爲logk。’--第三章續、Top K算法問題的實現。
固然,你也能夠採用trie樹,關鍵字域存該查詢串出現的次數,沒有出現爲0。最後用10個元素的最小推來對出現頻率進行排序。」
相信,如此,也就不難理解開頭所提出的方法了:Trie樹+ TOP K「hashmap+堆,hashmap+堆 統計出如10個近似的熱詞,也就是說,只存與關鍵詞近似的好比10個熱詞」。
並且你之後就能夠告訴你身邊的夥伴們,爲什麼輸入「結構之」,會提示出來一堆以「結構之」爲前綴的詞拉:
方法貌似成型了,但有哪些須要注意的細節呢?如@江申_Johnson所說:「實際工做裏,好比當前綴很短的時候,候選詞不少的時候,查詢和排序性能可能有問題,也許能夠加一層索引trie(這層索引能夠只索引頻率高於某一個閾值的詞,很短的時候查這個就能夠了。數量不夠的話再去查索引了所有詞的trie樹);並且有時候不能根據query頻率來排,而要引導用戶輸入信息量更全面的query,或者或不只僅是前綴匹配這麼簡單。」
除了上文提到的trie樹,三叉樹或許也是一個不錯的解決方案:http://igoro.com/archive/efficient-auto-complete-with-a-ternary-search-tree/。此外,StackOverflow上也有兩個討論帖子,你們能夠看看:①http://stackoverflow.com/questions/2901831/algorithm-for-autocomplete,②http://stackoverflow.com/questions/1783652/what-is-the-best-autocomplete-suggest-algorithm-datastructure-c-c。
題目詳情:找一個點集中與給定點距離最近的點,同時,給定的二維點集都是固定的,查詢可能有不少次,時間複雜度O(n)沒法接受,請設計數據結構和相應的算法。
題目分析:此題是去年微軟的三面題,相似於一朋友@陳利人 出的這題:附近地點搜索,就是搜索用戶附近有哪些地點。隨着GPS和帶有GPS功能的移動設備的普及,附近地點搜索也變得煊赫一時。在龐大的地理數據庫中搜索地點,索引是很重要的。可是,咱們的需求是搜索附近地點,例如,座標(39.91, 116.37)附近500米內有什麼餐館,那麼讓你來設計,該怎麼作?
假定只容許你初中數學知識,那麼你可能建一個X-Y座標系,即以座標(39.91, 116.37)爲圓心,以500的長度爲半徑,畫一個園,而後一個一個座標點的去查找。此法看似可行,但複雜度可想而知,即使你自覺得聰明的說把整個平面劃分爲四個象限,一個一個象限的查找,此舉雖然優化程度不夠,但也說明你一步步想到點子上去了。
即不一個一個座標點的查找,而是一個一個區域的查找,相對來講,其平均查找速度和效率會顯著提高。如此,便天然而然的想到了有沒有一種一次查找定位於一個區域的數據結構呢?
若看過博客內以前介紹R樹的這篇文章http://blog.csdn.net/v_JULY_v/article/details/6530142#t2 的讀者立馬便能意識到,R樹就是解決這個區域查找繼而不斷縮小規模的問題。特直接引用原文:
R樹是B樹在高維空間的擴展,是一棵平衡樹。每一個R樹的葉子結點包含了多個指向不一樣數據的指針,這些數據能夠是存放在硬盤中的,也能夠是存在內存中。根據R樹的這種數據結構,當咱們須要進行一個高維空間查詢時,咱們只須要遍歷少數幾個葉子結點所包含的指針,查看這些指針指向的數據是否知足要求便可。這種方式使咱們沒必要遍歷全部數據便可得到答案,效率顯著提升。下圖1是R樹的一個簡單實例:
咱們在上面說過,R樹運用了空間分割的理念,這種理念是如何實現的呢?R樹採用了一種稱爲MBR(Minimal Bounding Rectangle)的方法,在此我把它譯做「最小邊界矩形」。從葉子結點開始用矩形(rectangle)將空間框起來,結點越往上,框住的空間就越大,以此對空間進行分割。有點不懂?不要緊,繼續往下看。在這裏我還想提一下,R樹中的R應該表明的是Rectangle(此處參考wikipedia上關於R樹的介紹),而不是大多數國內教材中所說的Region(不少書把R樹稱爲區域樹,這是有誤的)。咱們就拿二維空間來舉例。下圖是Guttman論文中的一幅圖:
我來詳細解釋一下這張圖。
我想你們都應該理解這個數據結構的特徵了。用地圖的例子來解釋,就是全部的數據都是餐廳所對應的地點,先把相鄰的餐廳劃分到同一塊區域,劃分好全部餐廳以後,再把鄰近的區域劃分到更大的區域,劃分完畢後再次進行更高層次的劃分,直到劃分到只剩下兩個最大的區域爲止。要查找的時候就方便了。
下面就能夠把這些大大小小的矩形存入咱們的R樹中去了。根結點存放的是兩個最大的矩形,這兩個最大的矩形框住了全部的剩餘的矩形,固然也就框住了全部的數據。下一層的結點存放了次大的矩形,這些矩形縮小了範圍。每一個葉子結點都是存放的最小的矩形,這些矩形中可能包含有n個數據。
地圖查找的實例
講完了基本的數據結構,咱們來說個實例,如何查詢特定的數據。又以餐廳爲例,假設我要查詢廣州市天河區天河城附近一千米的全部餐廳地址怎麼辦?
遍歷全部在此區域內的結點,看是否知足咱們的要求便可。怎麼樣,其實R樹的查找規則跟查地圖很像吧?對應下圖:
一棵R樹知足以下的性質:
先來探究一下葉子結點的結構。葉子結點所保存的數據形式爲:(I, tuple-identifier)。
其中,tuple-identifier表示的是一個存放於數據庫中的tuple,也就是一條記錄,它是n維的。I是一個n維空間的矩形,並能夠剛好框住這個葉子結點中全部記錄表明的n維空間中的點。I=(I0,I1,…,In-1)。其結構以下圖所示:
下圖描述的就是在二維空間中的葉子結點所要存儲的信息。
在這張圖中,I所表明的就是圖中的矩形,其範圍是a<=I0<=b,c<=I1<=d。有兩個tuple-identifier,在圖中即表示爲那兩個點。這種形式徹底能夠推廣到高維空間。你們簡單想一想三維空間中的樣子就能夠了。這樣,葉子結點的結構就介紹完了。
非葉子結點的結構其實與葉子結點很是相似。想象一下B樹就知道了,B樹的葉子結點存放的是真實存在的數據,而非葉子結點存放的是這些數據的「邊界」,或者說也算是一種索引(有疑問的讀者能夠回顧一下上述第一節中講解B樹的部分)。
一樣道理,R樹的非葉子結點存放的數據結構爲:(I, child-pointer)。
其中,child-pointer是指向孩子結點的指針,I是覆蓋全部孩子結點對應矩形的矩形。這邊有點拗口,但我想不是很難懂?給張圖:
D,E,F,G爲孩子結點所對應的矩形。A爲可以覆蓋這些矩形的更大的矩形。這個A就是這個非葉子結點所對應的矩形。這時候你應該悟到了吧?不管是葉子結點仍是非葉子結點,它們都對應着一個矩形。樹形結構上層的結點所對應的矩形可以徹底覆蓋它的孩子結點所對應的矩形。根結點也惟一對應一個矩形,而這個矩形是能夠覆蓋全部咱們擁有的數據信息在空間中表明的點的。
我我的感受這張圖畫的不那麼精確,應該是矩形A要剛好覆蓋D,E,F,G,而不該該再留出這麼多沒用的空間了。但爲尊重原圖的繪製者,特不做修改。」
但R樹有些什麼問題呢?如@宋梟_CD所說:「單純用R樹來做索引,搜索附近的地點,可能會遍歷樹的不少個分支。並且當全國的地圖或者全省的地圖時候,樹的葉節點數目不少,樹的深度也會是一個問題。通常會把地理位置上附近的節點(二維地圖中點線面)預處理成page(大小爲4K的倍數),在這些page上創建R樹的索引。」
我在微博上跟一些朋友討論這個附近點搜索的問題時,除了談到R樹,有幾個朋友都指出GeoHash算法能夠解決,故才瞭解了下GeoHash算法,此文http://blog.nosqlfan.com/html/1811.html 清晰闡述了MongoDB藉助GeoHash算法實現地理位置索引的原理,特引用其內容加以說明,以下:
「支持地理位置索引是MongoDB的一大亮點,這也是全球最流行的LBS服務foursquare 選擇MongoDB的緣由之一。咱們知道,一般的數據庫索引結構是B+ Tree,如何將地理位置轉化爲可創建B+Tree的形式。首先假設咱們將須要索引的整個地圖分紅16×16的方格,以下圖(左下角爲座標0,0 右上角爲座標16,16):
單純的[x,y]的數據是沒法創建索引的,因此MongoDB在創建索引的時候,會根據相應字段的座標計算一個能夠用來作索引的hash值,這個值叫作geohash,下面咱們以地圖上座標爲[4,6]的點(圖中紅叉位置)爲例。咱們第一步將整個地圖分紅等大小的四塊,以下圖:
劃分紅四塊後咱們能夠定義這四塊的值,以下(左下爲00,左上爲01,右下爲10,右上爲11):
db.map.ensureIndex({point : "2d"}, {min : 0, max : 16, bits : 4})
本章完。