【PHP資料結構】雜湊表查詢

lvxfcjf發表於2021-09-09

上篇文章的查詢是不是有意猶未盡的感覺呢?因為我們是真真正正地接觸到了時間複雜度的最佳化。從線性查詢的 O(n) 直接最佳化到了折半查詢的 O(logN) ,絕對是一個質的飛躍。但是,我們的折半查詢最核心的一個要求是什麼呢?那就是必須是原始資料是要有序的。這可是個麻煩事啊,畢竟如果資料量很龐大的話,排序又會變得很麻煩。不過彆著急,今天我們要學習的雜湊表查詢又是另一種形式的查詢,它能做到什麼程度呢?

O(1) ,是的,你沒看錯,雜湊表查詢在最佳情況下是可以達到這種常數級別的查詢效率的,是不是很神奇。

雜湊雜湊(除留餘數法)

先透過實際的例子看一種非常簡單的雜湊演算法。在資料量比較大的情況下,我們往往要對資料表進行表操作,最簡單的一種方案就是根據某一個欄位,比如說 ID 來對它進行取模。也就是說,假如我們要分20張表,那麼就用資料的 ID 來除以 20 ,然後獲得它的餘數。然後將這條資料新增到餘數所對應的這張表中。我們透過程式碼來模擬這個操作。

or($i=0;$i100;$i++){
    $arr[] = $i+1;
}

$hashKey = 7;
$hashTable = [];
for($i=0;$i100;$i++){
    $hashTable[$arr[$i]%$hashKey][] = $arr[$i];
}

print_r($hashTable);

在這裡,我們假設是將 100 條資料放到 7 張表中,就是直接使用取模運算子 % 來獲取餘數就行了,接著就將資料放入到對應的陣列下標中。這 100 個資料就被分別放置在了陣列中 0-6 的下標中。這樣,我們就實現了最簡單的一種資料分表的思想。當然,在實際的業務開發中要遠比這個複雜。因為我們考慮各種不同的場景來確定到底是以什麼形式進行分表,分多少張表,以及後續的擴充套件情況,也就是說,真實情況下要比我們這裡寫的這個複雜很多。

做為演示程式碼來說,這種分表的雜湊形式其實就是雜湊表查詢中最經典也是使用最多的除留餘數法。其實還有其它的一些方法,比如平方取中法、摺疊法、數字分析法之類的方法。它們的核心思想都是作為一個雜湊的雜湊演算法,讓原始資料對應到一個新的值(位置)上。

類似的思想其實最典型的就是 md5() 的雜湊運算,不同的內容都會產生不同的值。另外就是 Redis 、 Memcached 這類的鍵值對快取資料庫,它們其實也會將我們設定的 Key 值進行雜湊後儲存在記憶體中以實現快速的查詢能力。

雜湊衝突問題(線性探測法)

上面的例子其實我們會發現一個問題,那就是雜湊演算法的這個值如果很小的話,就會有很多的重複衝突的資料。如果是真實的一個儲存資料的雜湊表,這樣的儲存其實並不能幫我們快速準確的找到所需要的資料。查詢查詢,它核心的能力其實還是在查詢上。那麼如果我們隨機給定一些資料,然後在同樣長度的範圍內如何儲存它們並且避免衝突呢?這就是我們接下來要學習的雜湊衝突要解決的問題。

$arr = [];
$hashTable = [];
for($i=0;$i$hashKey;$i++){
    $r = rand(1,20);
    if(!in_array($r, $arr)){
        $arr[] = $r;
    }else{
        $i--;
    }
}

print_r($arr);
for($i=0;$i$hashKey;$i++){
    if(!$hashTable[$arr[$i]%$hashKey]){
        $hashTable[$arr[$i]%$hashKey] = $arr[$i];
    }else{
        $c = 0;
        echo '衝突位置:', $arr[$i]%$hashKey, ',值:',$arr[$i], PHP_EOL;
        $j=$arr[$i]%$hashKey+1;
        while(1){
            if($j>=$hashKey){
                $j = 0;
            }
            if(!$hashTable[$j]){
                $hashTable[$j] = $arr[$i];
                break;
            }
            $c++;
            $j++;
            if($c >= $hashKey){
                break;
            }
        }
    }
}
print_r($hashTable);

這回我們只生成 7 個隨機資料,讓他們依然以 7 為模進行除留取餘。同時,我們還需要將它們以雜湊後的結果儲存到另一個陣列中,可以將這個新的陣列看做是記憶體中的空間。如果有雜湊相同的資料,那當然就不能放在同一個空間了,要不同一個空間中有兩條資料我們就不知道真正要取的是哪個資料了。

在這段程式碼中,我們使用的是開放地址法中的線性探測法。這是最簡單的一種處理雜湊衝突的方式。我們先看一下輸出的結果,然後再分析衝突的時候都做了什麼。

// Array
// (
//     [0] => 17     // 3
//     [1] => 13     // 6
//     [2] => 9      // 2
//     [3] => 19     // 5
//     [4] => 2      // 2 -> 3 -> 4
//     [5] => 20     // 6 -> 0
//     [6] => 12     // 5 -> 6 -> 0 -> 1
// )
// 衝突位置:2,值:2
// 衝突位置:6,值:20
// 衝突位置:5,值:12
// Array
// (
//     [3] => 17
//     [6] => 13
//     [2] => 9
//     [5] => 19
//     [4] => 2
//     [0] => 20
//     [1] => 12
// )
  • 首先,我們生成的數字是 17、13、9、19、2、20、12 這七個數字。

  • 17%7=3,17 儲存到下標 3 中。

  • 13%7=6,13 儲存到下標 6 中。

  • 9%7=2,9 儲存到下標 2 中。

  • 19%7=5,19 儲存到下標 5 中。

  • 2%7=2,好了,衝突出現了,2%7 的結果也是 2 ,但是 2 的下標已經有人了,這時我們就從 2 開始往後再看 3 的下標有沒有人,同樣 3 也被佔了,於是到 4 ,這時 4 是空的,就把 2 儲存到了下標 4 中。

  • 20%7=6,和上面一樣,6 已經被佔了,於是我們回到開始的 0 下標,發現 0 還沒有被佔,於是 20 儲存到下標 0 中。

  • 最後的 12%7=5,它將依次經過下標 5 、6 、0、1 最後放在下標 1 處。

最後生成的結果就是我們最後陣列輸出的結果。可以看出,線性探測其實就是如果發現位置被人佔了,就一個一個的向下查詢。所以它的時間複雜度其實並不是太好,當然,最佳情況是資料的總長度和雜湊鍵值的長度相吻合,這樣就能達到 O(1) 級別了。

當然,除了線性探測之外,還有二次探測(平方)、偽隨機探測等演算法。另外也可以使用連結串列來實現鏈地址法來解決雜湊衝突的問題。這些內容大家可以自己查閱一下相關的文件或書籍。

總結

雜湊雜湊最後的查詢功能其實就和我們上面生成那個雜湊表的過程一樣,發現有衝突的解決方式也是一樣的,這裡就不多說了。對於雜湊這塊來說,不管是教材還是各類學習資料,其實介紹的內容都並不是特別的多,所以,我們也是以入門的心態來簡單地瞭解一下雜湊雜湊這塊的知識,更多的內容大家可以自己研究多多分享哈!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3486/viewspace-2807049/,如需轉載,請註明出處,否則將追究法律責任。

相關文章