《算法导论》学习总结——第三部分2Hash表

        Hash表是对普通数组的推广,因为可以对数组进行直接寻址,所以可以在O(1)的时间内访问数组的任意元素。为了说明Hash的优势和必要,我们首先引入直接寻址表的介绍。

        当关键字的全域U比较小时,直接寻址就简单有效了,其实就是数组,我们直接用关键字对应下表值,不论查找还是插入删除,都只需O(1)的时间。但是直接寻址存在一个问题:如果域U很大,那机器分配内存的时候,会有问题(比如以前排序的时候,定义一个100w的数组,都会stack overflow);另一方面,如果实际存储的关键字集合K相对于U来说可能很小,因为会造成空间的极大浪费。

        说到这,突然想到,计数排序与桶排序,计数排序在直接展开时,如果差距较大,空间会浪费很多的,辅助数组C会有很多元素为0,所以引入桶,将元素都集中起来,浪费的概率会很低,可以看做一个原理吧。

        说到Hash表,核心在于Hash函数的使用,说白了,散列表,其实是将元素通过Hash函数映射到数组中进行存储的方式,映射过程中可能存在冲突的情况,即不同元素映射到了同一个位置,所以需要有解决冲突的方案。

        我们先来试试链接法解决碰撞。链接法的基本思想是把散列到同一位置的元素都放到一个链表中(这个其实和桶排序的那个很像了吧,哈哈)这样,冲突必然是没有了,那么对他的操作运行时间怎么样呢?插入操作最坏情况运行时间为O(1),插入过程很快,因为肯定假设插入的x不在表中啊,找到位置,插入即可(当然也可以在插入前进行搜索,确定是否已经存在);查找操作的最坏情况与表长度成正比。删除也可以在O(1)内完成。(不过有单链表和双链表实现的差别,双链表可以到这个量级,单链表貌似不行吧)链接法散列的性能怎么样呢?最差情况下,所有关键字都在一个链表上,Θ(n);平均情况下,O(1)内可以完成全部的字典操作。

       Hash函数,一个好的Hash函数,应该每个关键字都等可能的分布到m个槽位的任何一个中去。说到Hash,貌似TankyWoo很喜欢Hash,以前看他代码输出数组都是hash_print()。像最简单的Hash函数,除法散列,乘法散列,全域散列。个人喜欢除法散列,简单。TankyWoo列出的比较如下:


         下面我们实现一下最简单的开放寻址法吧,开放寻址,好处就是不用指针,这样就省了空间啊,潜在效果就是减少了碰撞,提高了查找速度,下面我们也来简单实现下书上的HASH-INSERT和HASH-SEARCH2个伪代码吧,http://www.cnblogs.com/xiangshancuizhu/articles/1894916.html 也有个不错的实现,可以参考一下

          我的实现如下:

//=======================================================  
// Name        : Hash.cpp  
// Author      : xia  
// Description : Hash函数开放寻址法的一个示例  
//======================================================= 
#include <iostream>
#include <ctime>
using namespace std;

const int NIL= -1;//为了简便,NIL设为-1
const int m = 20; //hash表大小

void InitHash(int T[m])
{
	for (int i=0 ; i<m ; i++)
		T[i] = NIL;
}
void PrintHash(int T[m])
{
	for (int i=0 ; i< m ;i++)
		cout << T[i] << "  " ;
	cout << endl;
}
int HashFunction(int value ,int i)
{//Hash函数要求对探查序列h(k,0),h(k,..),h(k,m)
 //必须是<0,1....m-1>的一个排列,取简,我们取余
	return (value+i)%m;
}
int HashInsert(int T[m], int k)
{
	for (int i=0 ; i<m ; i++)
	{
		int j = HashFunction(k,i);
		if (T[j] == NIL)
		{
			T[j] = k ;
			return j ;
		}
	}
	return NIL;//Hash overflow
}
int HashSearch(int T[m] ,int k)
{//在Hash表T中查找关键字k
	int j=HashFunction(k,0);
	for (int i=0 ; T[j] != NIL && i<m ; i++)
	{
		j = HashFunction(k,i);
		if (T[j] == k)
			return j;
	}
	return NIL ;
}
void HashDelete(int T[m] ,int k)
{//在Hash表T中删除关键字k
	int j=HashSearch(T,k);//先找
	if (j!=NIL)
		T[j]=NIL;
	else
		cout << "Error" << endl;
}
int main(int argc ,char *argv[])
{
	int Hash[m];
	InitHash(Hash);
	PrintHash(Hash);

	srand(time(NULL));
	int test[m];
	for (int i=0 ; i<m ; i++)
	{
		test[i] = rand()%10; //为了验证查找,必须
		cout << test[i] << "  " ;
		HashInsert(Hash,test[i]);
	}
	cout << endl;
	PrintHash(Hash);
	cout << HashSearch(Hash,3)  << endl;
	cout << HashSearch(Hash,48) << endl;
	HashDelete(Hash,3);
	PrintHash(Hash);
	return 0;
}

         测试数组test[m],采用随机数,为了保证Search一般能成功,采取了对10进行模运算,再用个肯定找不到的48,进行测试,运行结果如下:

      运行结果基本靠谱啊。
      关于开放寻址的删除,不同意书上说的,书上说:如果从i中删除了k,不能只是将NIL置于其中标记为空,因为这样在插入关键字k的探查中,会发现i被占用了。(实际上,在插入时,只是检查它是否为NIL,难道不是吗?)

      对于开放寻址中探查序列的计算,通常有三种方法:线性探查,二次探查,双重探查。我们这里用的最简单的线性探查。他们的比较如下:

(图from TankyWoo)

        测试程序的一个不足之处在于,测试数组与hash数组大小一样,这样就没有体现到hash的精髓:稀疏+映射。在测试时可将test数组大小改为一半或用其他方法。Hash存在的价值在于其出色的期望性能,O(1)很强啊,神一样的存在。。。


         至此,搞定,菜鸟goes on ~~~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值