数据结构之哈希表

哈希表是种数据结构,它可以提供快速的插入操作和查找操作。第一次接触哈希表时,它的优点多得让人难以置信。不论哈希表中有多少数据,插入和删除(有时包括侧除)只需要接近常量的时间即0(1)的时间级。实际上,这只需要几条机器指令。

  对哈希表的使用者一一人来说,这是一瞬间的事。哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。

  哈希表也有一些缺点它是基于数组的,数组创建后难于扩展某些哈希表被基本填满时,性能下降得非常严重,所以程序虽必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。

  而且,也没有一种简便的方法可以以任何一种顺序〔例如从小到大〕遍历表中数据项。如果需要这种能力,就只能选择其他数据结构。

然而如果不需要有序遍历数据,井且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。

哈希表算法-哈希表的概念及作用

  一般的线性表,树中,记录在结构中的相对位置是随机的,即和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较。这一类查找方法建立在“比较“的基础上,查找的效率依赖于查找过程中所进行的比较次数。

  理想的情况是能直接找到需要的记录,因此必须在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。

哈希表最常见的例子是以学生学号为关键字的成绩表,1号学生的记录位置在第一条,10号学生的记录位置在第10条...

如果我们以学生姓名为关键字,如何建立查找表,使得根据姓名可以直接找到相应记录呢?

哈希表算法 哈希表算法

用上述得到的数值作为对应记录在表中的位置,得到下表:

哈希表算法 哈希表算法

上面这张表即哈希表。

如果将来要查李秋梅的成绩,可以用上述方法求出该记录所在位置:

李秋梅:lqm 12+17+13=42 取表中第42条记录即可。

问题:如果两个同学分别叫 刘丽 刘兰 该如何处理这两条记录?

这个问题是哈希表不可避免的,即冲突现象:对不同的关键字可能得到同一哈希地址。

哈希表算法-哈希表的构造方法(哈希函数)

1、直接定址法

例如:有一个从1到100岁的人口数字统计表,其中,年龄作为关键字,哈希函数取关键字自身。

           但这种方法效率不高,时间复杂度是O(1),空间复杂度是O(n),n是关键字的个数

哈希表算法 哈希表算法

2、数字分析法

有学生的生日数据如下:

年.月.日

75.10.03
75.11.23
76.03.02
76.07.12
75.04.21
76.02.15
...

经分析,第一位,第二位,第三位重复的可能性大,取这三位造成冲突的机会增加,所以尽量不取前三位,取后三位比较好。

3、平方取中法

取关键字平方后的中间几位为哈希地址

4、折叠法

将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址,这方法称为折叠法。

例如:每一种西文图书都有一个国际标准图书编号,它是一个10位的十进制数字,若要以它作关键字建立一个哈希表,当馆藏书种类不到10,000时,可采用此法构造一个四位数的哈希函数。如果一本书的编号为0-442-20586-4,则:

哈希表算法 哈希表算法

5、除留余数法

取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址。

H(key)=key MOD p (p<=m)

6、随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即

H(key)=random(key) ,其中random为随机函数。通常用于关键字长度不等时采用此法。

5、除留余数法

取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址。

H(key)=key MOD p (p<=m)

6、随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即

H(key)=random(key) ,其中random为随机函数。通常用于关键字长度不等时采用此法。

5、除留余数法

取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址。

H(key)=key MOD p (p<=m)

6、随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即

H(key)=random(key) ,其中random为随机函数。通常用于关键字长度不等时采用此法。

哈希表算法-处理冲突的方法


哈希表算法

如果两个同学分别叫 刘丽 刘兰,当加入刘兰时,地址24发生了冲突,我们可以以某种规律使用其它的存储位置,如果选择的一个其它位置仍有冲突,则再选下一个,直到找到没有冲突的位置。选择其它位置的方法有:

1、开放定址法

Hi=(H(key)+di) MOD m i=1,2,...,k(k<=m-1)

其中m为表长,di为增量序列

如果di值可能为1,2,3,...m-1,称线性探测再散列。

如果di取值可能为1,-1,2,-2,4,-4,9,-9,16,-16,...k*k,-k*k(k<=m/2)

称二次探测再散列

如果di取值可能为伪随机数列。称伪随机探测再散列。

例:在长度为11的哈希表中已填有关键字分别为17,60,29的记录,现有第四个记录,其关键字为38,由哈希函数得到地址为5,若用线性探测再散列,如下:

哈希表算法 哈希表算法

2、再哈希法

当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。

3、链地址法

将所有关键字为同义词的记录存储在同一线性链表中。

哈希表算法 哈希表算法

4、建立一个公共溢出区

假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。

 

哈希表算法的编写

hash表,有时候也被称为散列表。个人认为,hash表是介于链表和二叉树之间的一种中间结构。链表使用十分方便,但是数据查找十分麻烦;二叉树中的数据严格有序,但是这是以多一个指针作为代价的结果。hash表既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

    打个比方来说,所有的数据就好像许许多多的书本。如果这些书本是一本一本堆起来的,就好像链表或者线性表一样,整个数据会显得非常的无序和凌乱,在你找到自己需要的书之前,你要经历许多的查询过程;而如果你对所有的书本进行编号,并且把这些书本按次序进行排列的话,那么如果你要寻找的书本编号是n,那么经过二分查找,你很快就会找到自己需要的书本;但是如果你每一个种类的书本都不是很多,那么你就可以对这些书本进行归类,哪些是文学类,哪些是艺术类,哪些是工科的,哪些是理科的,你只要对这些书本进行简单的归类,那么寻找一本书也会变得非常简单,比如说如果你要找的书是计算机方面的书,那么你就会到工科一类当中去寻找,这样查找起来也会显得麻烦。

    不知道这样举例你清楚了没有,上面提到的归类方法其实就是hash表的本质。下面我们可以写一个简单的hash操作代码。

    a)定义hash表和基本数据节点

  1. typedef struct _NODE 
  2.     int data; 
  3.     struct _NODE* next; 
  4. }NODE; 
  5.  
  6. typedef struct _HASH_TABLE 
  7.     NODE* value[10]; 
  8. }HASH_TABLE; 
typedef struct _NODE
{
	int data;
	struct _NODE* next;
}NODE;

typedef struct _HASH_TABLE
{
	NODE* value[10];
}HASH_TABLE;


    b)创建hash表

  1. HASH_TABLE* create_hash_table() 
  2.     HASH_TABLE* pHashTbl = (HASH_TABLE*)malloc(sizeof(HASH_TABLE)); 
  3.     memset(pHashTbl, 0, sizeof(HASH_TABLE)); 
  4.     return pHashTbl; 
HASH_TABLE* create_hash_table()
{
	HASH_TABLE* pHashTbl = (HASH_TABLE*)malloc(sizeof(HASH_TABLE));
	memset(pHashTbl, 0, sizeof(HASH_TABLE));
	return pHashTbl;
}


    c)在hash表当中寻找数据

  1. NODE* find_data_in_hash(HASH_TABLE* pHashTbl, int data) 
  2.     NODE* pNode; 
  3.     if(NULL ==  pHashTbl) 
  4.         return NULL; 
  5.  
  6.     if(NULL == (pNode = pHashTbl->value[data % 10])) 
  7.         return NULL; 
  8.  
  9.     while(pNode){ 
  10.         if(data == pNode->data) 
  11.             return pNode; 
  12.         pNode = pNode->next; 
  13.     } 
  14.     return NULL; 
NODE* find_data_in_hash(HASH_TABLE* pHashTbl, int data)
{
	NODE* pNode;
	if(NULL ==  pHashTbl)
		return NULL;

	if(NULL == (pNode = pHashTbl->value[data % 10]))
		return NULL;

	while(pNode){
		if(data == pNode->data)
			return pNode;
		pNode = pNode->next;
	}
	return NULL;
}


    d)在hash表当中插入数据

  1. STATUS insert_data_into_hash(HASH_TABLE* pHashTbl, int data) 
  2.     NODE* pNode; 
  3.     if(NULL == pHashTbl) 
  4.         return FALSE; 
  5.  
  6.     if(NULL == pHashTbl->value[data % 10]){ 
  7.         pNode = (NODE*)malloc(sizeof(NODE)); 
  8.         memset(pNode, 0, sizeof(NODE)); 
  9.         pNode->data = data; 
  10.         pHashTbl->value[data % 10] = pNode; 
  11.         return TRUE; 
  12.     } 
  13.  
  14.     if(NULL != find_data_in_hash(pHashTbl, data)) 
  15.         return FALSE; 
  16.  
  17.     pNode = pHashTbl->value[data % 10]; 
  18.     while(NULL != pNode->next) 
  19.         pNode = pNode->next; 
  20.  
  21.     pNode->next = (NODE*)malloc(sizeof(NODE)); 
  22.     memset(pNode->next, 0, sizeof(NODE)); 
  23.     pNode->next->data = data; 
  24.     return TRUE; 
STATUS insert_data_into_hash(HASH_TABLE* pHashTbl, int data)
{
	NODE* pNode;
	if(NULL == pHashTbl)
		return FALSE;

	if(NULL == pHashTbl->value[data % 10]){
		pNode = (NODE*)malloc(sizeof(NODE));
		memset(pNode, 0, sizeof(NODE));
		pNode->data = data;
		pHashTbl->value[data % 10] = pNode;
		return TRUE;
	}

	if(NULL != find_data_in_hash(pHashTbl, data))
		return FALSE;

	pNode = pHashTbl->value[data % 10];
	while(NULL != pNode->next)
		pNode = pNode->next;

	pNode->next = (NODE*)malloc(sizeof(NODE));
	memset(pNode->next, 0, sizeof(NODE));
	pNode->next->data = data;
	return TRUE;
}


    e)从hash表中删除数据

  1. STATUS delete_data_from_hash(HASH_TABLE* pHashTbl, int data) 
  2.     NODE* pHead; 
  3.     NODE* pNode; 
  4.     if(NULL == pHashTbl || NULL == pHashTbl->value[data % 10]) 
  5.         return FALSE; 
  6.  
  7.     if(NULL == (pNode = find_data_in_hash(pHashTbl, data))) 
  8.         return FALSE; 
  9.  
  10.     if(pNode == pHashTbl->value[data % 10]){ 
  11.         pHashTbl->value[data % 10] = pNode->next; 
  12.         goto final; 
  13.     } 
  14.  
  15.     pHead = pHashTbl->value[data % 10]; 
  16.     while(pNode != pHead ->next) 
  17.         pHead = pHead->next; 
  18.     pHead->next = pNode->next; 
  19.  
  20. final: 
  21.     free(pNode); 
  22.     return TRUE; 
STATUS delete_data_from_hash(HASH_TABLE* pHashTbl, int data)
{
	NODE* pHead;
	NODE* pNode;
	if(NULL == pHashTbl || NULL == pHashTbl->value[data % 10])
		return FALSE;

	if(NULL == (pNode = find_data_in_hash(pHashTbl, data)))
		return FALSE;

	if(pNode == pHashTbl->value[data % 10]){
		pHashTbl->value[data % 10] = pNode->next;
		goto final;
	}

	pHead = pHashTbl->value[data % 10];
	while(pNode != pHead ->next)
		pHead = pHead->next;
	pHead->next = pNode->next;

final:
	free(pNode);
	return TRUE;
}


总结:

    1、hash表不复杂,我们在开发中也经常使用,建议朋友们好好掌握;

    2、hash表可以和二叉树形成复合结构,至于为什么,建议朋友们好好思考一下?

 

 

哈希表算法-哈希表的实际应用

以上就是一些关于hash以及其相关的一些基本预备知识。那么在emule里面他具体起到什么作用呢?
大家都知道emule是基于P2P (Peer-to-peer的缩写,指的是点对点的意思的软件), 它采用了"多源文件传输协议”( MFTP,the Multisource FileTransfer Protocol)。在协议中,定义了一系列传输、压缩和打包还有积分的标准,emule 对于每个文件都有md5-hash的算法设置,这使得该文件独一无二,并且在整个网络上都可以追踪得到。
什么是文件的hash值呢?
MD5-Hash-文件的数字文摘通过Hash函数计算得到。不管文件长度如何,它的Hash函数计算结果是一个固定长度的数字。与 加密算法不同,这一个Hash算法是一个不可逆的单向函数。采用安全性高的Hash算法,如MD5、SHA时,两个不同的文件几乎不可能得到相同的Hash结果。因此,一旦文件被修改,就可检测出来。
当我们的文件放到emule里面进行共享发布的时候,emule会根据hash算法自动生成这个文件的hash值,他就是这个文件唯一的身份标志,它包含了这个文件的基本信息,然后把它提交到所连接的服务器。当有他人想对这个文件提出下载请求的时候, 这个hash值可以让他人知道他正在下载的文件是不是就是他所想要的。尤其是在文件的其他属性被更改之后(如名称等)这个值就更显得重要。而且服务器还提供了,这个文件当前所在的用户的地址,端口等信息,这样emule就知道到哪里去下载了。
一般来讲我们要搜索一个文件,emule在得到了这个信息后,会向被添加的服务器发出请求,要求得到有相同hash值的文件。而服务器则返回持有这个文件的用户信息。这样我们的 客户端就可以直接的和拥有那个文件的用户沟通,看看是不是可以从他那里下载所需的文件。
对于emule中文件的hash值是固定的,也是唯一的,它就相当于这个文件的信息摘要,无论这个文件在谁的机器上,他的hash值都是不变的,无论过了多长时间,这个值始终如一,当我们在进行文件的下载上传过程中,emule都是通过这个值来确定文件。
那么什么是userhash呢?
道理同上,当我们在第一次使用emule的时候,emule会自动生成一个值,这个值也是唯一的,它是我们在emule世界里面的标志,只要你不卸载,不删除config,你的userhash值也就永远不变,积分制度就是通过这个值在起作用,emule里面的积分保存,身份识别,都是使用这个值,而和你的id和你的用户名无关,你随便怎么改这些东西,你的userhash值都是不变的,这也充分保证了公平性。其实他也是一个信息摘要,只不过保存的不是文件信息,而是我们每个人的信息。
那么什么是hash文件呢?
我们经常在emule日志里面看到,emule正在hash文件,这里就是利用了hash算法的 文件校验性这个功能了,文章前面已经说了一些这些功能,其实这部分是一个非常复杂的过程,目前在ftp,bt等软件里面都是用的这个基本原理,emule里面是采用文件分块传输,这样传输的每一块都要进行对比校验,如果错误则要进行重新下载,这期间这些相关信息写入met文件,直到整个任务完成,这个时候part文件进行重新命名,然后使用move命令,把它传送到incoming文件里面,然后met文件自动删除,所以我们有的时候会遇到hash文件失败,就是指的是met里面的信息出了错误不能够和part文件匹配,另外有的时候开机也要疯狂hash,有两种情况一种是你在第一次使用,这个时候要hash提取所有文件信息,还有一种情况就是上一次你 非法关机,那么这个时候就是要进行排错校验了。
关于hash的算法研究,一直是信息科学里面的一个前沿,尤其在 网络技术普及的今天,他的重要性越来越突出,其实我们每天在网上进行的信息交流安全验证,我们在使用的操作系统 密钥原理,里面都有它的身影,特别对于那些研究信息安全有兴趣的朋友,这更是一个打开信息世界的钥匙,他在hack世界里面也是一个研究的焦点。
一般的线性表、树中,记录在结构中的相对位置是随机的即和记录的关键字之间不存在确定的关系,在结构中查找记录时需进行一系列和关键字的比较。这一类查找方法建立在“比较”的基础上,查找的效率与比较次数密切相关。理想的情况是能直接找到需要的记录,因此必须在记录的存储位置和它的关键字之间建立一确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。因而查找时,只需根据这个对应关系f找到给定值K的像f(K)。若结构中存在关键字和K相等的记录,则必定在f(K)的存储位置上,由此不需要进行比较便可直接取得所查记录。在此,称这个对应关系f为 哈希函数,按这个思想建立的表为哈希表(又称为杂凑法或 散列表)。
哈希表不可避免冲突(collision)现象:对不同的关键字可能得到同一哈希地址 即key1≠key2,而hash(key1)=hash(key2)。具有相同函数值的关键字对该 哈希函数来说称为同义词(synonym)。因此,在建造哈希表时不仅要设定一个好的 哈希函数,而且要设定一种处理冲突的方法。可如下描述哈希表:根据设定的 哈希函数H(key)和所选中的处理冲突的方法,将一组 关键字映象到一个有限的、地址连续的地址集(区间)上并以关键字在地址集中的“象”作为相应记录在表中的存储位置,这种表被称为哈希表。
对于动态 查找表而言,1) 表长不确定;2)在设计查找表时,只知道关键字所属范围,而不知道确切的关键字。因此,一般情况需建立一个函数关系,以f(key)作为关键字为key的录在表中的位置,通常称这个函数f(key)为 哈希函数。(注意:这个函数并不一定是数学函数)
哈希函数是一个映象,即:将关键字的集合映射到某个地址集合上,它的设置很灵活,只要这个地址集合的大小不超出允许范围即可。
现实中 哈希函数是需要构造的,并且构造的好才能使用的好。
用途:加密,解决冲突问题。
用途很广,比特精灵中就使用了 哈希函数,你可以自己看看。
具体可以学习一下 数据结构和算法的书。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值