哈希表支持一种最有效的检索方法:散列。
从根本上,一个哈希表包含一个数组,通过特殊的索引值(键)来访问数组中的元素。
哈希表的基本思想:
通过一个哈希函数,在所有可能的键和槽位之间建立一张映射表。
哈希函数每次接受一个键将返回与键对应的哈希编码或哈希值。
键的数据类型可能是多种多样,但哈希值的类型只能是整型。
由于计算哈希值和在数组中进行索引都只消耗固定的时间,因此哈希表的最大亮点在于它是一种运行时间在常量级的检索方法。
通常与各种各样的键相比,哈希表的条目数目相应较少。
因此,绝大多数哈希函数会将一些不同的键映射到表中相同的槽位上。
当两个键映射到一个相同的槽位上时,他们就产生了冲突。
一个好的哈希函数能最大限度地减少冲突,但冲突不可能完全消除,我们仍然要想办法处理这些冲突。
1.链式哈希表
1)数据结构描述
链式哈希从根本上来说是一组链表组成的。
每个链表都可以看做一个桶bucket,我们将所有的元素通过散列的方式放到具体的不同的桶中。
# 元素的插入:
首先将哈希键传入一个哈希函数,函数通过散列的方式告知元素属于哪个桶,然后子啊相应的链表头插入元素
# 元素的查找和删除:
依然是通过哈希键传入哈希函数,通过散列找到元素所在的桶,然后遍历相应的链表,知道找到我们想要查找的元素。
如果链表过长,遍历性能下降。
2)冲突解决
当哈希表中两个键散列到一个相同的槽位时,这两个键之间将发生冲突。
链式哈希表解决冲突的办法很简单:当冲突发生时,它就将元素放到已经准备好的桶中。
带来的问题:当过多的冲突发生在同一个槽位上,此位置的桶就会变得越来越深了,从而遍历桶的效率也就会越低了
理想的目标是均匀散列:尽可能均匀和随机地分配表中的元素
但如果元素的数量远远大于表中桶的数量,表的性能依然很低。
哈希表负载因子:a = n / m;
n是表中元素的数量,m是桶的个数。
在均匀散列的情况下,链式哈希表的负载因子告诉我们表中的桶能装下元素个数的最大值。
3)选择哈希函数
一个好的哈希函数旨在近似均匀散列。
定义一个哈希函数f,它将键k映射到哈希表中的位置x。x称为哈希编码。
h(k)= x;
大多数情况下假设k为整型,如果不是整型,也可以很容易地转化为整型。
尽可能地获取键的特性尤为重要。
# 取余法
h(k) = k mod m;
m的取值:
通常不会选择2的幂,而会选择一个不太接近2的幂的素数,同时考虑存储空间的限制和限制因子。
# 乘法
它将整型键乘以一个常数A(0<A<1),取结果的小数部分,然后乘以m取结果的整数部分。
例:一个能够较好地处理字符串的哈希函数,通过一系列的位操作将键强制转换为整数。
usigned int hashpjw(const void *key){
const char *ptr;
unsigned int val;
val = 0;
ptr = key;
while(*ptr != '\0'){
unsigned int tmp;
val = (val<<4)+(*ptr);
if(tmp == (val & oxf0000000)){
val = val ^ (tmp >> 24);
val = val ^ tmp;
}
++ptr;
}
return val % PRIME_TALSIZE;
}
4)链式哈希表的实现
//chtbl.h
#ifndef CHTBL_H
#define CHTBL_H
#include <stdio.h>
#include "list.h"
typedef struct CHTbl_{
int buckets;
int (*h)(const void *key);
int (*match)(const void *key1,const void *key2);
void (*destroy)(void *data);
int size;
List *table;
}CHTbl;
int chtbl_init(CHTbl *htbl,
int buckets,
int (*h)(const void *key),
int (*match)(const void *key1,const void *key2),
void (*destroy)(void *data));
void chtbl_destory(CHTbl *htbl);
int chtbl_insert(CHTbl *htbl,const void *data);
int chtbl_remove(CHTbl *htbl,void **data);
int chtbl_lookup(CHTbl *htbl,void **data);
#define chtbl_size(htbl) ((htbl)->size)
#endif
5)链式哈希表的例子:符号表
哈希表的一个人重要的应用是在编译器中,用来维护程序中出现的符号信息。
编译器将一张编程语言写成的程序翻译成另外一种能够在机器上运行的机器代码。
为了能偶有效管理程序中的符号信息,编译器通常使用一种叫做符号表的数据结构,符号表通常通过哈希表实现。
2.开地址哈希表
开地址哈希表中,元素存放在表本身中。这种特性依赖于固定大小的表比较有用。
冲突解决:
探查这个表,直到找到一个可以放置元素的槽。
元素插入:直到找到一个空槽,然后将元素插入。
元素删除和查找:探查槽位直到定位到该元素,或直到找到一个空槽。
要尽量减少探查次数,进行多少次探查后就停止探查主要取决于两件事:
哈希表的负载因子和元素均匀分布的程度
根据开地址哈希表的定义,它所包含的元素不大可能大于表中槽位的数量,
也就是n<=m,负载因子通常小于等于1,因为每一个槽至多能容纳一个元素。
在开地址的哈希函数定义为:h(k,i) = x;
k是键,i是目前为止探查的次数,x是得到的哈希编码。
通常情况下,与链式哈希表一样,h会调用一个或多个相同属性的辅助哈希函数。
# 线性探查:
开地址哈希表中一个简单的探查方法就是探查表中的连续的槽位。
h(k,i) = (h'(k) + i)mod m
线性探查的优点是简单,而且它对m没有限制,这样就可以保证所有的卡槽最终都可能探查到。
# 双散列
最有效的探查开地址哈希表的方法之一,通过计算两个辅助哈希函数哈希编码的和来得到哈希编码
h(k,i) = (h1(k) + h2(k))mod m
为了保证第二次访问任何一个槽之前其他所有槽都访问过,必须遵循以下规定:
办法1:m必须是2的幂,让h2返回一个奇数值
办法2:m为一个素数,h2返回值在1和m-1之间
从根本上,一个哈希表包含一个数组,通过特殊的索引值(键)来访问数组中的元素。
哈希表的基本思想:
通过一个哈希函数,在所有可能的键和槽位之间建立一张映射表。
哈希函数每次接受一个键将返回与键对应的哈希编码或哈希值。
键的数据类型可能是多种多样,但哈希值的类型只能是整型。
由于计算哈希值和在数组中进行索引都只消耗固定的时间,因此哈希表的最大亮点在于它是一种运行时间在常量级的检索方法。
通常与各种各样的键相比,哈希表的条目数目相应较少。
因此,绝大多数哈希函数会将一些不同的键映射到表中相同的槽位上。
当两个键映射到一个相同的槽位上时,他们就产生了冲突。
一个好的哈希函数能最大限度地减少冲突,但冲突不可能完全消除,我们仍然要想办法处理这些冲突。
1.链式哈希表
1)数据结构描述
链式哈希从根本上来说是一组链表组成的。
每个链表都可以看做一个桶bucket,我们将所有的元素通过散列的方式放到具体的不同的桶中。
# 元素的插入:
首先将哈希键传入一个哈希函数,函数通过散列的方式告知元素属于哪个桶,然后子啊相应的链表头插入元素
# 元素的查找和删除:
依然是通过哈希键传入哈希函数,通过散列找到元素所在的桶,然后遍历相应的链表,知道找到我们想要查找的元素。
如果链表过长,遍历性能下降。
2)冲突解决
当哈希表中两个键散列到一个相同的槽位时,这两个键之间将发生冲突。
链式哈希表解决冲突的办法很简单:当冲突发生时,它就将元素放到已经准备好的桶中。
带来的问题:当过多的冲突发生在同一个槽位上,此位置的桶就会变得越来越深了,从而遍历桶的效率也就会越低了
理想的目标是均匀散列:尽可能均匀和随机地分配表中的元素
但如果元素的数量远远大于表中桶的数量,表的性能依然很低。
哈希表负载因子:a = n / m;
n是表中元素的数量,m是桶的个数。
在均匀散列的情况下,链式哈希表的负载因子告诉我们表中的桶能装下元素个数的最大值。
3)选择哈希函数
一个好的哈希函数旨在近似均匀散列。
定义一个哈希函数f,它将键k映射到哈希表中的位置x。x称为哈希编码。
h(k)= x;
大多数情况下假设k为整型,如果不是整型,也可以很容易地转化为整型。
尽可能地获取键的特性尤为重要。
# 取余法
h(k) = k mod m;
m的取值:
通常不会选择2的幂,而会选择一个不太接近2的幂的素数,同时考虑存储空间的限制和限制因子。
# 乘法
它将整型键乘以一个常数A(0<A<1),取结果的小数部分,然后乘以m取结果的整数部分。
例:一个能够较好地处理字符串的哈希函数,通过一系列的位操作将键强制转换为整数。
usigned int hashpjw(const void *key){
const char *ptr;
unsigned int val;
val = 0;
ptr = key;
while(*ptr != '\0'){
unsigned int tmp;
val = (val<<4)+(*ptr);
if(tmp == (val & oxf0000000)){
val = val ^ (tmp >> 24);
val = val ^ tmp;
}
++ptr;
}
return val % PRIME_TALSIZE;
}
4)链式哈希表的实现
//chtbl.h
#ifndef CHTBL_H
#define CHTBL_H
#include <stdio.h>
#include "list.h"
typedef struct CHTbl_{
int buckets;
int (*h)(const void *key);
int (*match)(const void *key1,const void *key2);
void (*destroy)(void *data);
int size;
List *table;
}CHTbl;
int chtbl_init(CHTbl *htbl,
int buckets,
int (*h)(const void *key),
int (*match)(const void *key1,const void *key2),
void (*destroy)(void *data));
void chtbl_destory(CHTbl *htbl);
int chtbl_insert(CHTbl *htbl,const void *data);
int chtbl_remove(CHTbl *htbl,void **data);
int chtbl_lookup(CHTbl *htbl,void **data);
#define chtbl_size(htbl) ((htbl)->size)
#endif
5)链式哈希表的例子:符号表
哈希表的一个人重要的应用是在编译器中,用来维护程序中出现的符号信息。
编译器将一张编程语言写成的程序翻译成另外一种能够在机器上运行的机器代码。
为了能偶有效管理程序中的符号信息,编译器通常使用一种叫做符号表的数据结构,符号表通常通过哈希表实现。
2.开地址哈希表
开地址哈希表中,元素存放在表本身中。这种特性依赖于固定大小的表比较有用。
冲突解决:
探查这个表,直到找到一个可以放置元素的槽。
元素插入:直到找到一个空槽,然后将元素插入。
元素删除和查找:探查槽位直到定位到该元素,或直到找到一个空槽。
要尽量减少探查次数,进行多少次探查后就停止探查主要取决于两件事:
哈希表的负载因子和元素均匀分布的程度
根据开地址哈希表的定义,它所包含的元素不大可能大于表中槽位的数量,
也就是n<=m,负载因子通常小于等于1,因为每一个槽至多能容纳一个元素。
在开地址的哈希函数定义为:h(k,i) = x;
k是键,i是目前为止探查的次数,x是得到的哈希编码。
通常情况下,与链式哈希表一样,h会调用一个或多个相同属性的辅助哈希函数。
# 线性探查:
开地址哈希表中一个简单的探查方法就是探查表中的连续的槽位。
h(k,i) = (h'(k) + i)mod m
线性探查的优点是简单,而且它对m没有限制,这样就可以保证所有的卡槽最终都可能探查到。
# 双散列
最有效的探查开地址哈希表的方法之一,通过计算两个辅助哈希函数哈希编码的和来得到哈希编码
h(k,i) = (h1(k) + h2(k))mod m
为了保证第二次访问任何一个槽之前其他所有槽都访问过,必须遵循以下规定:
办法1:m必须是2的幂,让h2返回一个奇数值
办法2:m为一个素数,h2返回值在1和m-1之间