散列表(哈希表)-- 计算机科学里的一个伟大发明

来源:互联网 发布:java和嵌入式 编辑:程序博客网 时间:2024/05/16 15:34

      “ 散列表是计算机科学里的一个伟大发明,它是由数组、表和一些数学方法相结合,构造起来的一种能够有效支持动态数据的存储和提取的结构。散列表的一个典型应用是符号表,在一些值(数据)与动态的字符串(关键码)集合的成员间建立一种关联。你最喜欢用的编译系统十之八九是使用了散列表,用于管理你的程序里各个变量的信息。你的网络浏览器可能也很好地使用了一个散列表来维持最近使用的页面踪迹。你与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都很方便使用,减少代码编写量,但是它们没有针对特殊目的的哈希函数来的高效。

原创粉丝点击