上篇文章的查找是不是有意犹未尽的感觉呢?因为我们是真真正正地接触到了时间复杂度的优化。从线性查找的 O(n) 直接优化到了折半查找的 O(logN) ,绝对是一个质的飞跃。但是,我们的折半查找最核心的一个要求是什么呢?那就是必须是原始数据是要有序的。这可是个麻烦事啊,毕竟如果数据量很庞大的话,排序又会变得很麻烦。不过别着急,今天我们要学习的散列表查找又是另一种形式的查找,它能做到什么程度呢?

O(1) ,是的,你没看错,散列表查找在最佳情况下是可以达到这种常数级别的查找效率的,是不是很神奇。

哈希散列(除留余数法)

先通过实际的例子看一种非常简单的散列算法。在数据量比较大的情况下,我们往往要对数据表进行表操作,最简单的一种方案就是根据某一个字段,比如说 ID 来对它进行取模。也就是说,假如我们要分20张表,那么就用数据的 ID 来除以 20 ,然后获得它的余数。然后将这条数据添加到余数所对应的这张表中。我们通过代码来模拟这个操作。

or($i=0;$i<100;$i++){
$arr[] = $i+1;
} $hashKey = 7;
$hashTable = [];
for($i=0;$i<100;$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) 级别了。

当然,除了线性探测之外,还有二次探测(平方)、伪随机探测等算法。另外也可以使用链表来实现链地址法来解决哈希冲突的问题。这些内容大家可以自己查阅一下相关的文档或书籍。

总结

哈希散列最后的查找功能其实就和我们上面生成那个哈希表的过程一样,发现有冲突的解决方式也是一样的,这里就不多说了。对于哈希这块来说,不管是教材还是各类学习资料,其实介绍的内容都并不是特别的多,所以,我们也是以入门的心态来简单地了解一下哈希散列这块的知识,更多的内容大家可以自己研究多多分享哈!

测试代码:

https://github.com/zhangyue0503/Data-structure-and-algorithm/blob/master/6.查找/source/6.2散列表查找.php

参考文档:

《数据结构》第二版,严蔚敏

《数据结构》第二版,陈越

关注公众号:【硬核项目经理】获取最新文章

添加微信/QQ好友:【xiaoyuezigonggong/149844827】免费得PHP、项目管理学习资料

知乎、公众号、抖音、头条搜索【硬核项目经理】

B站ID:482780532

【PHP数据结构】散列表查找的更多相关文章

  1. 数据结构---散列表查找(哈希表)概述和简单实现(Java)

    散列表查找定义 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,是的每个关键字key对应一个存储位置f(key).查找时,根据这个确定的对应关系找到给定值的key的对应f(key) ...

  2. 数据结构(四十二)散列表查找(Hash Table)

    一.散列表查找的基础知识 1.散列表查找的定义 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key).查找时,根据这个确定的对应关系找到 ...

  3. Python数据结构——散列表

    散列表的实现常常叫做散列(hashing).散列仅支持INSERT,SEARCH和DELETE操作,都是在常数平均时间执行的.需要元素间任何排序信息的操作将不会得到有效的支持. 散列表是普通数组概念的 ...

  4. 哈希表查找(散列表查找) c++实现HashMap

    算法思想: 哈希表 什么是哈希表 在前面讨论的各种结构(线性表.树等)中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较.这一类 ...

  5. python 散列表查找

    class HashTable: def __init__(self, size): self.elem = [None for i in range(size)] self.count = size ...

  6. 哈希表(散列表),Hash表漫谈

    1.序 该篇分别讲了散列表的引出.散列函数的设计.处理冲突的方法.并给出一段简单的示例代码. 2.散列表的引出 给定一个关键字集合U={0,1......m-1},总共有不大于m个元素.如果m不是很大 ...

  7. hashtable——散列表

    2018-11-01 散列表---哈希表基于快速存取,时间换空间一种基于线性数组的线性表,不过元素之间并非紧密排列 散列函数--通过函数,有key关键码计算地址(相当于数组下标),函数尽可能使元素均匀 ...

  8. 【Java】 大话数据结构(13) 查找算法(4) (散列表(哈希表))

    本文根据<大话数据结构>一书,实现了Java版的一个简单的散列表(哈希表). 基本概念 对关键字key,将其值存放在f(key)的存储位置上.由此,在查找时不需比较,只需计算出f(key) ...

  9. JavaScript数据结构——集合、字典和散列表

    集合.字典和散列表都可以存储不重复的值. 在集合中,我们感兴趣的是每个值本身,并把它当作主要元素.在字典和散列表中,我们用 [键,值] 的形式来存储数据. 集合(Set 类):[值,值]对,是一组由无 ...

随机推荐

  1. A Python Environment Detector

    The user provide the method to get result(command on remote host), the check standard(a callback fun ...

  2. Create Shortcut for SSH Hosts

    You frequently visit host 10.0.7.141 for example. It's a waste to type "ssh gcp@10.0.7.141" ...

  3. 消息协议AMQP 与 JMS对比

    https://blog.csdn.net/hpttlook/article/details/23391967 https://www.jianshu.com/p/6e6821604efc https ...

  4. 安全工具推荐之HackTools插件

    朋友推荐 链接:https://github.com/LasCC/Hack-Tools 一款多合一Chromium类红队浏览器插件,火狐也有对应版本 功能包括: 动态反向Shell生成器(PHP.Ba ...

  5. 关于下载远程文件为未知文件.txt的解决方法

    本地下载文件后缀正常,服务器下载文件后缀都为.txt的解决方法: 后缀为 未知文件.txt 的原因为前端无权限获取Content-Disposition中的文件名 response.setHeader ...

  6. Git(12)-- Git 分支 - 分支简介

    @ 目录 1.分支简介 1.1.初始化并首次提交 首次提交对象及其树结构: git 的 cat-file 的命令用法: 1.2.修改并第二次提交 第二次提交对象及其树结构: 1.3.修改并第三次提交 ...

  7. noip35

    T1 考场乱搞出锅了... 正解: 把原序列按k往左和往右看成两个序列,求个前缀和,找下一个更新的位置,直接暴跳. Code #include<cstdio> #include<cs ...

  8. docker 安装 sonarQube

    sonarQube 是一款开源代码检测工具.本篇介绍通过 docker 来安装.大概的一个运作流程是这样的,先通过 sonar-scanner 插件扫描代码,把数据存储到数据库,sonarQube 读 ...

  9. Java-线程池专题 (美团)

    实现多线程的三种方式,继承Thread,实现Runnable 和 实现 Executor接口 ,具体参考:Java 多线程 三种实现方式 去美团,问到了什么是线程池,如何使用,为什么要用,以下做个总结 ...

  10. ORM 之 EF的使用(一)

    早期对数据库进行操作 通过Ado.Net 操作数据库 需要操作sqlCommand/sqlConnection/adapter/datareader 如图 后来 基于面向对象的思想 出现了中间件ORM ...