1. 散列的定义
犹如关键字索引,我们把带有关键字
i
的元素存储到表的第
散列(Hash)搜索算法 试图通过算术运算将关键字的值转换成表中的地址来直接引用表中的元素;而不是像在其他数据结构中比较搜索关键字的值与元素中关键字的值来实现搜索.
散列与二叉树的对比:
在符号表的实现中散列比二叉树应用更广泛,因为它简单并可以提供最优的搜索性能(常数时间)。二叉树优于散列的地方:接口简单不需计算散列函数;二叉树是动态的;二叉树可以在最坏情况下保证性能(即使最好的散列算法,仍有可能将所有元素散列到同一位置);二叉树支持更多操作(尤其是排序和选择)。
散列搜索算法的实现可以分为两个部分:
(1)计算一个散列函数的值,将搜索关键字转换成表中地址
(2)冲突解决,如何解决不同的关键字对应同一个hash函数值(表地址)的问题,有如下三种解决方法:链地址法、线性探测法、双重散列表。
2. 散列函数的选择
理想的散列函数要易于计算并且近似为一个随机函数:即对于每个输入,每个输出等概率出现,另外一个好的散列函数要考虑关键字的所有位。
散列函数与关键字的类型有关,即对于每一种可能的关键字需要不同的散列函数。
(1)关键字为0~1的浮点数:
将关键字其乘以表长M,取最接近的整数即得到0~(M-1)的地址
#define Hash(v, M) (v-s)/(t-s)*M //s为最小关键字,t为最大关键字
(2)关键字为
w
-位的整数:
- 选择表长M为质数,对任何关键字计算
h(k)=k mod
M
;
之所以选择M为质数(素数),是为了使散列函数考虑到关键字的所有位,如:选择非素数M=26 ,那么该散列函数对所有关键字的返回值都是关键字的后6位。莫尼森素数:
2t−1
的值是素数。 #define Hash(v, M) (v % M)
- 当表长M不是素数时,对于整数关键字的另一种方法:将乘法和取模法结合,将关键字乘以0~1之间的一个常数,然后取模M运算,
h(k)=(kα)
mod
M
。常用
α=0.618 (黄金分割),乘法取模法要求乘数和表大小M互为素数。
#define Hash(v, M) ((int)( 0.618 * (float)v) % M)
或
#define Hash(v, M) (16161 * (unsigned)v % M)
之所以选择M为质数(素数),是为了使散列函数考虑到关键字的所有位,如:选择非素数
#define Hash(v, M) (v % M)
- 当表长M不是素数时,对于整数关键字的另一种方法:将乘法和取模法结合,将关键字乘以0~1之间的一个常数,然后取模M运算,
h(k)=(kα)
mod
M
。常用
α=0.618 (黄金分割),乘法取模法要求乘数和表大小M互为素数。
#define Hash(v, M) ((int)( 0.618 * (float)v) % M)
或
#define Hash(v, M) (16161 * (unsigned)v % M)
散列函数的性能影响
散列函数的计算很可能在内循环中,因此提高散列的计算速度就提高了整个算法的速度。所以散列函数尽可能简单且随机。
典型错误:散列函数总返回同一值,原因可能是一次数据类型的转换(如int)没能正确完成,因此对特殊的符号表中遇到的关键字类型应该校验类型转换的执行的正确性。
3. 链地址法
散列算法的第二步就是解决当两个或多个关键字散列到同一个地址时如何处理的问题。最简单的方法是:对每个散列地址建立一个链表,将散列到同一地址的不同关键字(冲突元素)存入到一个相应的链表中。要建立M个链表。使用无序表,插入快,插入到头结点的前面。
HashTable HTInit(int max) //max为最多可能的元素数
{
M = max / 5; //期望每个链表平均存储5个元素
N = 0; //当前hash表中元素个数
hash_table = new HTNodeLink[M];
z = NewNode(NULLItem, NULL);
for(int i = 0; i < M; i++)
hash_table[i] = z; //z为哑节点
return hash_table;
}
void HTInsert(Item item)
{
KeyItem v = GetKey(item);
int k = Hash(v, M); //计算散列值
hash_table[k] = NewNode(item, hash_table[k]);
N++;
}
Item HTSearchR(HTNodeLink h, KeyItem v)
{
if(h == z)
return NULLItem;
if(eq(GetKey(h->item), v))
return h->item;
else
return HTSearchR(h->next, v);
}
Item HTSearch(KeyItem v)
{
int k = Hash(v, M);
return HTSearchR(hash_table[k], v);
}
void HTDelete(Item item)
{
int k = Hash(GetKey(item), table_length);
hash_table[k] = HTDeleteR2(hash_table[k], item);
}
(1)虽然链地址法使用M个链表的额外巩健,但平均来说,将顺序搜索的比较次数降低到1/M;而散列在实际中有用的真正原因在于每个地址链以极大概率包含N/M个元素
(2)链地址法中,M取尽量小避免空链浪费内存空间,M取足够大使顺序搜索效率最高。
3. 完整代码
http://download.csdn.net/detail/quzhongxin/8620465
参考资料:《算法:C语言实现》p377-387