散列表(哈希表)-- 计算机科学里的一个伟大发明
2010-02-06 10:22
274 查看
“ 散列表是计算机科学里的一个伟大发明,它是由数组、表和一些数学方法相结合,构造起来的一种能够有效支持动态数据的存储和提取的结构。散列表的一个典型应用是符号表,在一些值(数据)与动态的字符串(关键码)集合的成员间建立一种关联。你最喜欢用的编译系统十之八九是使用了散列表,用于管理你的程序里各个变量的信息。你的网络浏览器可能也很好地使用了一个散列表来维持最近使用的页面踪迹。你与I n t e r n e t的连接可能也用到一个散列表,缓存最近使用的域名和它们的I P地址。” 在《The Prictice of Programming》中散列表那一节的开头就这样写到。
最近在看《Programming Pearls》和《The Prictice of Programming》这两本书,写的都很不错。里面讲了很多在实际编程遇到问题的比较优美的解决方法,和需要注意的问题。这两本书都很薄,简短,精炼,却又全面的。例如在前面写的一篇文章中讲到用STL的map来解决统计单词出现的次数问题,也提到可以用哈希表来解决。在其他语言,C#和Java好像有hash table 这个类,而STL中没有,其实有些c++库有包含hash table。以前在学数据结构这门课时,只是了解,没有实际编过。后来在复习数据结构时,发现哈希表是一个很高效的数据结构。如果选择了好的哈希函数,而且表并不太长,它对元素访问提供了一个O( 1 )的期望性能(二叉查找树是O(logn))。哈希表也有一些缺点。如果散列函数不好,或者所用的数组太小,其中的链接表就可能变得很长。
其中构造哈希函数是个关键,它应该把数据均匀地散布到数组里。对于字符串,网上有很多构造字符串的哈希函数,最常见的散列算法之一(而且比较好理解,我个认为)就是:逐个把字节加到已经构造的部分哈希值的一个倍数上。乘法能把新字节在已有的值中散开来根据经验,在对A S C I I串的哈希函数中,选择3 1和3 7作为乘数是很好的。字符串哈希函数如下:
#define MULT 31
unsigned int hash(char *str)
{
unsigned int h = 0;
unsigned char * p;
for (p = (unsigned char *)str ; *p != '/0'; p++)
h = MULT * h + *p;
return h % NHASH;
}
在这个计算中用到了无符号字符。这样做的原因是, C和C + +对于c h a r是不是有符号数据没有给出明确定义。而我们需要哈希函数总返回正值。
还有一个问题就是如何取哈希表的大小,用素数作为数组的大小是比较明智的,因为这样能保证在数组大小、散列的乘数和可能的数据值之间不存在公因子。
在这两本书中都讲了哈希表实现的伪代码或C语言代码,书上有些编码还有点不习惯,于是自己动手写了一个C语言实现的哈希表,并用来解决统计单词出现的次数问题,大部分代码和《Programming Pearls》中的差不多。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct node *nodeptr;
typedef struct node {
char *word;
int count;
nodeptr next;
} node;
#define NHASH 29989
#define MULT 31
nodeptr bin[NHASH];
unsigned int hash(char *str)
{
unsigned int h = 0;
unsigned char * p;
for (p = (unsigned char *)str ; *p != '/0'; p++)
h = MULT * h + *p;
return h % NHASH;
}
void insertWord(char *str)
{
int h;
nodeptr p;
h = hash(str);
for(p = bin[h]; p != NULL; p = p->next)
if(strcmp(p->word,str) == 0)
{
p->count++;
return;
}
p = (nodeptr)malloc(sizeof(node));
if(p != NULL)
{
p->count = 1;
p->word = (char *)malloc(strlen(str)+1);
strcpy(p->word, str);
p->next = bin[h];
bin[h] = p;
}
}
int main()
{
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
int i;
nodeptr p;
char word[100];
for (i = 0; i < NHASH; i++)
bin[i] = NULL;
while (scanf("%s", word) != EOF)
insertWord(word);
for(i = 0; i < NHASH; i++)
for(p = bin[i]; p != NULL; p = p->next)
printf("%s: %d/n", p->word, p->count);
return 0;
}
后来我用一个2M的英文文本分别测试了用map 和 hash table来实现的两个程序,如果是用cin,cout函数来输入输出的话,前者运行时间为6.578秒,后者为3.031秒!如果用scanf,printf函数来输入输出的话,后者运行时间为0.421秒!总之,哈希表如果使用得当,常数时间的检索、插入和删除操作是任何其他技术都望尘莫及的。
查找和字符串处理都是些很实际的问题,很多地方都要用到它。我们编程的时候会面临这样一个问题,是用标准库呢还是自定义的组件呢?虽然STL中的map,set和string都很方便使用,减少代码编写量,但是它们没有针对特殊目的的哈希函数来的高效。
最近在看《Programming Pearls》和《The Prictice of Programming》这两本书,写的都很不错。里面讲了很多在实际编程遇到问题的比较优美的解决方法,和需要注意的问题。这两本书都很薄,简短,精炼,却又全面的。例如在前面写的一篇文章中讲到用STL的map来解决统计单词出现的次数问题,也提到可以用哈希表来解决。在其他语言,C#和Java好像有hash table 这个类,而STL中没有,其实有些c++库有包含hash table。以前在学数据结构这门课时,只是了解,没有实际编过。后来在复习数据结构时,发现哈希表是一个很高效的数据结构。如果选择了好的哈希函数,而且表并不太长,它对元素访问提供了一个O( 1 )的期望性能(二叉查找树是O(logn))。哈希表也有一些缺点。如果散列函数不好,或者所用的数组太小,其中的链接表就可能变得很长。
其中构造哈希函数是个关键,它应该把数据均匀地散布到数组里。对于字符串,网上有很多构造字符串的哈希函数,最常见的散列算法之一(而且比较好理解,我个认为)就是:逐个把字节加到已经构造的部分哈希值的一个倍数上。乘法能把新字节在已有的值中散开来根据经验,在对A S C I I串的哈希函数中,选择3 1和3 7作为乘数是很好的。字符串哈希函数如下:
#define MULT 31
unsigned int hash(char *str)
{
unsigned int h = 0;
unsigned char * p;
for (p = (unsigned char *)str ; *p != '/0'; p++)
h = MULT * h + *p;
return h % NHASH;
}
在这个计算中用到了无符号字符。这样做的原因是, C和C + +对于c h a r是不是有符号数据没有给出明确定义。而我们需要哈希函数总返回正值。
还有一个问题就是如何取哈希表的大小,用素数作为数组的大小是比较明智的,因为这样能保证在数组大小、散列的乘数和可能的数据值之间不存在公因子。
在这两本书中都讲了哈希表实现的伪代码或C语言代码,书上有些编码还有点不习惯,于是自己动手写了一个C语言实现的哈希表,并用来解决统计单词出现的次数问题,大部分代码和《Programming Pearls》中的差不多。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct node *nodeptr;
typedef struct node {
char *word;
int count;
nodeptr next;
} node;
#define NHASH 29989
#define MULT 31
nodeptr bin[NHASH];
unsigned int hash(char *str)
{
unsigned int h = 0;
unsigned char * p;
for (p = (unsigned char *)str ; *p != '/0'; p++)
h = MULT * h + *p;
return h % NHASH;
}
void insertWord(char *str)
{
int h;
nodeptr p;
h = hash(str);
for(p = bin[h]; p != NULL; p = p->next)
if(strcmp(p->word,str) == 0)
{
p->count++;
return;
}
p = (nodeptr)malloc(sizeof(node));
if(p != NULL)
{
p->count = 1;
p->word = (char *)malloc(strlen(str)+1);
strcpy(p->word, str);
p->next = bin[h];
bin[h] = p;
}
}
int main()
{
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
int i;
nodeptr p;
char word[100];
for (i = 0; i < NHASH; i++)
bin[i] = NULL;
while (scanf("%s", word) != EOF)
insertWord(word);
for(i = 0; i < NHASH; i++)
for(p = bin[i]; p != NULL; p = p->next)
printf("%s: %d/n", p->word, p->count);
return 0;
}
后来我用一个2M的英文文本分别测试了用map 和 hash table来实现的两个程序,如果是用cin,cout函数来输入输出的话,前者运行时间为6.578秒,后者为3.031秒!如果用scanf,printf函数来输入输出的话,后者运行时间为0.421秒!总之,哈希表如果使用得当,常数时间的检索、插入和删除操作是任何其他技术都望尘莫及的。
查找和字符串处理都是些很实际的问题,很多地方都要用到它。我们编程的时候会面临这样一个问题,是用标准库呢还是自定义的组件呢?虽然STL中的map,set和string都很方便使用,减少代码编写量,但是它们没有针对特殊目的的哈希函数来的高效。
相关文章推荐
- 正则表达式分类 区别 原文地址:http://www.cnblogs.com/chengmo/archive/2010/10/10/1847287.html 则表达式:在计算机科学中,是指一个用来描述
- 计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决
- 人工智能(计算机科学的一个分支)
- 尼古拉·特斯拉——一个比爱迪生更伟大却被世界遗忘的科学巨人
- 计算机编程领域最伟大的20个发明
- 计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决
- 计算机科学专业应该传授的3件事
- 一个计算机网络的作业题
- 最伟大的计算机程序员是如何诞生的?——解读高德纳(Donald E.Knuth)
- 20161210计算机科学导论04_磁盘
- 计算机科学精彩帖子收集
- 20161210计算机科学导论05_操作系统
- 20161210计算机科学导论05_操作系统
- 20161210计算机科学导论06_函数调用过程
- 语录:101条伟大的计算机编程名言
- 20161211计算机科学导论07_软件与硬件
- 计算机科学论文写作1-引言
- 一个计算机爱好者的不完整回忆(三十)VB与Delphi
- 2017年度计算机科学各领域热点词汇
- Computer and computer Science(计算机与计算机科学)