《数据结构》学习-- Hash(2) --Separate Chaining
本系列是《数据结构与算法分析-C语言描述》(Data Structures and Algorithm Analysis in C,作者Mark Weiss)一书的学习笔记,当我在做cc150需要补某个知识点时,就会把这本书翻出来学习一下,同时分享~ 1. 回顾上一次我们简单介绍了:
实际上,Hash表最难设计的就是Hash Function以及冲突解决方法(Collision Resolution)。关于Hash Function的设计,没有一个统一的方法,常用的方法我们也在第一章介绍过了。 2. Separate Chaining简介Separate Chaining的思想无比简单。即原本Hash表每个元素是一个输入数据的数据结构类型,现在把每个元素改成一个由该数据结构类型构成的指针链表。这样,当发生冲突时,只要在该指针链表的尾端或首端插入该值即可。 3.Rehash在详述我们的HashTable实现之前,我们还要引入最后一个概念:rehash。当我们不断往HashTable内插入元素,HashTable就会越来越满,而Find,Insert,Remove的操作都会越来越慢! 3. Separate Chaining实现接下来使用C++代码详解Separate Chaining的实现。该代码在Mac OS X 64bit系统,clang++编译器下调试通过,其他平台不能保证。想直接下载文件可以戳[这里]。 3.1 Hash表主体struct HashNode{
ElementType elementValue;
HashNode* next;
};
typedef HashNode* HashList;
struct HashTbl{
HashList* table;
int tableSize;
int content;
};
HashTbl* hashTable;
不知道为何CSDN的Markdown对混合了大段注释语句的高亮支持很不好,我把注释放在下面,已经清楚解释了这个代码的功能。 /*our hash table is actually a link to a struct called HashTbl,* the HashTbl consists of a array of HashList,which is a linked list of HashNode * and a tableSize,indicating how large is our hash table * and a content,indicating how full is our hash table * the HashNode is just a node in a linked list,consisting of the content(elementValue) and a linked to next node */ 3.2 初始化操作初始化操作很简单,不断把内存空间分配就好了。 template<class ElementType>
void HashTable<ElementType>::initialize(HashTbl*& newHashTable,int minSize)
{
newHashTable=new HashTbl;
if(newHashTable==NULL){
printf("new operation failed! At file: %s,line: %dn",__FILE__,__LINE__);
exit(-1);
}
//寻找比minSize大的最近的质数。因为质数大小的Hash表性能最好。minSize是用户一开始指定的Hash表大小。
int tableSize=nextPrime(minSize);
newHashTable->table=new HashList[tableSize];
if(newHashTable->table==NULL){
printf("new operation failed! At file: %s,__LINE__);
exit(-1);
}
for(int i=0;i<tableSize;i++){
newHashTable->table[i]=new HashNode;
if(newHashTable->table[i]==NULL){
printf("new operation failed! At file: %s,__LINE__);
exit(-1);
}
newHashTable->table[i]->next=NULL;
}
newHashTable->tableSize=tableSize;
newHashTable->content=0;
}
值得注意的一点是,这里函数声明时,第一个参数是HashTbl*&,即一个指向指针的引用。这样做的目的是,之后我们传递实际的HashTable指针进来时,可以在函数内部修改这个指针。可以参考这个文章。 3.3 Hash Functiontemplate<class ElementType>
int HashTable<ElementType>::hashFunc(ElementType elementValue)
{
//getElementKey是根据输入数据的数据结构来获得数据Key的函数。如果数据数据是整数,那么Key可以就等于输入数据。如果输入数据是字符串,那么Key可以等于所有字符对应ASCII码之和。等等。
int key=getElementKey(elementValue);
return key%(hashTable->tableSize);//using simple module method to get new position
}
3.4 Findtemplate<class ElementType>
class HashTable<ElementType>::HashNode* HashTable<ElementType>::findInner(HashTbl* _hashTable,ElementType elementValue)
{
int position=hashFunc(_hashTable,elementValue);
HashNode* hashNode=_hashTable->table[position];
while(hashNode->next!=NULL && !isEqual(hashNode->next->elementValue,elementValue)){
hashNode=hashNode->next;
}
return hashNode;
}
template<class ElementType>
bool HashTable<ElementType>::find(ElementType elementValue)
{
HashNode* hashNode = findInner(hashTable,elementValue);
return hashNode->next != NULL;
}
这个函数总体是很容易看懂的(前提是你得懂类模板哈哈,不懂的话可以参考:类模板基础 和 类模板中的结构体) 3.5 Inserttemplate<class ElementType>
bool HashTable<ElementType>::insertInner(HashTbl*& _hashTable,ElementType elementValue)
{
//rehash
if(_hashTable->content>_hashTable->tableSize*10)
{
_hashTable=rehash();
}
HashNode* insertNode=findInner(_hashTable,elementValue);
if(insertNode->next==NULL){
insertNode->next=new HashNode;
if(insertNode->next==NULL){
printf("new operation failed! At file: %s,__FILE__,__LINE__);
exit(-1);
}
insertNode->next->elementValue=elementValue;
insertNode->next->next=NULL;
_hashTable->content++;
return true;
}
return false;
}
template<class ElementType>
bool HashTable<ElementType>::insert(ElementType elementValue)
{
return insertInner(hashTable,elementValue);
}
同样,这段代码也很容易理解。rehash函数的实现见下面。 3.6 Removetemplate<class ElementType>
bool HashTable<ElementType>::removeInner(HashTbl* _hashTable,ElementType elementValue)
{
HashNode* removeNode=findInner(_hashTable,elementValue);
if(removeNode->next !=NULL){
HashNode* toBeDelete=removeNode->next;
removeNode->next=removeNode->next->next;
delete toBeDelete;
_hashTable->content--;
return true;
}
return false;
}
template<class ElementType>
bool HashTable<ElementType>::remove(ElementType elementValue)
{
return removeInner(hashTable,elementValue);
}
同样比较清楚。 3.7 rehashtemplate<class ElementType>
class HashTable<ElementType>::HashTbl* HashTable<ElementType>::rehash()
{
HashTbl* newTable;
initialize(newTable,hashTable->tableSize*10);
for(int i=0;i<hashTable->tableSize;i++){
HashNode* hashNode=hashTable->table[i]->next;
while(hashNode){
insertInner(newTable,hashNode->elementValue);
hashNode=hashNode->next;
}
}
delete hashTable;
return newTable;
}
rehash函数非常简单,创建一个两倍于原来大小的新HashTable,然后把之前每个HashNode重新插入到新的HashTable中。 3.8 nextPrimetemplate<class ElementType>
bool HashTable<ElementType>::isPrime(int num)
{
bool result;
if(num==2)
result=true;
else if(num/2*2 == num)
result=false;
else{
int sqrtNum=sqrt(num);
result=true;
for(int i=3;i<sqrtNum;i+=2){
if(num/i*i==num){
result=false;
break;
}
}
}
return result;
}
template<class ElementType>
int HashTable<ElementType>::nextPrime(int num)
{
int result=num+1;
while(!isPrime(result))
result++;
printf("result:%dn",result);
return result;
}
寻找下一个质数。 4. HashTable测试我们首先定义好使用的输入数据的数据结构,getElementKey函数以及isEqual函数。在这里,我们用最简单的int做测试。 typedef int ElementType;
int getElementKey(ElementType elementValue){
return elementValue;
}
bool isEqual(ElementType elementValue1,ElementType elementValue2){
return elementValue1==elementValue2;
}
4.1正确性测试为了测试我们的HashTable是否正确。我们在一个循环中,插入循环index,然后搜索这个index,确保每一次都能搜索到。然后在一个循环中,删除index,然后搜索这个index,确保每一次都搜索不到。 bool testHashCorrectness(HashTable<ElementType> &hashTable)
{
int checkTotal=10000000;
for(int i=0;i<checkTotal;i++){
hashTable.insert(i);
if(!hashTable.find(i))
return false;
}
for(int i=0;i<checkTotal;i++){
hashTable.remove(i);
if(hashTable.find(i))
return false;
}
return true;
}
int main()
{
HashTable<int> hashTable(100000,&getElementKey,&isEqual);
printf("check correctness: %dn",testHashCorrectness(hashTable));
return 0;
}
结果为: check correctness: 1 4.2 性能测试最后,我们对于使用或不使用rehash函数来做一个性能测试。 int main()
{
HashTable<int> hashTable(100000,&isEqual);
clock_t start=clock();
for(int i=0;i<10000000;i++){
int r=rand();
hashTable.insert(r);
}
clock_t finish=clock();
printf("time is %fsn",(double)(finish-start)/CLOCKS_PER_SEC);
return 0;
}
当我们不适用rehash函数,测试结果为: time is 47.611208s 而使用了rehash函数之后,测试结果为: time is 8.676079s 相信细调load factor之后,性能可以有更大的提升。 5. 总结这一章我们介绍了HashTable中的Separate Chaining解决Collision的方法。其核心思想就是用一个链表结构替代原先最简单的HashTable中的每一个元素。 另外我们还介绍了rehash函数,并引入了load factor概念。 Separate Chaining的优点在于其实现非常简单。并且能非常好的解决冲突问题。 Separate Chaining的缺点在于,每次插入一个元素都需要new一个内存空间,这个操作涉及到核心函数,所以速度会慢。 如何解决这个问题呢?请期待下一篇“《数据结构》学习–Hash(3)–Open Addressing”! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |