由“两数之和”谈谈哈希表

无分号教派
• 阅读 2633

为了响应陈皓老师的 ARTS 挑战,之后打算每周至少在 LetCode 上做一道算法题。那么就从最简单的开始做起吧。

LetCode 算法题 - 两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例:

给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

LetCode 题解

解题思路有两种,一种是遍历两次去查找元素的暴力解法;另一种则要用到哈希表对时间复杂度进行优化:LetCode 官方题解

暴力解法在此就不过多叙述,这篇文章我想重点来讲讲 Hash Table 这种数据结构。

Hash Table - 哈希表

Hash Table 中文译名 散列表 ,又名 哈希表 或者 Hash 表。散列表利用了数组支持按照下标随机访问数据的特性,由数组扩展而来。

基本概念

首先在这里定义一下下文中有关 Hash Table 的基本概念:

  • 键(key):用于操作数据的标示,例如 PHP 数组中的索引,或者字符串键等等
  • 槽(slot/bucket):哈希表中用于保存数据的一个单元,也就是数据真正存放的容器
  • 哈希函数(hash function):将 key 映射 (map) 到数据应该存放的 slot 所在位置的函数
  • 哈希冲突(hash collision):哈希函数将两个不同的 key 映射到同一个索引的情况

散列思想

想要弄清散列表,就一定要先了解数组的实现:1.使用连续的内存空间储存 2.储存一组相同类型的数据

数组能够具有按照下标随机访问数据的特性,也是通过上面两条实现。首先获取数据类型的长度 type_size,然后根据数组下标 k 和数组的首地址 base_address,利用寻址公式 a[k]_address = base_address + k * type_size 即可实现时间复杂度 O(1) 的情况下随机访问数据。

而散列表的思想,便是通过散列函数将键转换为数组下标,然后根据数组下标存取数据,即 hash(key) => index。实际上,PHP 关联数组的设计便是利用了散列表这一思想。在 PHP 关联数组中,每个键关联一个值,即 key => value,通过合理的散列函数的设计,便可以将 key 从字符串转换为数组的数字下标 index,利用上文所述寻址公式到数组中存取 arr[index]

散列函数的设计

散列函数的基本要求有以下三点:

  • 散列函数计算得到的散列值是一个非负整数
  • 相同的 key 根据相同的散列函数得到的 value 相同
  • 不同的 key 根据相同的散列函数得到的 value 不同

第一条很好理解,由于数组下标都是非负整数,散列函数计算得到的值也应该为非负整数;第二条讲的是散列函数具有幂等性,使用同样的参数通过散列函数,都应该得到相同的返回值;而第三点,实际情况下想要完全实现并不是一件简单的事情。即使是一些知名的(例如 MD5)哈希算法,也无法避免这种散列冲突,再加上数组的存储空间有限,也会加大散列冲突的概率。

目前解决散列冲突常见的方法有 开放寻址法链表法,而 PHP 在处理散列冲突时则使用了链表法,这里简单介绍一下。

散列表中,每个槽 slot 对应一条链表,当出现散列冲突时,将值存放到对应槽中对链表中。

插入时,只需要通过散列函数 hash() 计算 key 对应的散列槽位 slot,将其插入对应链表中即可。所以插入的时间复杂度为O(1)。

查找或者删除时,时间复杂度与链表的长度 k 成正比,也就是 O(k)。对于散列比较均匀的散列函数,理论上 k = n/m。其中 n 表示散列中数据的个数,m 表示散列表中槽 slot 的个数。

值得注意的是,如果别有用心的人设计大量不同的 key 经过散列后都储存到同一个 slot 时,哈希表便会退化为一个链表,操作链表的时间复杂度为 O(n)。也就是说,如果原来散列表中有 10 万条数据,退化为链表之后,查询的效率就下降了 10 万倍。这样可能会导致大量 CPU 和线程资源用来处理查询操作,导致无法响应其他请求,使服务器遭受 DoS (服务拒绝攻击)。

因此,设计散列函数时,要保证以下两点:

  • 生成的值要尽可能的随机且均匀分布
  • 设计不能太复杂

其中设计太复杂是因为通过散列函数计算对应槽位时,也会有时间消耗。

小结

本篇文章主要是从 LetCode 的 “两数之和” 引发的思考,由于缺乏对数据结构的了解,看到这个题目时的思路只局限在暴力解法,很难想到利用哈希表查找时时间复杂度为 O(1) 的特性。希望之后能够对数据结构更深入了解,开拓自己的思路。

参考资料:

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Peter20 Peter20
4年前
mysql中like用法
like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
分布式系统的主键生成方案对比 | 京东云技术团队
UUID​UUID(通用唯一识别码)是由32个十六进制数组成的无序字符串,通过一定的算法计算出来。为了保证其唯一性,UUID规范定义了包括网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,以及从这些元素生成UUID的算法。
贾蔷 贾蔷
4星期前
力扣15题三数之和解法(C++双指针算法详解)
一、题目解读15题()要求在一个包含n个整数的中,找出所有三个数之和为0的组合,且每个组合的元素不能重复。题目考察数组遍历、与技巧的结合,是经典的多问题,对时间复杂度优化有较高要求。二、解题思路采用“双指针”策略:首先对原数组排序,然后固定第一个数,通过左