哈希表
我们通过某个函数f,使得存储位置=f(关键词)这样就可以通过查找关键字不需要比较就可以获得需要记录的存储位置,这就是一种新的存储技术——散列技术。
散列技术
散列技术是在记录的存储位置和它的关键词之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。散列技术既是一种存储方法,也是一种查找方法。
散列函数(哈希函数)
我们把这种对应关系f称为散列函数,又称哈希函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。
冲突(哈希冲突)
如果碰到两个关键字,但是却有
,这种现象我们称为冲突,并把
和
称为这个散列函数的同义词。
散列函数的构造方法
1.直接地址法
取关键字的某个线性函数值为散列地址,即
f(key)=a*key+b
这样的散列函数的优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。
2.数字分析法
抽取方法是使用关键字的一部分来计算散列存储位置的方法。
数字分析法通常适合处理关键字位数比较多的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑使用这个方法。范例太长了懒得打。
3.平方取中法
这个方法计算简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间三位227,用做散列地址。适用于不知道关键字的分布,而位数又不是很多的情况。
4.折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
比如我们的关键字是9876543210,散列表表长为3位,我们将它分为4组,987|654|321|0,然后将它们叠加求和987+654+321+0=1962,再求后3位得到散列地址为962。
有时可能这还不能够保证分布均匀,不妨从一端向另一端来回折叠后对齐相加。比如我们将987和321反转,再与654和0相加,变成789+654+123+0=1566,此时散列地址为566。
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
5.除留取余法
f(key)=key mod p(p<=m)
mod是取模(求余数)不仅可以对关键字直接模,也可以在折叠、平方之后取模
因此根据前辈们的经验,若散列表表长为m,通常p为小于或等于表长(最好接近
6.随机数法
选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key)=random(key)。这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
有同学问,那如果关键字是字符串如何处理?其实无论是英文字符,还是中文字符,也包括各种各样的符号,它们都可以转化为某种数字来对待,比如ASCII码或者Unicode码等,因此也就可以使用上面的这些方法。
总之,现实中,应该视不同的情况采用不同的散列函数。我们只能给出一些考虑的因素来提供参考:
(1)计算散列地址所需的时间。
(2)关键字的长度。
(3)散列表的大小。
(4)关键字的分布情况。
(5)记录查找的频率。
综合这些因素,才能决策选择哪种散列函数更合适。
处理散列冲突的方法
1.开放地址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列己够大,空的散列地址总能找到,并将记录存入。
它的公式是:
fi(key)=(f(key)+di)MODm (di=1,2,3,⋯,m−1)
我们把这种解决冲突的开放定址法称为线性探测法。
从这个例子我们也看到,在解决冲突的时候,还会碰到如48和37这种本来都不是同义词却需要争夺一个地址的情况,我们称这种现象为堆积。很显然,堆积的出现,使得我们需要不断处理冲突,无论是存入还是查找效率都会大大降低。
考虑深一步,如果发生这样的情况,当最后一个key=34, f(key)=10,与22所在的位置冲突,可是22后面没有空位置了,反而它的前面有一个空位置,尽管可以不断地求余数后得到结果,但效率很差。因此我们可以改进( di=1^2,−1^2,2^2,−2^2,⋯,q^2,−q^2(q≤m/2),这样就等于是可以双向寻找到可能的空位置。对于34来说,我们取 ᵢdᵢ=−1
即可找到空位置了。另外增加平方运算的目的是为了不让关键字都聚集在某一块区域。我们称这种方法为二次探测法。
fi(key)=(f(key)+di)MODm (di=1^2,−1^2,2^2,−2^2;⋯,q^2,−q^2,q≤m/2)
还有一种方法是,在冲突时,对于位移量d,采用随机函数计算得到,我们称之为随机探测法。
此时一定有人问,既然是随机,那么查找的时候不也随机生成 ᵢdᵢ吗?如何可以获得相同的地址呢?这是个问题。这里的随机其实是伪随机数。伪随机数是说,如果我们设置随机种子相同,则不断调用随机函数可以生成不会重复的数列,我们在查找时,用同样的随机种子,它每次得到的数列是相同的,相同的( dᵢ)当然可以得到相同的散列地址。
嗯?随机种子又不知道?罢了罢了,不懂的还是去查阅资料吧,我不能在课上没完没了地介绍这些基础知识呀。
₁f₁(key)=(f(key)+d₁)MODm(d,是一个随机数列)
总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是我们常用的解决冲突的办法。
2.再散数列法
3.链地址法
链地址法的原理时如果遇到冲突,他就会在原地址新建一个空间,然后以链表结点的形式插入到该空间。我感觉业界上用的最多的就是链地址法。下面从百度上截取来一张图片,可以很清晰明了反应下面的结构。比如说我有一堆数据{1,12,26,337,353…},而我的哈希算法是H(key)=key mod 16,第一个数据1的哈希值f(1)=1,插入到1结点的后面,第二个数据12的哈希值f(12)=12,插入到12结点,第三个数据26的哈希值f(26)=10,插入到10结点后面,第4个数据337,计算得到哈希值是1,遇到冲突,但是依然只需要找到该1结点的最后链结点插入即可,同理353。
![](https://img-blog.csdnimg.cn/img_convert/732424d41cf6b1c761e39cd62e09db17.png)
4.公共溢出法
散列表查找的算法实现
//定义一个散列表的结构以及一些相关常数
//其中HashTable是散列表结构,结构中的elem为一个动态数组
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12
#define NULLKEY -32768
//定义散列表长为数组的长度
typedef struct
{
int *elem;//数据元素存储基址
int count;//当前数据元素的个数
}HashTable;
int m=0;//散列表长,全局变量
//初始化散列表
Status InitHashTable(HashTable *H)
{
int i;
m=HASHSIZE;
H->count=m;
H->count=m;
H->elem=(int*)malloc(m*sizeof(int));
for(i=0;i<m;i++)
H->elem[i]=NULLKEY;
return OK;
}
//散列函数
int Hash(int key)
{
return key%m;//除留余数法
}
//插入关键字进行搜索
void InsertHash(HashTable *H,int key)
{
int addr=Hash(key);//求散列地址
while(H->elem[addr]!=NULLKEY)//如果不为空则冲突
{
addr=(addr+1)%m;//开放地址法的线性探测
}
H->elem[addr]=key;//直到有空位后插入关键字
}
//散列表查找关键字
Status SearchHash(HashTable H,int key,int addr)
{
*addr=Hash(key);//求散列地址
while(H.elem[*addr]!=key)//如果不为空则冲突
{
*addr=(*addr+1)%m;//开放地址法
if(H.elem[*addr]==NULLKEY||*addr==Hash(key))//如果循环回到原点
return UNSUCCESS;//该关键字不存在
}
return SUCCESS;
}
以上内容来自《大话数据结构》,前面学了那么多,到代码的时候就完全学废了,无所谓,我师父会出手😏
P3370 【模板】字符串哈希
题目描述
如题,给定 N 个字符串(第 ii个字符串长度为 Mi,字符串内包含数字、大小写字母,大小写敏感),请求出 N个字符串中共有多少个不同的字符串。
友情提醒:如果真的想好好练习哈希的话,请自觉。
输入格式
第一行包含一个整数 N,为字符串的个数。
接下来 N行每行包含一个字符串,为所提供的字符串。
输出格式
输出包含一行,包含一个整数,为不同的字符串个数。
输入输出样例
输入 #1
5
abc
aaaa
abc
abcc
12345
输出 #1
4
说明/提示
对于 30% 的数据:N≤10,Mi≈6,Mmax≤15。
对于 70% 的数据:N≤1000,Mi≈100,Mmax≤150。
对于 100% 的数据:N≤10000,Mi≈1000,Mmax≤1500。
样例说明:
样例中第一个字符串(abc)和第三个字符串(abc)是一样的,所以所提供字符串的集合为{aaaa,abc,abcc,12345},故共计4个不同的字符串。
#include<stdio.h>
#include<string.h>
int N;
int a[10010];
int Hash(char str[])
{
int len=strlen(str),k=0;
for(int i=0;i<len;i++)
k=k*10+str[i];
return k;
}
void qsort(int a[],int x,int y)
{
int i,j,t,temp;
if(x>y)
return ;
temp=a[x];
i=x;j=y;
while(i!=j)
{
while(a[j]>=temp&&i<j)
j--;
while(a[i]<=temp&&i<j)
i++;
if(i<j)
{
t=a[i];
a[i]=a[j];
a[j]=t;
}
}
a[x]=a[i];
a[i]=temp;
qsort(a,x,i-1);
qsort(a,i+1,y);
return ;
}//快速排序
int main()
{
int num=0;
scanf("%d",&N);
for(int i=1;i<=N;i++)
{
char str[1515];
scanf("%s",str);
a[i]=Hash(str);
}
qsort(a,1,N);
for(int i=1;i<=N;i++)
{
if(a[i]!=a[i+1])
num++;
}
printf("%d",num);
return 0;
}
大意了,这个题目其实非常简单,不需要用我之前那么多冗长的代码,只需要写一个hash函数搞定不同字符串的哈希值再进行判断就可以了。我在这里犯了个特别蠢的错误,我想把字符串的所有字符的ascll码加起来做他们的哈希值,但是这样对于例如ab和ba就会有冲突,而且输入的N也并不是所有字符串的会有重复,用开放地址法的话也会出现错误,导致答案只有3个。改成 k=k*10+str[i]这个想法太聪明了!!当然不是我这种菜鸟想出来的。
之前学了字典树,看到有人用字典树解决这个就也想来试试。
还是不要轻易尝试了,内存超限
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
struct node
{
int next[100];
}trie[1000010];
int tot=1,t;
int end[1000010];
int insert(char s[])
{
int now=0;//字典树的编号
int len=strlen(s);
for(int i=0;i<len;i++)
{
int ch=s[i]-'0'+1;
if(!trie[now].next[ch])
{
trie[now].next[ch]=tot;
tot++;
}
now=trie[now].next[ch];
}
end[now]++;
}
void search(char s[])
{
int len=strlen(s);
int now=0;
for(int i=0;i<len;i++)
{
int ch=s[i]-'0'+1;
if(!trie[now].next[ch])
return ;
now=trie[now].next[ch];
}
if(end[now]>1)
t++;
}
int main()
{
int N;
scanf("%d",&N);
for(int i=1;i<=N;i++)
{
char str[1515];
scanf("%s",str);
insert(str);
search(str);
}
printf("%d",N-t);
}
还是感觉挺对的😭
![](https://img-blog.csdnimg.cn/img_convert/46ebb9030fdfb1034397481a6e56c8ee.jpeg)