測一測你的演算法階段學習成果

weixin_33843409發表於2019-03-06

《資料結構與演算法之美》專欄最重要的基礎篇馬上就要講完了,我從前面的文章中挑選了幾個例子,稍加修改,組成了一套測試題。你先不要著急看答案,自己先想一想怎麼解決,測一測自己對之前的知識掌握的程度。

如果有哪裡卡殼或者不怎麼清楚的,可以回過頭再複習一下。正所謂溫故知新,這種通過實際問題查缺補漏的學習方法,非常利於你鞏固前面講的知識點,你可要好好珍惜這次機會哦!

實戰測試題(一)

假設獵聘網有10萬名獵頭顧問,每個獵頭顧問都可以通過做任務(比如釋出職位),來積累積分,然後通過積分來下載簡歷。假設你是獵聘網的一名工程師,如何在記憶體中儲存這10萬個獵頭ID和積分資訊,讓它能夠支援這樣幾個操作:

  • 根據獵頭的ID快速查詢、刪除、更新這個獵頭的積分資訊;
  • 查詢積分在某個區間的獵頭ID列表;
  • 查詢積分從小到大排在第x位的獵頭ID資訊;
  • 查詢按照積分從小到大排名在第x位到第y位之間的獵頭ID列表。

相關章節
17 | 跳錶:為什麼Redis一定要用跳錶來實現有序集合?

題目解析

這個問題既要通過ID來查詢,又要通過積分來查詢,所以,對於獵頭這樣一個物件,我們需要將其組織成兩種資料結構,才能支援這兩類操作。

我們按照ID,將獵頭資訊組織成雜湊表。這樣,就可以根據ID資訊快速的查詢、刪除、更新獵頭的資訊。我們按照積分,將獵頭資訊組織成跳錶這種資料結構,按照積分來查詢獵頭資訊,就非常高效,時間複雜度是O(logn)。

我剛剛講的是針對第一個、第二個操作的解決方案。第三個、第四個操作是類似的,按照排名來查詢,這兩個操作該如何實現呢?

我們可以對剛剛的跳錶進行改造,每個索引結點中加入一個span欄位,記錄這個索引結點到下一個索引結點的包含的連結串列結點的個數。這樣就可以利用跳錶索引,快速計算出排名在某一位的獵頭或者排名在某個區間的獵頭列表。

實際上,這些就是Redis中有序集合這種資料型別的實現原理。在開發中,我們並不需要從零開始程式碼實現一個雜湊表和跳錶,我們可以直接利用Redis的有序集合來完成。

實戰測試題(二)

電商交易系統中,訂單資料一般都會很大,我們一般都分庫分表來儲存。假設我們分了10個庫並儲存在不同的機器上,在不引入複雜的分庫分表中介軟體的情況下,我們希望開發一個小的功能,能夠快速地查詢金額最大的前K個訂單(K是輸入引數,可能是1、10、1000、10000,假設最大不會超過10萬)。如果你是這個功能的設計開發負責人,你會如何設計一個比較詳細的、可以落地執行的設計方案呢?

為了方便你設計,我先交代一些必要的背景,在設計過程中,如果有其他需要明確的背景,你可以自行假設。

  • 資料庫中,訂單表的金額欄位上建有索引,我們可以通過select order by limit語句來獲取資料庫中的資料;
  • 我們的機器的可用記憶體有限,比如只有幾百M剩餘可用記憶體。希望你的設計儘量節省記憶體,不要發生Out of Memory Error。

相關章節
12 | 排序(下):如何用快排思想在O(n)內查詢第K大元素?

題目解析
解決這個題目的基本思路我想你應該能想到,就是藉助歸併排序中的合併函式,這個我們在排序(下)以及堆的應用那一節中講過。

我們從每個資料庫中,通過select order by limit語句,各取區域性金額最大的訂單,把取出來的10個訂單放到優先順序佇列中,取出最大值(也就是大頂堆堆頂資料),就是全域性金額最大的訂單。然後再從這個全域性金額最大訂單對應的資料庫中,取出下一條訂單(按照訂單金額從大到小排列的),然後放到優先順序佇列中。一直重複上面的過程,直到找到金額前K(K是使用者輸入的)大訂單。

從演算法的角度看起來,這個方案非常完美,但是,從實戰的角度來說,這個方案並不高效,甚至很低效。因為我們忽略了,資料庫讀取資料的效能才是這個問題的效能瓶頸。所以,我們要儘量減少SQL請求,每次多取一些資料出來,那一次性取出多少才合適呢?這就比較靈活、比較有技巧了。一次性取太多,會導致資料量太大,SQL執行很慢,還有可能觸發超時,而且,我們題目中也說了,記憶體有限,太多的資料載入到記憶體中,還有可能導致OOM。

所以,一次性不能取太多資料,也不能取太少資料,到底是多少,還要根據實際的硬體環境做benchmark測試去找最合適的。

實戰測試題(三)

我們知道,CPU資源是有限的,任務的處理速度與執行緒個數並不是線性正相關。相反,過多的執行緒反而會導致CPU頻繁切換,處理效能下降。所以,執行緒池的大小一般都是綜合考慮要處理任務的特點和硬體環境,來事先設定的。

當我們向固定大小的執行緒池中請求一個執行緒時,如果執行緒池中沒有空閒資源了,這個時候執行緒池如何處理這個請求?是拒絕請求還是排隊請求?各種處理策略又是怎麼實現的呢?

相關章節
09 | 佇列:佇列線上程池等有限資源池中的應用

題目解析

這個問題的答案涉及佇列這種資料結構。佇列可以應用在任何有限資源池中,用於排隊請求,比如資料庫連線池等。實際上,對於大部分資源有限的場景,當沒有空閒資源時,基本上都可以通過“佇列”這種資料結構來實現請求排隊。

這個問題的具體答案,在佇列那一節我已經講得非常詳細了,你可以回去看看,這裡我就不贅述了。

實戰測試題(四)

通過IP地址來查詢IP歸屬地的功能,不知道你有沒有用過?沒用過也沒關係,你現在可以開啟百度,在搜尋框裡隨便輸一個IP地址,就會看到它的歸屬地。

這個功能並不複雜,它是通過維護一個很大的IP地址庫來實現的。地址庫中包括IP地址範圍和歸屬地的對應關係。比如,當我們想要查詢202.102.133.13這個IP地址的歸屬地時,我們就在地址庫中搜尋,發現這個IP地址落在[202.102.133.0, 202.102.133.255]這個地址範圍內,那我們就可以將這個IP地址範圍對應的歸屬地“山東東營市”顯示給使用者了。

[202.102.133.0, 202.102.133.255]  山東東營市 [202.102.135.0, 202.102.136.255]  山東煙臺 [202.102.156.34, 202.102.157.255] 山東青島 [202.102.48.0, 202.102.48.255] 江蘇宿遷 [202.102.49.15, 202.102.51.251] 江蘇泰州 [202.102.56.0, 202.102.56.255] 江蘇連雲港

在龐大的地址庫中逐一比對IP地址所在的區間,是非常耗時的。假設在記憶體中有12萬條這樣的IP區間與歸屬地的對應關係,如何快速定位出一個IP地址的歸屬地呢?

相關章節

15 | 二分查詢(上):如何用最省記憶體的方式實現快速查詢功能?

題目解析

這個問題可以用二分查詢來解決,不過,普通的二分查詢是不行的,我們需要用到二分查詢的變形演算法,查詢最後一個小於等於某個給定值的資料。不過,二分查詢最難的不是原理,而是實現。要實現一個二分查詢的變形演算法,並且實現的程式碼沒有bug,可不是一件容易的事情,不信你自己寫寫試試。

關於這個問題的解答以及寫出bug free的二分查詢程式碼的技巧,我們在二分查詢(下)那一節有非常詳細的講解,你可以回去看看,我這裡就不贅述了。

實戰測試題(五)

假設我們現在希望設計一個簡單的海量圖片儲存系統,最大預期能夠儲存1億張圖片,並且希望這個海量圖片儲存系統具有下面這樣幾個功能:

  • 儲存一張圖片及其它的元資訊,主要的元資訊有:圖片名稱以及一組tag資訊。比如圖片名稱叫玫瑰花,tag資訊是{紅色,花,情人節};
  • 根據關鍵詞搜尋一張圖片,比如關鍵詞是“情人節 花”“玫瑰花”;
  • 避免重複插入相同的圖片。這裡,我們不能單純地用圖片的元資訊,來比對是否是同一張圖片,因為有可能存在名稱相同但圖片內容不同,或者名稱不同圖片內容相同的情況。

我們希望自助開發一個簡單的系統,不希望藉助和維護過於複雜的三方系統,比如資料庫(MySQL、Redis等)、分散式儲存系統(GFS、Bigtable等),並且我們單臺機器的效能有限,比如硬碟只有1TB,記憶體只有2GB,如何設計一個符合我們上面要求,操作高效,且使用機器資源最少的儲存系統呢?

相關章節
21 | 雜湊演算法(上):如何防止資料庫中的使用者資訊被脫庫?

題目解析

這個問題可以分成兩部分,第一部分是根據元資訊的搜尋功能,第二部分是圖片判重。

第一部分,我們可以藉助搜尋引擎中的倒排索引結構。關於倒排索引我會在實戰篇詳細講解,我這裡先簡要說下。

如題目中所說,一個圖片會對應一組元資訊,比如玫瑰花對應{紅色,花,情人節},牡丹花對應{白色,花},我們可以將這種圖片與元資訊之間的關係,倒置過來建立索引。“花”這個關鍵詞對應{玫瑰花,牡丹花},“紅色”對應{玫瑰花},“白色”對應{牡丹花},“情人節”對應{玫瑰花}。

當我們搜尋“情人節 花”的時候,我們拿兩個搜尋關鍵詞分別在倒排索引中查詢,“花”查詢到了{玫瑰花,牡丹花},“情人節”查詢到了{玫瑰花},兩個關鍵詞對應的結果取交集,就是最終的結果了。

第二部分關於圖片判重,我們要基於圖片本身來判重,所以可以用雜湊演算法,對圖片內容取雜湊值。我們對雜湊值建立雜湊表,這樣就可以通過雜湊值以及雜湊表,快速判斷圖片是否存在。

我這裡只說說我的思路,這個問題中還有詳細的記憶體和硬碟的限制。要想給出更加詳細的設計思路,還需要根據這些限制,給出一個估算。詳細的解答,我都放在在雜湊演算法(下)那一節裡到了,你可以自己回去看。

實戰測試題(六)

我們知道,雜湊表的查詢效率並不能籠統地說成是O(1)。它跟雜湊函式、裝載因子、雜湊衝突等都有關係。如果雜湊函式設計得不好,或者裝載因子過高,都可能導致雜湊衝突發生的概率升高,查詢效率下降。

在極端情況下,有些惡意的攻擊者,還有可能通過精心構造的資料,使得所有的資料經過雜湊函式之後,都雜湊到同一個槽裡。如果我們使用的是基於連結串列的衝突解決方法,那這個時候,雜湊表就會退化為連結串列,查詢的時間複雜度就從O(1)急劇退化為O(n)。

如果雜湊表中有10萬個資料,退化後的雜湊表查詢的效率就下降了10萬倍。更直觀點說,如果之前執行100次查詢只需要0.1秒,那現在就需要1萬秒。這樣就有可能因為查詢操作消耗大量CPU或者執行緒資源,導致系統無法響應其他請求,從而達到拒絕服務攻擊(DoS)的目的。這也就是雜湊表碰撞攻擊的基本原理。

如何設計一個可以應對各種異常情況的工業級雜湊表,來避免在雜湊衝突的情況下,雜湊表效能的急劇下降,並且能抵抗雜湊碰撞攻擊?

相關章節
18 | 雜湊表(上):Word文件中的單詞拼寫檢查功能是如何實現的?

題目解析

我經常把這道題拿來作為面試題考察候選人。雜湊表可以說是我們最常用的一種資料結構了,程式語言中很多資料型別,都是用雜湊表來實現的。儘管很多人能對雜湊表都知道一二,知道有幾種雜湊表衝突解決方案,知道雜湊表操作的時間複雜度,但是理論跟實踐還是有一定距離的。光知道這些基礎的理論並不足以開發一個工業級的雜湊表。

所以,我在雜湊表(中)那一節中詳細給你展示了一個工業級的雜湊表要處理哪些問題,以及如何處理的,也就是這個問題的詳細答案。

這六道題你回答得怎麼樣呢?或許你還無法100%回答正確,沒關係。其實只要你看了解析之後,有比較深的印象,能立馬想到哪節課裡講過,這已經說明你掌握得不錯了。畢竟想要完全掌握我講的全部內容還是需要時間沉澱的。對於《資料結構與演算法之美》這門專欄的學習,你一定不要心急,慢慢來。只要方向對了就都對了,剩下就交給時間和努力吧!

相關文章