MIT算法导论第七讲学习笔记-哈希表(Hashing)

MIT算法导论学习第七讲+第八讲:哈希表

-1哈希表的定义

哈希表,又称散列表,其定义是根据一个哈希函数将集合S中的关键字映射到一个表中,这个表就称为哈希表,而这种方法就称为Hashing。

1.1引入哈希表

我们先来一个直观的理解:如果一个集合S中的关键字

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

并且关键字各不相同,那么我们按如下公式建立一个Array T[0,1…m-1]:

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

这个公式什么意思呢?就是我们建立这样的一个表,对于关键字k,我们把它存储在T[k],的位置上,那么对他们的访问用时为O(1),不需查找。

这个方法有一个缺点,如果关键字的范围比较大(如:0~2^32-1,32位所能表示的最大长度),那么我们建立的这个表的长度那么就大到无法接受了。假设有两个关键字:0和2^32,那么按上述的方法就要直接建立一个长度为2^32的表,显然这是不现实的。

怎么解决呢??这便是哈希表的引入。

哈希表就是把上述的直接映射,变成一个函数映射,这个函数就是哈希函数,如下图:

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

此时再来看哈希函数,哈希函数就是你给我一个值,我就能通过这个函数计算出它的存储地址,然后就能进行操作了。哈希表做到了什么呢?哈希表可以使得在表小的情况下仍能够保存数据,并且能够在常数时间O(1)内完成查询。为什么是常数时间呢?因为我们不需要比较,不需要遍历查找,只要计算出地址,有待查询的值那么就找到了;没有,那么表中就不存在。

1.2 怎样设计哈希函数

当然现实中要操作的不止两个元素,那么我们怎样设计一个哈希函数呢??

这里有两个要点

一个好的哈希函数应该能够将关键字均匀的分配到哈希表T中;

而关键字的分布率不应该影响这种均匀性。

第二点也许现在还不能很好的理解,先放下,先看一个最常用的哈希函数:

 1.3除模取余法

假设所有关键字k属于K都是整数,定义:

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

这个函数依赖于对除数m的选择,一般选择m是一个质数,并且与2或者10的幂次不靠近(因为这两个是日常缩减的最能体现规律性数的基数)。其
缺陷在于除法在机器中需要更多的时钟周期完成。

来看两个不好的m的选择:

1. m=2时,如果所有的关键字都是偶数,这时会有什么后果呢??

表中的所有的奇数位置都没用到,这便是关键字的分布规律(全偶)影响到了映射的均匀性。

2. 再看,当m=2^r时,我们把关键字用二进制表示,那么如下的一个关键字:《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

只与后六位有关

-2 哈希函数的分析

我们来对上述的方法进行分析。

假设我们要将12个数映射到一个哈希表,将表长定义为13(质数),如下图所示

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

按照上述方法,前四个都没有问题,都映射到了对应的位置,但是第5个关键字25和第一个关键字都映射到了位置12,这怎么办呢??

当一个待插入的关键字映射到一个已经被占用的位置时,便产生了一次冲突(Collision),
冲突可以通过选择好的哈希函数来减少,但是很难完全避免冲突

怎样解决冲突呢???

-3 冲突解决方法

3.1 开放地址法

一有冲突那么便去寻找下一个空的地址,只要表足够大,空的地址总能找到,即:

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

e.g. 

上面25在12的位置上发生了冲突,那么便去寻找下一个位置,即

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

那么便把25存储在0的位置上。

再继续往下时,关键字29又与16发生了冲突,都映射到了位置3,那么继续用上面的方法:

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

而在4的位置又与56,发生了冲突,那么继续:

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

这样在5的位置终于有了位置,存储在位置5.

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

其他的以此类推。

值得注意的是,当di选择不同时,有不同的探查方法(寻找空位的方法),这里注重思想的理解,其他方法便略去了。

3.2 拉链法(链地址法)

上述的方法是一出现冲突就换地方,那么一个想法是:凭什么它在那儿而我要换呢??

链地址法就是不换地址,而是在原来的位置后面加一个节点,如下图:

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

以后的冲突就这样解决了,而且这种方法有一个优点,就是不管有多少冲突它都能解决。

然而,优点也是缺点,假设所有的关键字都映射到了一个位置,那么后果就是这个散列表退化成了链表,查找链表的时间是O(n),这可不是散列表的初衷。

3.3 公共溢出区

这个方法更好理解,所有的有冲突的都去一个事先开辟好的空间,叫做公共溢出区。这样当25与12发生冲突了,那么25便去公共溢出区去呆着了,在查找的时候除了要查询映射到基本表的位置外,还要查找公共溢出区,这样没找到才是真的没有了。

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

理解了哈希思想后,我们再看其他的常用哈希函数构造方法:直接定址法、数字分析法、平方取中法、折叠法、随机数法等。这些方法在理解了哈希思想之后就会发现它只是一个映射地址的函数,在不同的情况下可能某个性能更好。

-4 代码实现

本节我们对上述的例子进行代码实现,采用除留取余法作为哈希函数,采用开放地址法来解决冲突。

4.1 分段代码

首先,定义表长13,并且,定义一个表示空表的数-32768,和哈希表的结构,定义如下:

typedef int datatType ;
#define HashSize 13
#define NULLKEY -32768
/*Hash Table*/
typedef struct
{
	int elem[HashSize] ;
	int count ;
}HashTable;

其次是哈希函数:

/*Hashing_Mod*/
int Hashing_Mod(datatType key)
{
	return key%HashSize ;
}

然后,我们定义插入函数:

/*HashInsert*/
void HashInsert(HashTable *H, datatType key)
{
	int addr = Hashing_Mod(key) ;
	while (H->elem[addr] != NULLKEY)
	{
		addr = (addr + 1)% HashSize ;
	}
	H->elem[addr] = key ;
}

最后,我们定义查找函数:

/*SearchHashTable*/
int SearchHashTable(HashTable *H, datatType key, int *addr)
{
	*addr = Hashing_Mod(key) ;

	while(H->elem[*addr] != key)
	{
		*addr = (*addr + 1)% HashSize ;
		if (H->elem[*addr] == NULLKEY || *addr == Hashing_Mod(key))
		{
			return 0 ;
		}
	}
	return 1 ;
}

4.2 整体代码

整个代码如下:

#include <stdio.h>
#include <stdlib.h>

typedef int datatType ;
#define HashSize 13
#define NULLKEY -32768

/*Hash Table*/
typedef struct
{
	int elem[HashSize] ;
	int count ;
}HashTable;
/*InitHashTable*/
int InitHashTable(HashTable *&H)
{
	
	H = (HashTable *)malloc(sizeof(HashTable)) ;
	H->count = HashSize ;
	int i ;
	//H->elem = (int *)malloc(HashSize*sizeof(int)) ;
	for(i = 0; i < HashSize; i ++)
		H->elem[i] = NULLKEY ;

	return 1 ;
}

/*Hashing_Mod*/
int Hashing_Mod(datatType key)
{
	return key%HashSize ;
}

/*HashInsert*/
void HashInsert(HashTable *H, datatType key)
{
	int addr = Hashing_Mod(key) ;
	while (H->elem[addr] != NULLKEY)
	{
		addr = (addr + 1)% HashSize ;
	}
	H->elem[addr] = key ;
}

/*SearchHashTable*/
int SearchHashTable(HashTable *H, datatType key, int *addr)
{
	*addr = Hashing_Mod(key) ;

	while(H->elem[*addr] != key)
	{
		*addr = (*addr + 1)% HashSize ;
		if (H->elem[*addr] == NULLKEY || *addr == Hashing_Mod(key))
		{
			return 0 ;
		}
	}
	return 1 ;
}
//==================Auxiliary Code=================
/*PrintArray*/
void PrintArray(datatType *A, int n)
{
	int i ;
	for (i = 0; i < n; i ++)
	{
		printf("%d\t",A[i]) ;
	}
	printf("\n") ;
}

int main()
{
	//===================hashing================
	HashTable *H ;
	InitHashTable(H) ;
	int key[] = {12,67,56,16,25,37,22,29,15,47,48,34} ;
	for(int i = 0; i < HashSize - 1; i ++)
	{
		HashInsert(H,key[i]) ;
	}
	PrintArray(H->elem,HashSize) ;
	int *addr = (int *)malloc(sizeof(int)) ;
	int ans = SearchHashTable(H,47,addr) ;
	if (0 == ans)
	{
		printf("No Match !\n") ;
	}
	else
	{
		printf("Key is hashed at: %d\n",*addr) ;
	}
	return 0 ;
}

4.3程序运行结果与分析

程序查找的是47,在位置8(是从0开始的)。

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

注意上述的查找方法,查找结束的条件是要么找到了空的位置(有空位子没有填充,说明表里没有这个关键字,因为如果有的话它不会隔着一个空的位置来找下一个位置的,因为是线性查询。),要么查询返回了初始映射位置(说明找了一圈所有可能的位置都没有)

我们查找一个不存在的关键字55,结果如下:

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

最后采用这种方法最终构建的哈希表为:

《MIT算法导论第七讲学习笔记-哈希表(Hashing)》

    原文作者:哈希算法
    原文地址: https://blog.csdn.net/ai552368625/article/details/39126595
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞