哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它经过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫作散列函数,存放记录的数组叫作散列表。数组
顺序搜索以及二叉树搜索树中,元素存储位置和元素各关键码之间没有对应的关系,所以在查找一个元素时,必需要通过关键码的屡次比较。搜索的效率取决于搜索过程当中元素的比较次数。数据结构
理想的搜索方法:能够不通过任何比较,一次直接从表中获得要搜索的元素。
若是构造一种存储结构,经过某种函数(hashFunc)使元素的存储位置与它的关键码之间可以创建一一映射的关系,那么在查找时经过该函数能够很快找到该元素。dom
当向该结构中:
插入元素时:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素时:对元素的关键码进行一样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者
称散列表)
例如:数据集合{180,750,600,430,541,900,460}
用该方法进行搜索没必要进行屡次关键码的比较,所以搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素443,会出现什么问题?
这回就要引出一个概念叫哈希冲突:对于两个数据元素的关键字 和 (i !=j),有 != ,但有:HashFun(Ki) == HashFun(Kj)即不一样关键字经过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具备不一样关键码而具备相同哈希地址的数据元素称为“同义词”。函数
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列:
闭散列:也叫开放地址法,当发生哈希冲突时,若是哈希表未被装满,说明在哈希表中必然还有空位置,那么能够把key存放到表中“下一个” 空位中去
那如何寻找下一个空余位置? 这里就要用到两种方法:线性探测和二次探测
线性探测
设关键码集合为{37, 25, 14, 36, 49, 68, 57, 11},散列表为HT[12],表的大小m = 12,假设哈希函数为:Hash(x) = x %p(p = 11,是最接近m的质数),就有:
Hash(37) = 4
Hash(25) = 3
Hash(14) = 3
Hash(36) = 3
Hash(49) = 5
Hash(68) = 2
Hash(57) = 2
Hash(11) = 0
其中25,14,36以及68,57发生哈希冲突,一旦冲突必需要找出下一个空余位置
线性探测找的处理为:从发生冲突的位置开始,依次继续向后探测,直到找到空位置为止
【插入】
1). 使用哈希函数找到待插入元素在哈希表中的位置
2). 若是该位置中没有元素则直接插入新元素;若是该位置中有元素且和待插入元素相同,则不用插入;若是该位置中有元素但不是待插入元素则发生哈希冲突,使用线性探测找到下一个空位置,插入新元素;
采用线性探测,实现起来很是简单,缺陷是:
一旦发生哈希冲突,全部的冲突连在一块儿,容易产生数据“堆积”,即:不一样关键码占据了可利用的空位置,使得寻找某关键码的位置须要许屡次比较,致使搜索效率下降。 如何缓解呢? 引入新概念负载因子(负载因子的应用在下一篇博文)和二次探测
二次探测
发生哈希冲突时,二次探查法在表中寻找“下一个”空位置的公式为:
Hi= (Ho + i^2) % m,Hi = (Ho -i^2 ) % m, i = 1,2,3…,(m-1)/Ho. 是经过散列函数Hash(x)对元素的关键码 key 进行计算获得的位置,m是表的大小假设数组的关键码为37, 25, 14, 36, 49, 68, 57, 11,取m = 19,这样可设定为HT[19],采用散列函数Hash(x) = x % 19,则:
Hash(37)=18
Hash(25)=6
Hash(14)=14
Hash(36)=17
Hash(49)=11
Hash(68)=11
Hash(57)=0
Hash(11)=11
采用二次探测处理哈希冲突:
研究代表:当表的长度为质数且表装载因子a不超过0.5时,新的表项必定可以插入,并且任何一个位置都不会被探查两次。所以只要表中有一半的空位置,就不会存在表满的问题。在搜索时能够不考虑表装满的状况,但在插入时必须确保表的装载因子a不超过0.5;若是超出必须考虑增容测试
开散列法又叫链地址法(开链法)。(将在下一篇博文中写出)
开散列法:首先对关键码集合用散列函数计算散列地址,具备相同地址的关键码归于同一子集合,每个子集合称为一个桶,各个桶中的元素经过一个单链表连接起来,各链表的头结点存储在哈希表中。.net
设元素的关键码为37, 25, 14, 36, 49, 68, 57, 11, 散列表为HT[12],表的大小为12,散列函数为Hash(x) = x % 11
Hash(37)=4
Hash(25)=3
Hash(14)=3
Hash(36)=3
Hash(49)=5
Hash(68)=2
Hash(57)=2
Hash(11)=0
使用哈希函数计算出每一个元素所在的桶号,同一个桶的链表中存放哈希冲突的元素。
一般,每一个桶对应的链表结点都不多,将n个关键码经过某一个散列函数,存放到散列表中的m个桶中,那么每个桶中链表的平均长度为。以搜索平均长度为的链表代替了搜索长度为 n 的顺序表,搜索效率快的多。
应用链地址法处理溢出,须要增设连接指针,彷佛增长了存储开销。事实上:
因为开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,因此使用链地址法反而比开地址法节省存储空间。翻译
引发哈希冲突的一个缘由多是:哈希函数设计不够合理。
哈希函数设计原则:
.哈希函数的定义域必须包括须要存储的所有关键码,而若是散列表容许有m个地址时,其值域必须在0到m-1之间
.哈希函数计算出来的地址能均匀分布在整个空间中
.哈希函数应该比较简单
下面简单介绍了一些哈希函数:
1.直接定址法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优势:简单、均匀
缺点:须要事先知道关键字的分布状况
适合查找比较小且连续的状况
2.除留余数法
设散列表中容许的地址数为m,取一个不大于m,但最接近或者等于m的质数p做为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址3.平方取中法
假设关键字为1234,对它平方就是1522756,抽取中间的3位227做为哈希地址;
再好比关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)做为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的状况
4.折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数能够短些),而后将这几部分叠加求和,并按散列表表长,取后几位做为散列地址折叠法适合事先不须要知道关键字的分布,适合关键字位数比较多的状况
5.随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数一般应用于关键字长度不等时采用此法
6.数学分析法
设有n个d位数,每一位可能有r种不一样的符号,这r种不一样的符号在各位上出现的频率不必定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号常常出现。可根据散列表的大小,选择其中各类符号分布均匀的若干位做为散列地址。
例如:假设要存储某家公司员工登记表,若是用手机号做为关键字,那么极有可能前7位都是 相同的,那么咱们能够选择后面的四位做为散列地址,若是这样的抽取工做还容易出现 冲突,还能够对抽取出来的数字进行反转(如1234改为4321)、右环位移(如1234改为4123)、左环移位、前两数与后两数叠加(如1234改为12+34=46)等方法
说了这么多概念,来看看代码。
哈希表的结构定义:设计
typedef int KeyType; typedef int ValueType; typedef enum Status { EMPTY, EXIST, DELETE, }Status; typedef struct HashNode { KeyType _key; ValueType _value; Status _status; }HashNode; typedef struct HashTable { HashNode *_table; size_t _size; size_t _N; }HashTable;
哈希表的初始化:指针
void HashTableInit(HashTable* ht) //初始化 { size_t i = 0; assert(ht); ht->_size = 0; ht->_N = HashTablePrime(0); ht->_table = (HashNode *)malloc(sizeof(HashNode)*ht->_N); assert(ht->_table); for (i=0; i<ht->_N; i++) ht->_table[i]._status = EMPTY; }
哈希函数:调试
KeyType HashFunc(KeyType key,size_t n) { return key%n; }
看看哈希表的插入:(这里处理哈希冲突时采用线性探测,二次探测将在下一次博客中写出)
扩容时要特别注意,不能简单的用malloc和realloc开出空间后直接付给哈希表,必定记得扩容以后须要从新映射原表的全部值。
int HashTableInsert(HashTable* ht, KeyType key, ValueType value) //插入 { KeyType index = key; assert(ht); **if (ht->_N == ht->_size) //扩容 { KeyType index; size_t newN = HashTablePrime(ht->_N); HashNode *tmp = (HashNode *)malloc(sizeof(HashNode)*newN); size_t i = 0; assert(tmp); //HashTablePrint(ht); //扩容调试使用 for (i=0; i<newN; i++) tmp[i]._status = EMPTY; for (i=0; i<ht->_N; i++) //扩容以后把之前的表中元素从新映射 { if (ht->_table[i]._status == EXIST) //原表存在时 { index = HashFunc(ht->_table[i]._key,newN); if (tmp[index]._status == EXIST) //发生哈希冲突时 { while (1) { index +=1; if ((size_t)index > newN) index %= newN; if (tmp[index]._status != EXIST) break; } } tmp[index]._key = ht->_table[i]._key; tmp[index]._value = ht->_table[i]._value; tmp[index]._status = EXIST; } else tmp[i]._status = ht->_table[i]._status; } ht->_table = tmp; ht->_N = newN; }** index = HashFunc(key,ht->_N); if (ht->_table[index]._status == EXIST) //发生哈希冲突 { size_t i = 0; for (i=0; i<ht->_N;i++ ) { if (ht->_table[index]._key == key) return -1; index +=i; if ((size_t)index >ht->_N) index %= ht->_N; if (ht->_table[index]._status != EXIST) break; } } ht->_table[index]._key = key; ht->_table[index]._value = value; ht->_table[index]._status = EXIST; ht->_size++; return 0; }
哈希表的查找:
HashNode* HashTableFind(HashTable* ht, KeyType key) //查找 { size_t i = 0; KeyType index = key; assert(ht); index = HashFunc(key,ht->_N); if (ht->_table[index]._key == key) return &(ht->_table[index]); else { for (i=0; i<ht->_N; i++) { index += i; if (ht->_table[index]._key == key) return &(ht->_table[index]); if (ht->_table[index]._status == EMPTY) return NULL; } } return NULL; }
哈希表的删除:
int HashTableRemove(HashTable* ht, KeyType key) //删除 { assert(ht); if(HashTableFind(ht,key)) { HashTableFind(ht,key)->_status = DELETE; return 0; } else return -1; }
哈希表的销毁:(使用了malloc开辟空间必须手动销毁)
void HashTableDestory(HashTable* ht)//销毁 { free(ht->_table); ht->_table = NULL; ht->_size = 0; ht->_N = 0; }
哈希表的打印:
void HashTablePrint(HashTable *ht) //打印hash表 { size_t i = 0; assert(ht); for (i=0; i<ht->_N; i++) { if (ht->_table[i]._status == EXIST) printf("[%d]%d ",i,ht->_table[i]._key); else if (ht->_table[i]._status == EMPTY) printf("[%d]E ",i); else printf("[%d]D ",i); } printf("\n\n"); }
哈希表整个在插入这块会比较ran,要仔细理解,特别是扩容那块。
测试案例:
void TestHashTable() { HashTable ht; HashTableInit(&ht); HashTableInsert(&ht,53,0); HashTableInsert(&ht,54,0); HashTableInsert(&ht,55,0); HashTableInsert(&ht,106,0); HashTableInsert(&ht,1,0); HashTableInsert(&ht,15,0); HashTableInsert(&ht,10,0); HashTablePrint(&ht); printf("%d ",HashTableFind(&ht,53)->_key); printf("%d ",HashTableFind(&ht,54)->_key); printf("%d ",HashTableFind(&ht,10)->_key); printf("%d ",HashTableFind(&ht,15)->_key); printf("%p ",HashTableFind(&ht,3)); printf("\n\n"); HashTableRemove(&ht,53); HashTableRemove(&ht,54); HashTableRemove(&ht,106); HashTableRemove(&ht,10); HashTableRemove(&ht,5); HashTablePrint(&ht); HashTableInsert(&ht,53,0); HashTableInsert(&ht,54,0); HashTableInsert(&ht,106,0); HashTablePrint(&ht); HashTableDestory(&ht); HashTablePrint(&ht); }
测试结果:
更多内容请关注本文博客:请戳关注连接
如需转载和翻译请联系本人。