數據結構與算法之PHP查找算法(哈希查找)

1、哈希查找的定義
提起哈希,我第一印象就是PHP裏的關聯數組,它是由一組key/value的鍵值對組成的集合,應用了散列技術。
哈希表的定義以下:
哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key/value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。
給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能獲得包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。
哈希查找,是在記錄的存儲位置和記錄的關鍵字之間創建一個肯定的對應關係f,使得每一個關鍵字key對應一個存儲位置f(key)。查找時,根據這個肯定的對應關係找到給定值的映射f(key),若查找集合中存在這個記錄,則一定在f(key)的位置上。
哈希查找並不查找數據自己,而是先將數據映射爲一個整數(它的哈希值),並將哈希值相同的數據存放在同一個位置,即以哈希值爲索引構造一個數組。
 
2、設計哈希表
哈希查找的操做步驟以下:
一、取數據元素的關鍵字key,計算其哈希函數值。若該地址對應的存儲空間尚未被佔用,則將該元素存入;不然執行第2步解決衝突。
二、根據選擇的衝突處理方法,計算關鍵字key的下一個存儲地址。若下一個存儲地址仍被佔用,則繼續執行,直到找到能用的存儲地址爲止。
 
最經常使用的哈希函數構造方法是除留餘數法取關鍵字被某個不大於哈希表表長m的數p除後所得餘數爲哈希地址,即Hash(key)=key mod p (p≤m),其中,除數p稱做模,mod稱做取模(求餘數)。
好比:有12個關鍵字:12, 29, 56, 25, 15, 78, 16, 67, 2138, 22, 47,若是採用除留餘數法,哈希函數能夠設計爲f(key) = key mod 12,好比12 mod 12 = 0,它存儲在下標爲0的位置,29 mod 12 = 5,它存儲在下標爲5的位置。
下標
0
1
2
3
4
5
6
7
8
9
10
11
關鍵字
12
25
38
15
16
29
78
67
56
21
22
47
但若是將16改成30,它和78的餘數都爲6,就會和78所對應的下標位置衝突了,生成的hash表只有11個元素,下標爲6的位置對應的值是30,先前的78就被覆蓋掉了。這就是哈希衝突
所以合理選取p值是很重要的,實踐證實:若散列表表長爲m,一般p爲小於或等於表長(最好接近m)的最大質數,此時產生的哈希函數較好。
 
構造哈希表的代碼以下:
<?php
class HashTable{
    public $arr = array();
    public $size = 10;
    public function __construct(){
        // SplFixedArray建立的數組比通常的Array()效率更高,由於更接近C的數組。
        // 建立時須要指定尺寸
        $this->arr = new SplFixedArray($this->size);
    }
    // 簡單hash算法。輸入key,輸出hash後的整數
    private function key2Hash($key){
        $len = strlen($key);
        // key中每一個字符所對應的ASCII的值
        $asciiTotal = 0;
        for($i = 0; $i < $len; $i++){
            $asciiTotal += ord($key[$i]);
        }
        return $asciiTotal % $this->size;
    }
    // 賦值
    public function set($key, $value){
        $hash = $this->key2Hash($key);
        $this->arr[$hash] = $value;
        return true;
    }
    // 取值
    public function get($key){
        $hash = $this->key2Hash($key);
        return $this->arr[$hash];
    }
  // 哈希查找
  public function hashSearch($key) {
        $hash = $this->key2Hash($key);
        return $this->arr[$hash];
    }
    // 改變哈希表長度
    public function editSize($size){
        $this->size = $size;
        $this->arr->setSize($size);
    }
}
// 測試1
$ht = new HashTable();
for($i=0; $i < 15; $i++){
    $ht->set('key' . $i, 'value' . $i);
}
print_r($ht->arr);
/*
SplFixedArray Object
(
    [0] => value14
    [1] => value4
    [2] => value5
    [3] => value6
    [4] => value7
    [5] => value8
    [6] => value10
    [7] => value11
    [8] => value12
    [9] => value13
)
*/
// 測試2
$ht->editSize(15);
for($i = 0; $i < 15; $i++){
    $ht->set('key' . $i, 'value' . $i);
}
print_r($ht->arr);
/*
SplFixedArray Object
(
    [0] => value14
    [1] => value4
    [2] => value0
    [3] => value1
    [4] => value2
    [5] => value3
    [6] => value10
    [7] => value11
    [8] => value12
    [9] => value13
    [10] => value14
    [11] => value9
    [12] =>
    [13] =>
    [14] =>
)
*/
能夠看到,哈希表大小不管是10仍是15,都會出現賦值時後操做覆蓋前操做的問題。所以,在建造哈希表時不只要設定一個好的哈希函數,還要設定一種處理衝突的方法。
 
3、哈希衝突(碰撞)
哈希表不可避免衝突(collision)現象:對不一樣的關鍵字可能獲得同一哈希地址,即key1≠key2,而hash(key1)=hash(key2)。具備相同函數值的關鍵字對該哈希函數來講稱爲同義詞(synonym) 解決哈希衝突經常使用的兩種方法是:開放定址法和鏈地址法。
一、開放定址法
當衝突發生時,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。公式爲:fi(key) = (f(key)+di) MOD m (di=1,2,3,......,m-1)。
好比,12個關鍵字:12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34,用散列函數f(key) = key mod 12。當計算前5個數12, 67, 56, 16, 25時,都是沒有衝突的散列地址,直接存入:
下標
0
1
2
3
4
5
6
7
8
9
10
11
關鍵字
12
25
 
 
16
 
 
67
56
 
 
 
計算key = 37時,發現f(37) = 1,與25所在的位置衝突。
因而咱們應用上面的公式f(37) = (f(37)+1) mod 12 = 2。因而將37存入下標爲2的位置。
下標
0
1
2
3
4
5
6
7
8
9
10
11
關鍵字
12
25
37
 
16
 
 
67
56
 
 
 
接下來22,29,15,47都沒有衝突,正常的存入:
下標
0
1
2
3
4
5
6
7
8
9
10
11
關鍵字
12
25
37
15
16
29
 
67
56
 
22
47
到了 key=48,計算獲得f(48) = 0,與12所在的0位置衝突了,應用公式f(48) = (f(48)+1) mod 12 = 1,又與25所在的位置衝突。繼續應用公式f(48) = (f(48)+2) mod 12=2,仍是衝突……一直到 f(48) = (f(48)+6) mod 12 = 6時,纔有空位,存入:
下標
0
1
2
3
4
5
6
7
8
9
10
11
關鍵字
12
25
37
15
16
29
48
67
56
 
22
47
同理,最後一個key = 34,存入到下標爲9的位置上。
 
開放定址法解決衝突代碼以下:
<?php
/**
* 開放定址法解決衝突
*/
class HashTable{
    public $arr = array();
    public $size = 12;
    public function __construct(){
        // 建立時須要指定尺寸
        $this->arr = new SplFixedArray($this->size);
    }
    // Hash函數:採用取餘法,用關鍵字的值取餘表的長度,做爲哈希存儲的地址
    public function key2Hash($key){
        return $key % $this->size;
    }
 
    // 賦值
    public function set($key, $value){
        $cur = 0;
        $hash = $this->key2Hash($key);
        if (isset($this->arr[$hash])) {
            while (isset($this->arr[$hash]) && $cur < $this->size) {
                $hash = $this->key2Hash($hash + 1); // 開放定址法處理衝突
                $cur++;
            }
        }
        $this->arr[$hash] = $value;
    }
    // 查找
    public function hashSearch($key){
        $hash = $this->key2Hash($key);
        $k = 0;
        while (isset($this->arr[$hash]) && $this->arr[$hash] != $key && $k < $this->size) {
            $hash = $this->key2Hash($hash + 1);
            $k++;
        }
        if ($this->arr[$hash] == $key) { // 找到
            return $hash;
        } else {    // 沒找到
            return -1;
        }
    }
}
//測試
$ht = new HashTable();
$keys = [12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34];
for ($i = 0; $i < count($keys); $i++) {
    $key = $keys[$i];
    $ht->set($key, $key);
}
print_r($ht->arr);
$pos = $ht->hashSearch(37);
echo $pos;  // 2
 
二、鏈地址法(拉鍊法)
將全部的相同Hash值的key放在一個鏈表中,好比key3和key14在hash以後都是0,那麼在數組的鍵爲0的地方以鏈表的形式存儲這兩個值。這樣,不管有多少個衝突,都只是在當前位置給單鏈表增長節點。
<?php
/**
* 鏈地址法解決衝突
*/
// 建立HashNode類,用來存儲key和value的值,而且存儲相同hash的另外一個元素。
class HashNode{
    public $key;
    public $value;
    public $nextNode;
    public function __construct($key, $value, $nextNode = Null){
        $this->key = $key;              // 節點的關鍵字
        $this->value = $value;          // 節點的值
        $this->nextNode = $nextNode;    // 指向具備相同Hash值節點的指針
    }
}
 
class HashTable{
    public $arr;
    public $size = 10;
    public function __construct(){
        $this->arr = new SplFixedArray($this->size);
    }
    // 簡單hash算法。輸入key,輸出hash後的整數
    public function key2Hash($key){
        $asciiTotal = 0;
        $len = strlen($key);
        for($i=0; $i<$len; $i++){
            $asciiTotal += ord($key[$i]);
        }
        return $asciiTotal % $this->size;
    }
    // 賦值
    public function set($key, $value){
        $hash = $this->key2Hash($key);
        if (isset($this->arr[$hash])){
            $newNode = new HashNode($key, $value, $this->arr[$hash]);
        } else {
            $newNode = new HashNode($key, $value, null);
        }
        $this->arr[$hash] = $newNode;
        return true;
    }
    // 取值
    public function get($key){
        $hash = $this->key2Hash($key);
        $current = $this->arr[$hash];
        while (!empty($current)){
            if($current->key == $key){
                return $current->value;
            }
            $current = $current->nextNode;
        }
        return NULL;
    }
    // 哈希查找
    public function hashSearch($key){
        $hash = $this->key2Hash($key);
        $current = $this->arr[$hash];
        while (isset($current)) { //遍歷當前鏈表
            if ($current->key == $key){  //比較當前節點的關鍵字
                return $current->value;  //查找成功
            }
            $current = $current->nextNode;  //比較下一個節點
        }
        return null;  //查找失敗
    }
}
//測試1
$newArr = new HashTable();
for($i = 0; $i < 30; $i++){
    $newArr->set('key'.$i, 'value'.$i);
}
print_r($newArr->arr);
print_r($newArr->get('key3'));
修改後的插入的算法流程以下:
1)    使用Hash函數計算關鍵字的Hash值,經過Hash值定位到Hash表的指定位置。
2)    若是此位置已經被其餘節點佔用,把新節點的$nextNode指向此節點,不然把新節點$nextNode設置爲null。
3)    把新節點保存到Hash表的當前位置。
通過這三個步驟,相同的Hash值得節點會被鏈接到同一個鏈表。
 
修改後的查找算法流程以下:
1)    使用Hash函數計算關鍵字的Hash值,經過Hash值定位到Hash表的指定位置。
2)    遍歷當前鏈表,比較鏈表中的每一個節點的關鍵字與查找關鍵字是否相等。若是相等,查找成功。
3)    若是整個鏈表都沒有要查找的關鍵字,查找失敗。
相關文章
相關標籤/搜索