我是自动化专业的应届研究生,最终拿到了tplink、华为、vivo等公司的ssp的offer,分享自己学习过的计算机基础知识(C语言+操作系统+计算机网络+linux)以及数据结构与算法的相关知识,保证看完让你有所成长。
欢迎关注我,学习资料免费分享给你哦!还有其他超多学习资源,都是我自己学习过的,经过过滤之后的资源,免去你还在因为拥有大量资源不知如何入手的纠结,让你体系化学习。
散列
散列就是利用一段存储空间存储数据(大小为Tablesize),然后根据关键词可以生成一个数字,这个数字是在范围在0和Tablesize之间。这个将关键字映射到数字的办法叫做散列函数。其实散列就是一个线性表,只不过它的下标可以不是直接给出的,而是通过一个运算从关键字得出的。理论上是希望每一个关键字都独自占有一个下标的数字,但是不论是什么散列函数,都是有可能将不同的关键字映射为一个数字的,所以还必须解决关键字映射为数字的冲突问题。
散列函数
如果关键字是整数,一般散列函数采取的办法是直接取余返回“key mod Tablesize".
如果关键字是字符串,一种简单的方法是将逐个按照字符的ASCII相加,将得到的结果最后求余。
int Hash(int Tablesize,char *key)
{
unsigned int Hashval=0;
while(*key!='\0')
{
Hashval+=*key;
key++;
}
return Hashval%Tablesize;
}
还有一种更好的散列函数
int Hash(int Tablesize,char *key)
{
unsigned int Hashval=0;
while(*key!='\0')
{
Hashval=*key+( Hashval<<5);
key++;
}
return Hashval%Tablesize;
}
这些散列函数当然也存在着冲突的问题,下面学习两种解决冲突的方法。
分离链接法
这是一种采用链表的方法来解决冲突的问题的,因为是采用链表,所以当出现冲突时,在相应的位置申请一个节点,然后与链表的插入操作相同,可以插入到链表的首部或者尾部。
如图所示,就是一个简单的分离链接法的示意图,每个散列表的元素都自己组成了一链表,如下标为3的元素,构成了53与103的链表。假设此时插入一个元素101,经过散列函数的运算,得到的下标为1,此时在1处已经有一个元素了,但因为采用了链表的方式处理冲突,只需要申请一个结点的内存,插入到链表的表头后面就可以了,完美解决冲突。当需要访问101时,首先经过散列函数运算得到下标为1,然后沿着链表遍历,如果找到101这个值,将这个节点返回即可,找不到就说明不存在。
//节点元素定义
typedef struct Node
{
unsigned int num;
struct Node * next;
};
typedef struct Node * list;
typedef struct HashNode
{
int hashsize;
list* thelist; //指向指针的指针,指向散列数组的首地址
};
typedef struct HashNode * Hashtable;
//散列创建函数
Hashtable creatHashtable(int hashsize)
{
Hashtable hash;
int i;
hash=(Hashtable)malloc(sizeof(struct HashNode));//申请散列表管理结构,都是指针
if(hash==NULL)
{
printf("内存不足");
return NULL;
}
hash->hashsize=hashsize;
hash->thelist=(list*)malloc(sizeof(list)*hashsize);//申请散列表的数组(图中的表头部分)
if(hash->thelist==NULL)
{
printf("内存不足");
return NULL;
}
for(i=0;i<hashsize;i++)
{
hash->thelist[i]=(list)malloc(sizeof(struct Node));//(为每一个散列表头申请一个结点方便管理)
hash->thelist[i]->next=NULL;
}
return hash;
}
图中就是构建的一个大小为4的散列,每个指针与后面的一个结点构成了前面图中所示的表头,这两个一起作为散列表的管理结构,申请这个结点是为了后面管理链表的方便,相当于链表中的头结点。
//散列函数
unsigned int Hash(Hashtable hash,unsigned int num)//采用最简单的求余比较
{
return num%hash->hashsize;
}
list find(Hashtable hash,unsigned int num)//查找函数,返回值如果不为NULL,就是查找到的节点;为NULL就是表示没有查找到
{
unsigned int data;
list p;
data=Hash(hash,num);
p=hash->thelist[data]->next;//将p指向data所在的链表的首个节点。比如上面图中的101或者53
while((p!=NULL)&&(p->num!=num))//比较关键字是否相等
{
p=p->next;
}
return p;
}
对于散列的插入操作的基本思路:根据关键字输入到散列函数,得到在散列表中的下标,然后查找是否已经存在于散列表中,如果存在,什么也不做;如果不存在,申请新的节点,插入到表头的链表之中。前面已经举例子不在赘述。看代码
//0表示之前已经存在,不需要插入;1表示插入成功;-1表示内存不足,插入失败
int insert(Hashtable hash,unsigned int num)
{
unsigned int data;
list p;
p= find(hash,num);
if(p==NULL)
{
data=Hash(hash,num);
p=(list)malloc(sizeof(struct Node));
if(p==NULL)
{
printf("内存不足");
return -1;
}
p->next=hash->thelist[data]->next;
hash->thelist[data]->next=p;
p->num=num;
return 1;
}
else
{
return 0;
}
}
//0表示删除的元素不在散列表中;1表示删除成功
int delete(Hashtable hash,unsigned int num)
{
unsigned int data;
list p,k;
data=Hash(hash,num);
p=hash->thelist[data];//将p指向data所在的链表的表头。
while((p->next!=NULL)&&(p->next->num!=num))//比较关键字是否相等
{
p=p->next;
}
if(p->next==NULL)
{
return 0;
}
else
{
k=p->next;
p->next=k->next;
free(k);
return 1;
}
}
开放定址法
开放定址法采用的是发生冲突,就去尝试寻找另外的空闲单元来插入。hash(num)=(hash(num)+F(i))mod Tablesize,且F(0)=0.函数F就是解决冲突的函数,定义了如何寻找空的单元,因为需要插入所有的单元,所以开放定址散列法需要的表比分离链接散列表大。
F函数由三种实现的方法:
线性探测法
线性探测法就是函数F是线性的,F(i)=i。这相当于逐个探测每个单元是否为空。但是线性探测的问题是容易导致在某一个范围内的数据聚集。
平方探测法
平方探测法用来消除线性探测中的一次聚集问题的方法。F(i)=i*i.
双散列
双散列法就是F也是一个散列函数。F(i)=i*hash2(x)