hash-table基础以及一些运用例子

最近在复习算法和数据结构 ,这章把hash表的概念和相关题目进行汇总。

一、前言

1.1、哈希表和数组、以及链表的对比:

(1).数组的特点:寻址容易,插入和删除困难; 数组存储连续,查找一个元素的时间复杂度为O(1);

(2).链表的特点:寻址困难,插入和删除容易。链表存储区是离散的,遍历链表的元素的时间复杂度为O(N)。

(3).hash-table是根据关键值(key-value)来直接进行访问的数据结构,它结合了数组和链表的优点。hash表的难点在于设计hash函数,以及解决冲突。这里我们会在后面提及;

1.2、一个hash表运用的的直观理解(内容取自教材书)

这里是一些联系人的信息,如果要存储这些信息你会怎么做?我们比较直观的想法是,设计一个结构体,用链表来存储。结构体里面包含一个char型数组存放名字,char字符串存放电话号码,和一个结构体指针用来存放下个结构体的地址。

张三 13980593357
李四 15828662334
王五 13409821234
张帅 13890583472

当要查找”王五 15828662334“这条记录是否在这张链表中时,可能会从链表的头结点开始遍历,依次将每个结点中的姓名同”李四“进行比较,直到查找成功或者失败为止,这种做法的时间复杂度为O(n)。即使采用二叉排序树进行存储,也最多为O(logn)。假设能够通过”王五“这个信息直接获取到该记录在表中的存储位置,就能省掉中间关键字比较的这个环节,复杂度直接降到O(1)。Hash表就能够达到这样的效果。

Hash表采用一个映射函数 f : key —> address 将关键字映射到该记录在表中的存储位置,从而在想要查找该记录时,可以直接根据关键字和映射关系计算出该记录在表中的存储位置,通常情况下,这种映射关系称作为Hash函数,而通过Hash函数和关键字计算出来的存储位置(注意这里的存储位置只是表中的存储位置,并不是实际的物理地址)称作为Hash地址。比如上述例子中,假如联系人信息采用Hash表存储,则当想要找到“李四”的信息时,直接根据“李四”和Hash函数计算出Hash地址即可。

二、hash函数的设计

1、整数的hash函数设计

常见的hash函数有三种,分别是:直接取余法、乘积取整法、平方取中法。下面一一介绍:

1.1、直接取余法

直接取余法根据字面意思我们就能理解到,它的基本实现是用关键字直接除以散列表的大小(我们一般取跟元素个数最接近的质数作为散列表的大小)。如果知道Hash表的最大长度为m,可以取不大于m的最大质数p,然后对关键字进行取余运算,h(key)=key%p。很多的书上认为,哈希表的大小最好是选择一个大的质数,并且最好不要和2的整数幂接近。最不好的选择是哈希表的大小恰好是2的整数幂。


这里可以这么认为:计算机是用二进制存储的,当一个二进制数除以一个2的整数幂的时候,结果就是这个二进制数的后几位,前面的位都丢失了,也就意味着丢失了一部分信息,进而导致哈希表中的元素分布不均匀。为了避免产生冲突,我们可以采用加、乘法、移位等等运算关系来进行处理,然后再取余数,获得哈希地址。

下面是个例子。
<span style="font-size:18px;"> static int additiveHash(String key, int prime)  //prime为我们选取的hash表大小。
 {
   int hash, i;
   for (hash = key.length(), i = 0; i < key.length(); i++)
   <span style="white-space:pre">	</span>hash += key.charAt(i);
   return (hash % prime);
 }</span>

1.2、乘积取整法

关键字k乘以一个在(0,1)中的实数(最好是无理数),得到一个(0,1)之间的实数;取出其小数部分,乘以m,再取整数部分,即得K在Hash表中的位置。

1.3、平方取中法

对关键字进行平方运算,然后取结果的中间几位作为Hash地址。假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取{72,89,00}作为Hash地址。

2、字符串的hash函数设计


我们一般是通过某种算法,以把一个字符串"压缩" 成一个整数。当然,一个32位整数是无法对应回一个字符串的,但在程序中,两个字符串计算出的Hash值相等的可能非常小。下面我介绍几个经典的字符串hash函数设计。

2.1"One-Way Hash"算法

这个算法是Blizzard的创作,是一个非常高效的把字符串转换成整数的算法,举个例子,字符串"unitneutralacritter.grp",通过这个算法得到的结果是0xA26067F3。
<span style="font-size:18px;">unsigned long HashString(char *lpszFileName, unsigned long dwHashType)
{
unsigned char *key = (unsigned char *)lpszFileName;
unsigned long seed1 = 0x7FED7FED, seed2 = 0xEEEEEEEE;
int ch;
while(*key != 0)
{
   ch = toupper(*key++);   //toupper是转换为大写
seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2);
seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3;
}
return seed1;
}</span>

运用上面的函数就可以把字符串转化为整数,接下来我们用这个整数就可以通过hash函数产生hash地址了。

<span style="font-size:18px;">int GetHashTablePos(char *lpszString, SOMESTRUCTURE *lpTable, int nTableSize)
{
int nHash = HashString(lpszString), nHashPos = nHash % nTableSize;
if (lpTable[nHashPos].bExists && !strcmp(lpTable[nHashPos].pString, lpszString))
   return nHashPos;
else
   return -1; //Error value
}</span>

其他的字符串转换成整数算法,可以查阅相关书籍,这不再深入分析。

三、hash冲突的解决方法

1、拉链法

最常用的一种解决哈希冲突的方法,我们可以理解为“链表的数组”,如图:

左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。

这里给个例子:设有
m = 5 , H(K) = K mod 5 ,关键字值序例 5 , 21 , 17 , 9 , 15 , 36 , 41 , 24 ,按外链地址法所建立的哈希表如下图所示:

2、开放定址法

 用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。

注意:

①用开放定址法建立散列表时,建表前须将表中所有单元(更严格地说,是指单元中存储的关键字)置空。

②空单元的表示与具体的应用相关。

 按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、线性补偿探测法、随机探测等。

2.1、线性探查法(Linear Probing)

该方法的基本思想是:

将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为

d,d+l,d+2,…,m-1,0,1,…,d-

 即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。

探查过程终止于三种情况:

 (1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);

(2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;

 (3)若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。

利用开放地址法的一般形式,线性探查法的探查序列为:

hi=(h(key)+i)%m 0≤i≤m-1//即di=i

用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:

① 处理溢出需另编程序。一般可另外设立一个溢出表,专门用来存放上述哈希表中放不下的记录。此溢出表最简

单的结构是顺序表,查找方法可用顺序查找。

② 按上述算法建立起来的哈希表,删除工作非常困难。假如要从哈希表 HT 中删除一个记录,按理应将这个记录所

在位置置为空,但我们不能这样做,而只能标上已被删除的标记,否则,将会影响以后的查找。

③ 线性探测法很容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处

理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该

表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味

着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。

2.2、线性补偿探测法

线性补偿探测法的基本思想是:

将线性探测的步长从 1 改为 Q ,即将上述算法中的 j = (j + 1) % m 改为: j = (j + Q) % m ,而且要求 Q 与

m 是互质的,以便能探测到哈希表中的所有单元。

【例】 PDP-11 小型计算机中的汇编程序所用的符合表,就采用此方法来解决冲突,所用表长 m = 1321 ,选用

Q = 25 。

2.3、随机探测

随机探测的基本思想是:

将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先

用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次

序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记

录后也要打上删除标记。

四、与hash表相关的面试题

面试题目1、有一个庞大的字符串数组,然后给你一个单独的字符串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做?

解题思路:有一个方法最简单,老老实实从头查到尾,一个一个比较,直到找到为止,我想只要学过程序设计的人都能把这样一个程序作出来,但要是有程序员把这样的程序交给用户,我只能用无语来评价,或许它真的能工作,但…也只能如此了。

最合适的算法自然是使用HashTable(哈希表),所谓Hash,一般是一个整数,通过某种算法,可以把一个字符串”压缩”成一个整数。这里我把伪代码说一下:

字符串为lpszString,哈希表为hashTble,哈希表大小为tableSize。这里假设我们已经把所以得IP已经放到hash表里面去了。

(1).  intnHash= HashString(lpszString); //这里把字符串转换为一个整数。

(2). nHashPos= nHash%tableSize;    /查找字符串lpszString的Hash值,

(3).判断hashTble[nHashPos]是否存在,存在的话就说明这个字符串在里面。

当然,无论如何,一个32位整数是无法对应回一个字符串的,但在程序中,两个字符串计算出的Hash值相等的可能非常小。

面试题目2、题目:海量日志数据,提取出某日访问百度次数最多的那个IP。

解题思路:IP的数目还是有限的,最多2^32个,所以可以考虑使用hash将ip直接存入内存,然后进行统计。

面试题目3、求最小的K个数,题目:输入n个整数,找出其中最小的K个数,例如输入4、5、1、6、2、7、3、8这八个数字,则最小的4个数是:1、2、3、4。

 解题思路:这个题目的解决思路有很多,我这里列举几种:

1、排序法,先把所有的数进行排序,前面的K个数就是我们要找的k个数。这种算法的时间复杂度为O(NlogN),我们还可以找个时间复杂度更低的哦!

2、利用partition函数来解决,这个代码不在编写,我主要讲下面这个方法。

3、hash表,时间复杂度为O(N),思路如下,用数组实现一个hash表,hash表的大小为最大元素的大小。其次,把所有数压入hash表,然后再从头取出前k个数。

时间: 2024-10-27 17:50:59

hash-table基础以及一些运用例子的相关文章

数据结构基础-Hash Table详解(转)

理解Hash 哈希表(hash table)是从一个集合A到另一个集合B的映射(mapping). 映射是一种对应关系,而且集合A的某个元素只能对应集合B中的一个元素.但反过来,集合B中的一个元素可能对应多个集合A中的元素.如果B中的元素只能对应A中的一个元素,这样的映射被称为一一映射.这样的对应关系在现实生活中很常见,比如: A  -> B 人 -> 身份证号 日期 -> 星座 上面两个映射中,人 -> 身份证号是一一映射的关系.在哈希表中,上述对应过程称为hashing.A中元

哈希表(hash table)基础概念

哈希是什么 引入:我们在学习数组的时候,使用数组元素的下标值即可访问到该元素,所花费的时间是O(1),与数组元素的个数n没有关系,这就是哈希方法的核心思想. 哈希方法:以关键值K为自变量,通过一定的函数关系h(K)(哈希函数)计算出对应的函数值,把这个值解释为结点的存储地址,将结点的关键码(key)和属性数据(value)一起存入此存储单元中.检索时,用同样的函数计算出地址,找到对应的数据. 哈希表:按哈希存储方式构造的存储结构称为哈希表(hash table) 举例:已知线性表关键码值集合为S

交互设计算法基础(4) - Hash Table

1 2 import java.util.Map; 3 4 // Note the HashMap's "key" is a String and "value" is an Integer 5 HashMap<String,Integer> hm = new HashMap<String,Integer>(); 6 7 // Putting key-value pairs in the HashMap 8 hm.put("Ava&

算法导论---------------散列表(hash table)

摘要: 本章介绍了散列表(hash table)的概念.散列函数的设计及散列冲突的处理.散列表类似与字典的目录,查找的元素都有一个key与之对应,在实践当中,散列技术的效率是很高的,合理的设计散函数和冲突处理方法,可以使得在散列表中查找一个元素的期望时间为O(1).散列表是普通数组概念的推广,在散列表中,不是直接把关键字用作数组下标,而是根据关键字通过散列函数计算出来的.书中介绍散列表非常注重推理和证明,看的时候迷迷糊糊的,再次证明了数学真的很重要.在STL中map容器的功能就是散列表的功能,但

stl源码分析之hash table

本文主要分析g++ stl中哈希表的实现方法.stl中,除了以红黑树为底层存储结构的map和set,还有用哈希表实现的hash_map和hash_set.map和set的查询时间是对数级的,而hash_map和hash_set更快,可以达到常数级,不过哈希表需要更多内存空间,属于以空间换时间的用法,而且选择一个好的哈希函数也不那么容易. 一. 哈希表基本概念 哈希表,又名散列表,是根据关键字直接访问内存的数据结构.通过哈希函数,将键值映射转换成数组中的位置,就可以在O(1)的时间内访问到数据.举

算法导论-散列表(Hash Table)

目录 引言 直接寻址 散列寻址 散列函数 除法散列 乘法散列 全域散列 完全散列 碰撞处理方法 链表法 开放寻址法 线性探查 二次探查 双重散列 随机散列 再散列问题 完整源码(C++) 参考资料 内容 1.引言 如果想在一个n个元素的列表中,查询元素x是否存在于列表中,首先想到的就是从头到尾遍历一遍列表,逐个进行比较,这种方法效率是Θ(n):当然,如果列表是已经排好序的话,可以采用二分查找算法进行查找,这时效率提升到Θ(logn);  本文中,我们介绍散列表(HashTable),能使查找效率

php Hash Table(一) Hash Table的结构

Hash Table的结构图: 在上图中发现:Bucket1和Bucket2是hash冲突的双向链表,但是后添加的Bucket2是添加到头部的,可以看到Bucket2的pListLast和pNext指向Bucket1. 对HashTable结构体的字段解释: 1.nTableSize.顾名思义这个是整个哈希表分配的大小(在内部实现的C中分配的数组大小,PHP是动态的但到底层数组是有大小的是静态的),他的大小有一个固定的申请算法,一般是最接近并且大于当前这个数值的2的乘方,描述的可能有点模糊,举个

链接法(chaining)构建散列表(hash table)(C++实现)

问题 最近项目中遇到了一个分布式系统的并发控制问题.该问题可以抽象为:某分布式系统由一个数据中心D和若干业务处理中心L1,L2 ... Ln组成:D本质上是一个key-value存储,它对外提供基于HTTP协议的CRUD操作接口.L的业务逻辑可以抽象为下面3个步骤: read: 根据keySet {k1, ... kn}从D获取keyValueSet {k1:v1, ... kn:vn} do: 根据keyValueSet进行业务处理,得到需要更新的数据集keyValueSet' {k1':v1

C语言-简单哈希表(hash table)

腾讯三面的时候,叫我写了个哈希表,当时紧张没写好···结果跪了··· 回来后粪发涂墙,赶紧写了一个! 什么都不说了···先让我到厕所里面哭一会··· %>_<% 果然现场发挥,以及基础扎实才是important的! 用链地址法解决冲突的哈希表(C语言,VS2008编写.测试): 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <math.h> 4 #include <string.h>

【 python 学习笔记 -- 数据结构与算法 】哈希表 Implementation of a Hash Table

Python内建的字典就是用 hash table实现的.这里我们只是通过实现自己的hash table来加深对hash table 和hash functions的理解. [ 概念1: Mapping (映射)] 字典通过键(Key)来索引.一个key对应一个存储的value.任意不可变的数据类型均可作为key. [ 概念2:Hash Table (哈希表)] Hash Table根据key直接访问在内存存储位置的数据结构,因而加快了查找速度 (O(1)). 下图是一个size为11的空的Ha