「哈希表」是什么？有哪些常用的解决冲突的方法？
很多人谈起“哈希表”，就会直接聚焦到hash函数、“散列”/“杂凑”之类不明所以的大词上，搞的初学者一头雾水，完全不明白这东西是干嘛用的，更不知道什么时候该用它、怎么用它、出现问题如何解决。  那么，这里我就从问题开始，一步步把哈希表的来龙去脉剖析清楚。 想象一下：有一天，你开车到某商场去买东西。买完东西，你想不起自己的车在哪了。  这家商场非常非常大，楼下停车场少说停了上万台车；而你的健忘症又比较厉害……  那么，问题来了：你怎样才能找到自己的车？   一个理想的情形是，你从头到尾一行一行一辆一辆按顺序走一遍就找到了；运气最差时，你搜遍楼下N辆车，发现你的车在末尾——拿术语说，这个复杂度是O(N)。  能不能更快一些呢？  很简单，假如所有车按车牌号顺序排列，你直接往停车场中间走就行了；如果你的车牌号大于中间那辆车，那么你就往停车场后半部分的中间走，否则就往前半场走……依此类推。 如此一来，你最多只需要走ln（N）次“中间”，就能找到你的车了。黑话叫复杂度O(ln N)。  还能不能更给力一些？  可以。 假设这个停车场非常非常大，大到可以给每个车牌号分配一个固定的停车位；那么只要你把自己的车牌号报给看门老头，他拎着你的衣领子往后一丢，你就“吧唧”一下掉自己车顶上了——嗯，你看，一车一位，就是这么任性。 这就叫“查找复杂度O(1)”。 如果用程序实现的话，就是这么一个数组： car park[MAX_CAR_NUM]  第一个场景，你要直接以一个循环遍历park中的每个元素。 第二个场景，你只需先访问MAX_CAR_NUM除以2的那个位置，再根据车牌号大小访问数组前半拉或者后半拉中间的元素即可。 第三个场景，你的车牌号就是数组下标，所以你只需直接访问park[CAR_NUMBER]即可。   那么，第三个设计是不是完全解决问题了呢？  并不是。 很容易看出，第三个方案需要一个超级大的存储空间。 这个空间得有多大呢？ 它必须大到足以和过去未来的一切有效车牌号一一对应，你才可能做到“直接按号访问”。  假设车牌号共8位，每位可以使用26个英文字母或10个阿拉伯数字，那么不同的车牌号共有36^8=2821109907456种。  哪怕每辆车只需一个字节的存储空间，这也是接近3T的空间！  而事实上，哪怕最大的超市，修一个够停一万辆车的停车场也都太夸张了。 你看，这完全行不通啊。 那么，有没有办法在得到O(1)的查找效率的同时、又不付出太大的空间代价呢？  没错，的确是有的。这就是哈希表。  哈希表是怎么玩的呢？  很简单，我们把你的车牌号看作一个8位36进制的数字；为了方便，我们可以把它转换成十进制。 那么，你的车牌号就是一个不大于2821109907456的数字。  现在，我们把你的车牌号除以一万，只取余数——你看，你的车牌号是不是就和0~10000之间的数字对应起来了？  很好，你的车就停在这个数字对应的停车位上，过去开就是了——O(1)的查找效率！  这个“把你的车牌号映射进0~10000之间”的操作，就是所谓的“散列”“杂凑”“哈希”或者hash（当然，实践上，为了尽量减少冲突，哈希表的空间大小会尽量取质数）。 相对于“以key为下标直接访问的数组”，哈希表是“时间换空间”；相对于二分法查找，哈希表又是“以空间换时间”。这种“中庸”的定位使得它在许多场合极为好用。 等等，你发觉不对：我的车尾号3456，我朋友的车也是这个尾号。我们总不能停在同一个位置吧？  你这个方案有瑕疵啊！  没错，hash可能会把不同的数据映射到同一个点上，术语称其为“碰撞”。 由于hash自身的基本原理，碰撞是不可避免的。  怎么解决这个“碰撞”问题呢？  几种解决思路： 1、临时加个“立体车库”，哪里碰撞往哪放。于是车子就可以在同一位置“撂起来”存了。这叫“开链表法”。 2、车库面积肯定是够的。3456号被人占了，你存3457不就好了！ 换句话说，过去的散列函数是 （车牌号 模除 10000），发现碰撞了就换散列函数 （车牌号加1 模除 10000）试一试——这叫“再散列法”。 3、再修个小车库，碰撞了的停小车库去（小车库可以随便停，也可以搞一套别的机制）  总之，如此一来，我们就同时得到了“O（1）的查找效率”和“可接受的空间消耗”。  任何时候，当你有“数量有限”但“不同索引数量极大”的一些数据，必需极高的访问效率同时又不想无端消耗太多的存储空间时，你就可以考虑使用哈希表了。  当然，请注意，因为冲突的存在，哈希表虽然有着优异的平均访问时间（常数访问效率！）；但它的“最大访问时间”却是没有保证的——你可能一个微秒甚至几个纳秒就拿到了数据，也可能几十个毫秒了还在链表上狂奔。因此实时性要求严格的场合，用它前需要谨慎考虑。 知道了哈希表的设计思路，我们就可以进入稍微困难一些的部分了。  我们已经知道，所谓“哈希表”，实际上是我们把对象（value）的“键值（key)”转换成了“数组下标”；然后就可以借助这个下标一步到位的找到对应对象（value）了。  但这中间有“瑕疵”存在：和身份证号和公民一一对应不同，键值和下标并不是一对一的关系。就好像你的车牌尾号是23456而你朋友是53456，结果把你们安排到同一个停车位一样。  很容易想到，很多数据的键值（key）分布存在一定的规律。 比如，身份证其中4位是你的出生年份，这四位其实只能当两位用；再如，手机号码前3位只可能是135、132、153等少数数字…… 那么，如果我们的“键值转换数组下标的函数（也就是哈希函数）”选择不当（比如给手机前几位、身份证的出生年份字段这些相对缺乏变化的数据过高权值），就很容易使得“碰撞”频繁出现。 这就对哈希函数的选择提出了要求。  但是哈希函数本身也不应该过于复杂，不然每次计算耗时太久——O(1)虽然是常数时间；但如果时间常数太长，它可能就不如O（lnN）查找算法快。 要知道，在一百万数据里面做二分法搜索，最差时也不过需要20次搜索而已；如果你的哈希函数本身需要的计算时间已经超过了这个限度，那么改用二分法显然是个更为理智的选择：不仅更快，还更省空间。 工程问题，向来是需要根据实际情况灵活选择、做出合理折衷的。  扯远了。  继续说哈希函数。 很显然，用于哈希表的哈希函数可不能是MD5或者sha1系列函数，太慢；但也不能直接模除一个整数，太容易出现冲突。  简单说就是：哈希表用到的哈希函数，一方面要能尽量把key均匀散布在表空间中（从而尽量减少冲突），另一方面又要有尽量快的计算速度。  这类函数有很多种，稍微搜一搜就能找到很多。 无论如何，原理所限，哈希表中碰撞无法绝对避免。 当碰撞发生时，就不得不使用开链表法或再散列法存储冲突数据；而这必将影响哈希表的性能。  很容易想到，如果哈希表很大、里面却没存几条数据，那么它出现冲突（碰撞）的几率就会很小；反之，如果哈希表已经接近满了，那么每条新加入的数据都会产生碰撞。 换句话说，在哈希函数选择合理的前提下，想要减少碰撞，就只能扩大哈希表占用的空间。  哈希表实际所存数据量和哈希表最大容量之间的比值，叫做哈希表的“加载因子”。 加载因子越小，冲突的概率就越低，但浪费大量空间；加载因子越高，冲突概率越大，但空间浪费就越少。这是一个需要根据工程实践灵活选择的折衷值。很多语言提供的hash表允许你主动调节这个值。 一般来说，一个较为平衡的加载因子大约是0.7~0.8左右。这样既不会浪费太多空间，也不至于出现太多冲突。  另一方面，因为哈希表使用的哈希函数较为简单，因此对恶意的攻击者来说，他可以精心构造一大堆数据提交给你——所有这些数据散列后全都存在一个格子里。 我们前面提到过，当遇到这种冲突/碰撞时，为了避免彼此覆盖，这些数据就要存在链表中（或者再散列后存在同一个哈希表中）。 当这些数据被存进链表时，对它们的访问效率将降到O(N)——因为链表搜索效率只有O(N)。 之前就发生过这种攻击，包括Java在内的许多种语言全部落马。  解决方案也很简单： 1、提高哈希函数复杂度，想办法加入随机性（相当于每次使用一个不同的哈希函数），避免被人轻易捕捉到弱点 2、不要用开链表法存储冲突数据，采用“再散列法”，并且使用不同的哈希函数再散列、还可以把冲突数据存入另一个表——要构造同时让两个以上不同的哈希函数冲突的攻击数据，难度就大得多了。 总之，哈希表是用途广泛的一种数据结构，也是很多编程语言提供的基础服务之一（比如python的dict）。  你完全可以傻瓜式的使用它，无须搞懂它的一切；但想要把它用精、用好，你还是需要真正理解它的来龙去脉——千万不要 像某种最好的语言的作者那样，拿函数名长度当哈希值（哎呀一不小心又黑人了，顶锅逃……  想揍人你们找他：贺师俊：PHP黑系列之二：PHP 为什么函数命名是如此不一致？