【譯】Swift算法俱樂部-哈希表

Swift算法俱樂部

本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Clubraywenderlich.com網(wǎng)站出品的用Swift實現(xiàn)算法和數(shù)據(jù)結(jié)構(gòu)的開源項目,目前在GitHub上有18000+??,我初略統(tǒng)計了一下,大概有一百左右個的算法和數(shù)據(jù)結(jié)構(gòu),基本上常見的都包含了,是iOSer學(xué)習(xí)算法和數(shù)據(jù)結(jié)構(gòu)不錯的資源。
??andyRon/swift-algorithm-club-cn是我對Swift Algorithm Club,邊學(xué)習(xí)邊翻譯的項目。由于能力有限,如發(fā)現(xiàn)錯誤或翻譯不妥,請指正,歡迎pull request。也歡迎有興趣、有時間的小伙伴一起參與翻譯和學(xué)習(xí)??。當(dāng)然也歡迎加??,??????????。
本文的翻譯原文和代碼可以查看??swift-algorithm-club-cn/Hash Table


哈希表(Hash Table)

哈希表允許您通過“鍵”存儲和檢索對象。

哈希表用于實現(xiàn)一些結(jié)構(gòu),例如字典,映射和關(guān)聯(lián)數(shù)組。 這些結(jié)構(gòu)可以通過樹或普通數(shù)組實現(xiàn),但使用哈希表效率更高。

這也可以解釋為什么Swift的內(nèi)置Dictionary類型要求鍵符合Hashable協(xié)議:在內(nèi)部Dictionary使用哈希表實現(xiàn),就像你將在這里學(xué)到的那樣。

怎么工作的

哈希表只不過是一個數(shù)組。 最初,此數(shù)組為空。 將值放入某個鍵下的哈希表時,它使用該鍵計算數(shù)組中的索引。 這是一個例子:

hashTable["firstName"] = "Steve"

    The hashTable array:
    +--------------+
    | 0:           |
    +--------------+
    | 1:           |
    +--------------+
    | 2:           |
    +--------------+
    | 3: firstName |---> Steve
    +--------------+
    | 4:           |
    +--------------+

In this example, the key "firstName" maps to array index 3.
在這個例子中,鍵"firstName"映射到數(shù)組索引3。

Adding a value under a different key puts it at another array index:
在不同的鍵下添加值會將其放在另一個數(shù)組索引處:

hashTable["hobbies"] = "Programming Swift"

    The hashTable array:
    +--------------+
    | 0:           |
    +--------------+
    | 1: hobbies   |---> Programming Swift
    +--------------+
    | 2:           |
    +--------------+
    | 3: firstName |---> Steve
    +--------------+
    | 4:           |
    +--------------+

The trick is how the hash table calculates those array indices. That is where the hashing comes in. When you write the following statement,
這邊的訣竅是哈希表如何計算這些數(shù)組索引。 這就是哈希的用武之地。當(dāng)你寫下面的陳述時,

hashTable["firstName"] = "Steve"

哈希表使用鍵"firstName"并詢問它的hashValue屬性。 因此,鍵必須符合Hashable協(xié)議。

當(dāng)你寫"firstName".hashValue時,它返回一個大整數(shù):-4799450059917011053。 同樣,"hobbies".hashValue的哈希值為4799450060928805186.(您看到的值可能會有所不同。)

這些數(shù)字很大,可以用作我們數(shù)組的索引,其中一個甚至是負(fù)數(shù)!使這些大數(shù)字可用的常用方法是首先使哈希值為正,然后數(shù)組大小進行取模運算(取余數(shù)),這個值就是數(shù)組的索引。

我們的數(shù)組大小為5,所以"firstName"鍵的索引變?yōu)?code>abs(-4799450059917011053) % 5 = 3。 你可以計算出"hobbies"的數(shù)組索引是1(譯注: abs(4799450060928805186) % 5 = 1)。

以這種方式使用哈希是使字典有效的原因:要在哈希表中查找元素,必須用鍵的哈希值以獲取數(shù)組索引,然后在底層數(shù)組中查找元素。 所有這些操作都需要不變的時間,因此插入,檢索和刪除都是 O(1)。

注意: 很難預(yù)測數(shù)組中對象的最終位置。 因此,字典不保證哈希表中元素的任何特定順序。

避免沖突

有一個問題:因為我們采用哈希值的模數(shù)和數(shù)組的大小,可能會發(fā)生兩個或多個鍵被賦予相同的數(shù)組索引。 這稱為沖突。

避免沖突的一種方法是使用大型數(shù)組,這樣可以降低兩個鍵映射到同一索引的可能性。另一個技巧是使用素數(shù)作為數(shù)組大小。但是,必然會發(fā)生沖突,因此您需要找到一種方法來處理沖突。

因為我們的表很小,很容易出現(xiàn)沖突。 例如,鍵"lastName" 的數(shù)組索引也是3,但我們不想覆蓋已在此數(shù)組索引處的值。

處理沖突的常用方法是使用鏈接(chaining)。 該數(shù)組如下所示:

    buckets:
    +-----+
    |  0  |
    +-----+     +----------------------------+
    |  1  |---> | hobbies: Programming Swift |
    +-----+     +----------------------------+
    |  2  |
    +-----+     +------------------+     +----------------+
    |  3  |---> | firstName: Steve |---> | lastName: Jobs |
    +-----+     +------------------+     +----------------+
    |  4  |
    +-----+

使用鏈接,鍵和它們的值不會直接存儲在數(shù)組中。 相反,每個數(shù)組元素都是零個或多個鍵/值對的列表。 數(shù)組元素通常稱為 buckets(可以譯為桶),列表稱為 chains(可以譯為鏈)。 這里我們有5個,其中兩個。 其他三個都是空的。

如果我們編寫以下語句來從哈希表中檢索項目,

let x = hashTable["lastName"]

它首先用哈希鍵 "lastName" 來計算數(shù)組索引,即3。由于桶3有一個鏈,我們逐步遍歷列表,找到帶有 "lastName"鍵的值。這是通過使用字符串比較來比較鍵來完成的。 哈希表檢查鍵是否屬于鏈中的最后一項,并返回相應(yīng)的值 "Jobs"

實現(xiàn)此鏈機制的常用方法是使用鏈表或其他數(shù)組。由于鏈中項目的順序無關(guān)緊要,您可以將其視為集合而不是列表。(現(xiàn)在你也可以想象術(shù)語“桶”的來源;我們只是將所有對象一起轉(zhuǎn)儲到桶中。)

鏈不應(yīng)該變長,因為查找哈希表中的項將變得緩慢。理想情況下,我們根本沒有鏈,但實際上不可能避免沖突。您可以通過使用高質(zhì)量哈希函數(shù)為哈希表提供足夠的桶來提高(避免沖突的)幾率。

注意: 鏈的替代方法是“開放尋址”。 這個想法是這樣的:如果已經(jīng)采用了數(shù)組索引,我們將該元素放在下一個未使用的存儲桶中。 這種方法有其自身的優(yōu)點和缺點。

代碼

讓我們看一下Swift中哈希表的基本實現(xiàn)。 我們將逐步建立起來。

public struct HashTable<Key: Hashable, Value> {
  private typealias Element = (key: Key, value: Value)
  private typealias Bucket = [Element]
  private var buckets: [Bucket]

  private(set) public var count = 0
  
  public var isEmpty: Bool { return count == 0 }

  public init(capacity: Int) {
    assert(capacity > 0)
    buckets = Array<Bucket>(repeatElement([], count: capacity))
  }

HashTable是一個通用容器,兩個泛型類型被命名為Key(必須是Hashable)和Value。 我們還定義了另外兩種類型:Element是在鏈中使用的鍵/值對,而Bucket是這樣的Elements的數(shù)組。

主數(shù)組名為buckets。 初始化方法init(capacity)利用固定容量來確定數(shù)組的大小。 我們還可以使用count變量跟蹤已經(jīng)向哈希表添加了多少項。

如何創(chuàng)建新哈希表對象的示例:

var hashTable = HashTable<String, String>(capacity: 5)

哈希表還不能做任何事情,所以讓我們添加下面的功能。 首先,添加一個幫助方法來計算給定鍵的數(shù)組索引:

  private func index(forKey key: Key) -> Int {
    return abs(key.hashValue) % buckets.count
  }

這將執(zhí)行您之前看到的計算:它將鍵的hashValue的絕對值對桶數(shù)組的大小取模。 我們已將其置于其自身的功能中,因為它在少數(shù)不同的地方使用。

使用哈希表或字典有四種常見的事情:

  • insert a new element

  • 插入一個新元素

  • 查找元素

  • 更新現(xiàn)有元素

  • 刪除元素

這些的語法是:

hashTable["firstName"] = "Steve"   // insert
let x = hashTable["firstName"]     // lookup
hashTable["firstName"] = "Tim"     // update
hashTable["firstName"] = nil       // delete

我們也可以使用 subscript 函數(shù)完成所有這些操作:

  public subscript(key: Key) -> Value? {
    get {
      return value(forKey: key)
    }
    set {
      if let value = newValue {
        updateValue(value, forKey: key)
      } else {
        removeValue(forKey: key)
      }
    }
  }

這需要三個輔助函數(shù)來完成實際工作。 讓我們看一下value(forKey:),它從哈希表中檢索一個對象。

  public func value(forKey key: Key) -> Value? {
    let index = self.index(forKey: key)
    for element in buckets[index] {
      if element.key == key {
        return element.value
      }
    }
    return nil  // key not in hash table
  }

首先,它調(diào)用index(forKey:)將鍵轉(zhuǎn)換為數(shù)組索引。 這給了我們桶號,但如果有碰撞,這個桶可能被多個鍵使用。 value(forKey:)從該存儲桶循環(huán)鏈并逐個比較key。 如果找到,則返回相應(yīng)的值,否則返回nil

插入新元素或更新現(xiàn)有元素的代碼位于updateValue(_:forKey:)中。 這復(fù)雜一點:

  public mutating func updateValue(_ value: Value, forKey key: Key) -> Value? {
    let index = self.index(forKey: key)
    
    // Do we already have this key in the bucket?
    for (i, element) in buckets[index].enumerated() {
      if element.key == key {
        let oldValue = element.value
        buckets[index][i].value = value
        return oldValue
      }
    }
    
    // This key isn't in the bucket yet; add it to the chain.
    buckets[index].append((key: key, value: value))
    count += 1
    return nil
  }

同樣,第一步是將key轉(zhuǎn)換為數(shù)組索引以查找存儲桶。然后我們循環(huán)通過該桶的鏈。如果我們在鏈中找到key,我們就使用新值更新它。如果key不在鏈中,我們將新的鍵/值對插入到鏈的末尾。

正如您所看到的,保持鏈短(通過使哈希表足夠大)非常重要。 否則,你在這些for ...in循環(huán)中花費的時間過長,哈希表的性能將不再是 O(1),而更像O(n)。

刪除也類似,再次遍歷鏈:

  public mutating func removeValue(forKey key: Key) -> Value? {
    let index = self.index(forKey: key)

    // Find the element in the bucket's chain and remove it.
    for (i, element) in buckets[index].enumerated() {
      if element.key == key {
        buckets[index].remove(at: i)
        count -= 1
        return element.value
      }
    }
    return nil  // key not in hash table
  }

這些是哈希表的基本功能。 它們都以相同的方式工作:使用其哈希值將key轉(zhuǎn)換為數(shù)組索引,找到存儲桶,然后遍歷該存儲桶的鏈并執(zhí)行所需的操作。

在 playground 試試這些東西。 它應(yīng)該像標(biāo)準(zhǔn)的Swift Dictionary 一樣工作。

調(diào)整哈希表的大小

這個版本的HashTable總是使用固定大小或容量的數(shù)組。 如果要在哈希表中存儲許多項目,則對于容量,請選擇大于最大項數(shù)的素數(shù)。

哈希表的加載因子是當(dāng)前使用的容量的百分比。 如果哈希表中有3個項有5個桶,那么加載因子是3/5 = 60%。

如果哈希表很小,并且鏈很長,那么加載因子可能會大于1,這不是一個好主意。

如果加載因子變大,大于75%,則可以調(diào)整哈希表的大小。添加此條件的代碼留給讀者練習(xí)。請記住,使桶數(shù)組更大將改變鍵映射到的數(shù)組索引!這要求您在調(diào)整數(shù)組大小后再次插入所有元素。

然后去哪兒?(Where to go from here?)

HashTable非?;A(chǔ)。 作為Swift標(biāo)準(zhǔn)庫高效集成為SequenceType。

作者:Matthijs Hollemans
翻譯:Andy Ron
校對:Andy Ron

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載自:https://halfrost.com/go_map_chapter_one/ https://half...
    HuJay閱讀 6,473評論 1 5
  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,186評論 3 119
  • Swift 中的擴展擴展就是向一個已有的類、結(jié)構(gòu)體或枚舉類型添加新功能,這包括在沒有權(quán)限獲取原始源代碼的情況下擴展...
    Bobby0322閱讀 881評論 0 2
  • 九月四日,星期天。 上午,周逸正騎車去圖書館,費盡力氣爬上一個大坡,再以飛快的速度沖下去,他很享受這種在風(fēng)中穿梭的...
    凜楓閱讀 2,135評論 0 0
  • ????????????????????????????????????? ???????????????????...
    Nick9944閱讀 149評論 0 0

友情鏈接更多精彩內(nèi)容