数据结构与算法之PHP查找算法(哈希查找)

一、哈希查找的定义

提起哈希,我第一印象就是PHP里的关联数组,它是由一组key/value的键值对组成的集合,应用了散列技术。

哈希表的定义如下:

哈希表(Hash table,也叫散列表),是根据关键码值(Key/value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

而哈希查找,是在记录的存储位置和记录的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。

哈希查找并不查找数据本身,而是先将数据映射为一个整数(它的哈希值),并将哈希值相同的数据存放在同一个位置,即以哈希值为索引构造一个数组。

 

二、设计哈希表

哈希查找的操作步骤如下:

1、取数据元素的关键字key,计算其哈希函数值。若该地址对应的存储空间还没有被占用,则将该元素存入;否则执行第2步解决冲突。

2、根据选择的冲突处理方法,计算关键字key的下一个存储地址。若下一个存储地址仍被占用,则继续执行,直到找到能用的存储地址为止。

 

最常用的哈希函数构造方法是除留余数法。取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址,即Hash(key)=key mod p (p≤m),其中,除数p称作模,mod称作取模(求余数)。

比如:有12个关键字:12, 29, 56, 25, 15, 78, 16, 67, 21, 38, 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,都会出现赋值时后操作覆盖前操作的问题。因此,在建造哈希表时不仅要设定一个好的哈希函数,还要设定一种处理冲突的方法。

 

三、哈希冲突(碰撞)

哈希表不可避免冲突(collision)现象:对不同的关键字可能得到同一哈希地址,即key1≠key2,而hash(key1)=hash(key2)。具有相同函数值的关键字对该哈希函数来说称为同义词(synonym)。解决哈希冲突常用的两种方法是:开放定址法和链地址法。

1、开放定址法

当冲突发生时,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。公式为: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

 

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指向此节点,否则把新节点nextNode设置为null。

3)    把新节点保存到Hash表的当前位置。

经过这三个步骤,相同的Hash值得节点会被连接到同一个链表。

 

修改后的查找算法流程如下:

1)    使用Hash函数计算关键字的Hash值,通过Hash值定位到Hash表的指定位置。

2)    遍历当前链表,比较链表中的每个节点的关键字与查找关键字是否相等。如果相等,查找成功。

3)    如果整个链表都没有要查找的关键字,查找失败。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值