上篇文章的查找是不是有意犹未尽的感觉呢?因为我们是真真正正地接触到了时间复杂度的优化。从线性查找的 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. 跟我一起写 Makefile(七)

    使用变量 ---- 在Makefile中的定义的变量,就像是C/C++语言中的宏一样,他代表了一个文本字串,在Makefile中执行的时候其会自动原模原样地展开在所使用的地方.其与C/C++所不同的是 ...

  2. centos7上用docker搭建简单的前后端分离项目

    1. 安装docker Docker 要求 CentOS 系统的内核版本高于 3.10 ,首先验证你的CentOS 版本是否支持 Docker . 通过 uname -r 命令查看你当前的内核版本 使 ...

  3. 漏洞复现|Dubbo反序列化漏洞CVE-2019-17564

    01漏洞描述 - Apache Dubbo支持多种协议,官方推荐使用Dubbo协议.Apache Dubbo HTTP协议中的一个反序列化漏洞(CVE-2019-17564),该漏洞的主要原因在于当A ...

  4. Ubuntu系统Root用户无法登录

    默认 系统 root 登录 图形界面,出现 登录失败.解决方法如下: 1,登录普通用户, 打开终端执行命令, 使用su root或sudo -i切换到root用户(必须) su root 按照提示输入 ...

  5. python-scrapy框架学习

    Scrapy框架 Scrapy安装 正常安装会报错,主要是两个原因 0x01 升级pip3包 python -m pip install -U pip 0x02 手动安装依赖 需要手动安装 wheel ...

  6. C#序列化和反序列化 之 dynamic 动态Json的反序列化

    序列化和反序列化的常识不再赘述,如果不清楚这个,可以 参考一下其他人写的文章https://www.cnblogs.com/maitian-lf/p/3670570.html 总结的说, 序列化 是把 ...

  7. C#的生产者和消费者 实例

    class Program { //写线程将数据写入myData static int myData = 100; //读写次数 const int readWriteCount = 20; //fa ...

  8. SpringBoot2.0 防止XSS攻击

    一:什么是XSS XSS攻击全称跨站脚本攻击,是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中. 你可以自己做个简单尝试: 1. 在任何一个表单内,你输 ...

  9. 参数化SQL

    原文:http://www.cnblogs.com/aito/archive/2010/08/25/1808569.html 避免SQL注入的方法有两种:一是所有的SQL语句都存放在存储过程中,这样不 ...

  10. oracle中常用函数

    1.oracle中 trunc 是截取的函数,用在日期类型上,就是截取到的日或时间. select trunc(sysdate) from dual   默认是截取系统日期到日,得到 2012-12- ...