Go 數據結構和算法篇:哈希表、哈希函數、哈希衝突和哈希算法

一、哈希表

哈希表(HashTable,也叫散列表),是根據鍵名(Key)直接訪問對應內存存儲位置的數據結構。

其實現原理是通過哈希函數(也叫散列函數)將元素的鍵名映射爲數組下標(轉化後的值叫做哈希值或散列值),然後在對應下標位置存儲記錄值。當我們按照鍵名查詢元素時,可以使用同樣的哈希函數,將鍵名轉化爲數組下標,從對應的數組下標位置讀取數據:

散列表圖示

顯然,哈希表使用了數組支持按照下標隨機訪問數據的特性,所以哈希表其實就是數組的一種擴展,由數組演化而來。可以說,沒有數組,就沒有哈希表。我們知道,數組訪問元素的時間複雜度是 O(1),所以哈希表也是一樣(不考慮哈希函數的複雜度的話),因此非常高效。

此外,我們也可以看到,哈希技術既是一種存儲方法,也是一種查找方法。不過,與之前介紹的查找算法不同的是哈希表的不同記錄之間不存在邏輯關係,因此最適合求解的問題是查找與給定值相等的記錄,而不適合做範圍查詢。

哈希表中有兩個關鍵的概念,一個是哈希函數(或者叫散列函數),一個是哈希衝突(或者叫散列衝突)。下面,我們來重點介紹這兩個概念。

二、哈希函數與哈希衝突

哈希函數用於將鍵名經過處理後轉化爲對應的哈希值。具有以下特性:

所謂哈希衝突,簡單來說,指的是 key1 != key2 的情況下,通過哈希函數處理,hash(key1) == hash(key2),這個時候,我們就說發生了哈希衝突。

設計再好的哈希函數也無法避免哈希衝突,根本原因是哈希值是非負整數,總量是有限的,但是現實世界中要處理的鍵名是無限的,將無限的數據映射到有限的集合,肯定避免不了衝突。

事實上,如果不考慮哈希衝突,哈希表的查找效率是非常高的,時間複雜度是 O(1),比二分查找效率還要高,但是因爲無法避免哈希衝突,所以哈希表查找的時間複雜度取決於哈希衝突,最壞的情況可能是 O(n),退化爲順序查找。這種情況在哈希函數設計不合理的情況下更糟。

哈希函數設計

要減少哈希衝突,提高哈希表操作效率,設計一個優秀的哈希函數至關重要,我們平時經常使用的 MD5 加密就是一個哈希函數,但是其實還有其他很多自定義的設計實現,要根據不同場景,設計不同的哈希函數來減少哈希衝突,而且哈希函數也要足夠簡單,否則執行哈希函數本身會成爲哈希表的性能瓶頸。

我們日常很少會自己去設計哈希函數,但是做一些簡單的瞭解還是有必要的。通常有以下幾種哈希函數構造方法:

以上只是一些比較常見的哈希函數設計思路,還有很多其他的設計方法,這裏就不一一列舉了。

哈希衝突處理

我們前面說過,設計再好的哈希函數也不能完全避免哈希衝突,我們只能優化自己的實現讓哈希衝突儘可能少出現罷了,如果出現了哈希衝突,該如何處理呢?下面給出一些思路:

介紹完以上內容之後,想必你對如何打造工業級哈希表已經心中有數。主要考慮因素包含以下幾個方面:

補充一張鏈地址法處理哈希衝突的圖示:

鏈地址法解決哈希衝突圖示

三、哈希算法

我們前面分享了哈希表、哈希函數和哈希衝突,哈希算法簡單理解就是實現前面提到的哈希函數的算法,用於將任意長度的二進制值串映射爲固定長度的二進制值串,映射之後得到的二進制值就是哈希值。

我們日常開發中最常見的哈希算法應用就是通過 MD5 對數據進行加密了:

package main

import (
    "crypto/md5"
    "fmt"
)

func main() {
    data := []byte("Hello, World!")
    hash := md5.Sum(data)
    fmt.Printf("原始值: %s\n", data)
    fmt.Printf("MD5值: %x\n"hash)
}

這裏的 md5.Sum 就是一個哈希函數。執行上述代碼,打印結果如下:

哈希算法的一般特性如下:

四、哈希算法的應用

1、場景一:安全加密

我們日常用戶密碼加密通常使用的都是 md5sha 等哈希函數,因爲不可逆,而且微小的區別加密之後的結果差距很大,所以安全性更好。

2、場景二:唯一標識

比如我們的 URL 字段或者圖片字段要求不能重複,這個時候就可以通過對相應字段值做 md5 處理,將數據統一爲 32 位長度從數據庫索引構建和查詢角度效果更好,此外,還可以對文件之類的二進制數據做 md5 處理,作爲唯一標識,這樣判定重複文件的時候更快捷。

3、場景三:數據校驗

比如我們從網上下載的很多文件(尤其是 P2P 站點資源),都會包含一個 MD5 值,用於校驗下載數據的完整性,避免數據在中途被劫持篡改。

4、場景五:哈希函數

前面我們已經提到,PHP 中的 md5sha1hash 等函數都是基於哈希算法計算哈希值。

5、場景五:負載均衡

對於同一個客戶端上的請求,尤其是已登錄用戶的請求,我們需要將其會話請求都路由到同一臺機器,以保證數據的一致性,這可以藉助哈希算法來實現,通過用戶 ID 尾號對總機器數取模(取多少位可以根據機器數定),將結果值作爲機器編號。

6、場景六:分佈式緩存

分佈式緩存和其他機器或數據庫的分佈式不一樣,因爲每臺機器存放的緩存數據不一致,每當緩存機器擴容時,需要對緩存存放機器進行重新索引(或者部分重新索引),這裏應用到的也是哈希算法的思想。後面我們介紹 Redis 系列的時候會系統闡述這一塊。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/It1Rtci_A3dwZp1l3ZT90w