做者:Soroush Khanlou,原文連接,原文日期:2018-09-19 譯者:WAMaker;校對:numbbbbb,小鐵匠Linus;定稿:Forelaxgit
Swift 4.2 爲哈希的實現帶來了一些新的變化。在此以前,哈希交由對象自己全權代理。當你向對象索取 哈希值(hashValue)
時,它會把處理好的整型值做爲哈希值返回。而如今,實現了 Hashable
協議的對象則描述了它的參數是如何組合,並傳遞給做爲入參的 Hasher
對象。這樣作有如下幾點好處:github
我以前寫過一篇關於 如何使用 Swift 的 Hashable
協議從零實現 Dictionary
的文章(先閱讀它會幫助你閱讀本文,但這不是必須的)。今天,我想談論一種不一樣類型的,基於機率性而非明確性的數據結構:布隆過濾器(Bloom Filters)。咱們會使用 Swift 4.2 的新特性,包括新的哈希模型來構建它。算法
布隆過濾器很怪異。想象這樣一種數據結構:數據庫
可是:swift
何時會想要這種數據結構呢?Medium 使用它們來 跟蹤博文的閱讀狀態。必應使用它們作 搜索索引。你可使用它們來構建一個緩存,在無需訪問數據庫的狀況下就能判斷用戶名是否有效(例如在 @-mention 中)。像服務器這樣可能擁有巨大的規模,卻不必定有巨大資源的場景中,它們會很是有用。數組
(若是你以前作過圖形方面的工做,可能好奇它是如何與 高光過濾器 產生聯繫的。答案是沒有聯繫。高光過濾器(bloom filters)是小寫的 b,而布隆過濾器(Bloom Filters)是由一個叫布隆的人命名的。徹底是個巧合。)緩存
那它們是如何運做的呢?安全
將對象放入布隆過濾器如同將它放入集合或字典:計算對象的哈希值,並根據存儲數組的大小對哈希值求餘。就這點而言,使用布隆過濾器只須要修改該索引處的值:將 false 改成 true,而不用像使用集合或字典那樣,把對象存放到索引位置。bash
咱們先經過一個簡單的例子來理解過濾器是若是運做的,以後再對它進行擴展。想象一個擁有 8 個 false 值的布爾數組(或稱之爲 比特數組):服務器
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---------------------------------
| | | | | | | | |
複製代碼
它表明了咱們的布隆過濾器。我想要插入字符串「soroush」。它的哈希值是 9192644045266971309,當這個值餘 8 時獲得 5。咱們修改那一位的值。
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---------------------------------
| | | | | | * | | |
複製代碼
接下來我想要插入字符串「swift」,它的哈希值是 7052914221234348788,餘 8 得 4,修改索引 4 的值。
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---------------------------------
| | | | | * | * | | |
複製代碼
要測試布隆過濾器是否包含「soroush」,我再次計算它的哈希值並求餘,仍舊獲得餘數 5,對應值是 true。「soroush」確實在布隆過濾器中。
然而僅僅測試可以經過的用例是不夠的,咱們須要寫一些會致使失敗的用例。測試字符串「khanlou」取餘獲得的索引值是 2,所以咱們知道它不在布隆過濾器中。到此爲止一切都好。接下去一個測試:對「hashable」字符串取餘獲得的索引值是 5,這就發生了一次衝突!即便這個值歷來沒有被加入過,過濾器仍返回了 true。這即是布隆過濾器會發生誤報的例子。
有兩個主要的策略能夠儘量減小誤報。第一個策略,也是兩個策略中相對有趣的:咱們可使用不一樣的哈希函數計算兩次或三次哈希值而非一次。只要它們都是表現良好的哈希函數(均勻分佈,一致性,最小的碰撞概率),咱們就能爲每一個值生成多個索引改變布爾值。此次,咱們計算兩次「soroush」的哈希值,生成 2 個索引並改變布爾值。這時,當咱們檢查「khanlou」是否在布隆過濾器中,其中一個哈希值可能會和「soroush」的一個哈希值衝突,但兩個值同時發生衝突的可能性就會變得很小。你能夠把這個數字擴大。在下面的代碼我會作 3 次哈希計算,但你能夠作更屢次。
固然,若是你計算更屢次哈希值,每一個元素在布爾數組中會佔據更多的空間。事實上,如今的數據幾乎不佔用空間。8 個布爾值的數組對應 1 字節。因此第二個減少誤報的策略是擴大數組的規模。咱們能將數組變得足夠大而不用擔憂它的空間消耗。下面的代碼中咱們會默認使用 512 比特大小的數組。
如今,即便同時使用這些策略,你依然會獲得衝突,即誤報,但衝突的概率會減少。這是布隆過濾器的一個缺陷,但在合適的場景用它來節省速度與空間是一筆不錯的交易。
在開始具體的代碼以前我有另外三點想要談談。首先,你不能改變布隆過濾器的大小。當你對哈希值取餘時,這是在破壞信息,在不保留原始哈希值的狀況下你不能回溯以前的信息 —— 保留原始值至關於否決了這個數據結構節約空間的優點。
其次,你能看到想要枚舉布隆過濾器全部的值是多麼異想天開。你再也不擁有這些值,只是它們以哈希形式存在的替代品。
最後,你一樣能看到想要從布隆過濾器中移除元素是不可能的。若是想將布爾值變回 false,你並不知道是哪些值將它變爲 true。是準備移除的值仍是其它值?這樣作會形成漏報和誤報。(這對你來講多是值得權衡的)你能夠在每一個索引上保留計數而非布爾值來解決這個問題,雖然保留計數仍是會帶來存儲問題,但根據使用場景的不一樣,這樣作或許是值得的。
廢話很少說,讓咱們開始着手編碼。我在這裏作的一些決策和你可能會作的有所不一樣,第一個不一樣就是要不要讓對象支持範型。我認爲讓對象包含更多關於它須要存儲內容的元數據是有意義的,但若是你發現這樣作限制太多,你能夠改變它。
struct BloomFilter<Element: Hashable> {
// ...
}
複製代碼
咱們須要存儲兩種主要的數據。第一個是 data
,用於表示比特數組。它存儲了全部和哈希值有關的標記:
private var data: [Bool]
複製代碼
接下來,咱們須要不一樣的哈希函數。一些布隆過濾器確實會使用不一樣的方法計算哈希值,但我以爲使用相同的算法,同時混入一個隨機生成的值會更簡單。
private let seeds: [Int]
複製代碼
當初始化布隆過濾器時,咱們須要初始化這兩個實例變量。比特數組會簡單的重複 false
值來初始化,而種子值則使用 Swift 4.2 的新 API Int.random
來生成咱們須要的種子值。
init(size: Int, hashCount: Int) {
data = Array(repeating: false, count: size)
seeds = (0..<hashCount).map({ _ in Int.random(in: 0..<Int.max) })
}
複製代碼
同時,建立一個帶有默認值的便利構造器。
init() {
self.init(size: 512, hashCount: 3)
}
複製代碼
咱們要實現兩個主要的方法:insert
和 contains
。它們都須要接收元素做爲參數併爲每個種子值計算出對應的哈希值。私有的幫助方法會頗有用。因爲種子值表明了「不一樣的」哈希函數,咱們就須要爲每個種子生成對應的哈希值。
private func hashes(for element: Element) -> [Int] {
return seeds.map({ seed -> Int in
// ...
})
}
複製代碼
要實現函數主體,咱們須要建立一個 Hasher
對象(Swift 4.2 新特性),將想要進行哈希計算的對象傳給它。帶上種子確保了生成的哈希值不會衝突。
private func hashes(for element: Element) -> [Int] {
return seeds.map({ seed -> Int in
var hasher = Hasher()
hasher.combine(element)
hasher.combine(seed)
let hashValue = abs(hasher.finalize())
return hashValue
})
}
複製代碼
同時,注意哈希值的絕對值。哈希計算有可能產生負數,這會致使咱們的數組訪問崩潰。取絕對值操做減小了 1 比特的信息熵,對咱們來講是有益的。
理想的狀況是你可以使用種子來初始化 Hasher
而不是把它混合進去。Swift 的 Hasher
會在每次程序啓動的時候被分配一個不一樣的種子(除非你 設置固定的環境變量 讓種子在不一樣啓動間保持一致,而這樣作一般是一些測試目的),意味着你不能把這些值寫到磁盤上。若是咱們控制了 Hasher
的種子,咱們就能將這些值寫到磁盤上了。而就像這個布隆過濾器展現的那樣,它應該只被用於內存緩存。
hashes(for:)
方法完成了不少繁重的工做,讓 insert
方法很是簡潔:
mutating func insert(_ element: Element) {
hashes(for: element)
.forEach({ hash in
data[hash % data.count] = true
})
}
複製代碼
生成全部的哈希值,分別餘上 data
數組的長度,並設置對應索引位的值爲 true
。
contains
方法也一樣簡單,同時也給了咱們使用 Swift 4.2 另外一個新特性 allSatisfy
的機會。這個新方法能夠判斷序列中的全部對象是否都經過了某項用 block 表示的測試:
func contains(_ element: Element) -> Bool {
return hashes(for: element)
.allSatisfy({ hash in
data[hash % data.count]
})
}
複製代碼
由於 data[hash % data.count]
的結果已是布爾值了,它與 allSatisfy
十分契合。
你也能夠添加 isEmpty
方法用來檢測 data
中的全部值是否都是 false。
布隆過濾器是一種奇怪的數據結構。咱們接觸的大多數數據結構都是明確性的。當把一個對象放入字典中時,你知道那個值以後一直在那兒。而布隆過濾器是機率性的,犧牲肯定性來換取空間和速度。布隆過濾器不是你會天天用的數據結構,但當你確實須要它時,就會感覺到有它真好。
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 swift.gg。 Open Annotation Sidebar