1、哈希表
理想情况是不希望经过任何比较,一次存取,便可以直接定位,这是需要一种映射关系,我们把这个映射关系就叫为哈希函数;
哈希表在对大数据的处理查找上,有优势;
若数据有序,-------->我们一般会想到二分查找;但是哈希查找更加犀利;
针对哈希查找,我们主要学习:
i>如何构造哈希函数;
ii>怎么解决冲突问题
2、哈希函数
这个哈希函数的构造方法比较多:(1)、直接定址法 (2)、数字分析法 (3)、平方取中法 (4)、折叠法
(5)、除留余数法 (6)、随机数法;
简单说一下,直接定址法,用的少,且需要空间过大;
以上的哈希函数,最常用的是除留余数法;什么是除留余数法?
在哈希表中,总空间大小为m,选一个p,p满足两个条件i>: p <= m(越接近m越好); ii>: p必须是质数;则哈希函数为:
hash(key) = kep % p; p<=m;
p为什么一定要选质数呢?
:经过数学的理论推导,p为质数时,可以保证模之后概率均等,均匀分布;p要不是质数,则不满足随机、均匀这些条件,就不能保证每个数字均等存储。
3、解决冲突
(1)、线性探测法:
线性探测法存储,此时存放的位置如下图,因为在下标为1处已经存放数字了,此时其后的数据也要存放到这个位置,产生冲突;按照线性探测法的解决方案:有冲突一直往后探测,直到有空间可以放数据时,就存储在那个位置。
(2)、二次探测法
二次探测法存储,此时存放的位置如下图,因为在下标为1处已经存放数字了,此时其后的数据也要存放到这个位置,产生冲突;按照二次探测法的解决方案:有冲突先向左探测,若没有空间,再向右探测,来回左右探测,直到有空间可以放数据时,就存储在那个位置。
以上的两种解决冲突的方案不是很好,缺点:定位准确度不高,查找起来效率较低。
(3)、双散列法
需要两个哈希函数,第一个元素直接就是记哈希表的下标,第二个记到达该数字的移位量,从而达到直接定位。如下图:
(4)、链地址法
哈希表中存放的是指针(指向结点的指针),在冲突处,通过指针连接起来;如下图:
关于链表的连接问题:
头插----->效率比较高
尾插----->效率相对来说比较低,(因为的一个一个的查找最后一个结点);
解决尾插效率低的方案:以空间换时间,就是在哈希表中存放的是一个结构体类型,成员如下:
typedef struct HASH_TABLE{//哈希表也就是线性表结构,表中每个数据的类型; HASH_NODE *first; //指向哈希结点(链表结点)的指针;first始终指向第一个结点; HASH_NODE *last; //last始终指向最后一个结点; int size; //size记录其上挂了多少数据;可有可无,看实际需求; }HASH_TABLE;
4、除留余数法+开链法(重点实现)
此次全部代码均用C语言实现,与C++不牵扯!!
我把函数声明和定义放到了一个.h文件中,只是为了方便实现,本应该分开存放;
除留余数法的重点是:hash()函数;开链法的重点是:链表的操作;
哈希表上应有的操作函数(其实主要是链表的操作):
void initHashTable(HashTable ht); //初始化哈希表 void prevInsert(HashTable ht, ElemType value); //数据的前插 void showHashTable(HashTable ht); //显示数据 void endInsert(HashTable ht, ElemType value); //数据的后插 HASH_NODE* find(HashTable ht, ElemType value); //查找函数 boolean remove_(HashTable ht, ElemType value); //删除哈希表中的数据 void destoryHashTable(HashTable ht); //销毁哈希表 void clearHashTable(HASH_NODE *p); //真正的销毁函数
(1)、数据的前插:
void prevInsert(HashTable ht, ElemType value){ int index = Hash(value); HASH_NODE *p = (HASH_NODE *)malloc(sizeof(HASH_NODE)); p->data = value; p->next = NULL; //前插,这条语句可以不写; p->next = ht[index]; ht[index] = p; }
(2)、显示数据:
void showHashTable(HashTable ht){ int i; HASH_NODE *p; for(i = 0; i < P; i++){ printf("%d : ", i); if(ht[i]){ p = ht[i]; while(p){ printf("%d-->", p->data); p = p->next; } } printf("NULL\n"); } }
(3)、数据的后插:
void endInsert(HashTable ht, ElemType value){ int index = Hash(value); HASH_NODE *p = (HASH_NODE *)malloc(sizeof(HASH_NODE)); HASH_NODE *q; p->data = value; p->next = NULL; if(ht[index] == NULL){ //判断是不是第一个结点; ht[index] = p; }else{ q = ht[index]; while(q->next){ q = q->next; } q->next = p; } }
(4)、查找函数:
HASH_NODE* find(HashTable ht, ElemType value){ int index = Hash(value); HASH_NODE *p = ht[index]; while(p && p->data != value){ //没找完并且没找到,特别注意,已免发生内存非法访问! p = p->next; } return p; //直接返回p;p为空就是没找到,否则就是找到了; /* if(p){ return p; }else{ return NULL; } */ }
(5)、删除函数:
boolean remove_(HashTable ht, ElemType value){ int index = Hash(value); HASH_NODE *q; HASH_NODE *p = find(ht, value); //利用找到函数,求得要找值得地址 if(p == NULL){ //为空,就是没找到,直接退出 return FALSE; } q = ht[index]; if(ht[index] == p){ //是第一个结点 ht[index] = p->next; }else{ while(q->next != p){ //其后结点的删除,也就是再找前驱节点; q = q->next; } q->next = p->next; free(p); } return TRUE; }
5、完整代码、测试代码、测试结果
(1)、完整代码:
#ifndef _HASH_TABLE_H_ #define _HASH_TABLE_H_ typedef unsigned char boolean; #define TRUE 1 #define FALSE 0 #define P 7 #include//链表中的数据类型 typedef struct HASH_NODE{ ElemType data; struct HASH_NODE *next; }HASH_NODE; //哈希表中的每个元素是指针类型,但是哈希表又是数组;所以为指针数组类型; typedef HASH_NODE *HashTable[P]; int Hash(ElemType key){ return key % P; } void initHashTable(HashTable ht); void prevInsert(HashTable ht, ElemType value); void showHashTable(HashTable ht); void endInsert(HashTable ht, ElemType value); HASH_NODE* find(HashTable ht, ElemType value); boolean remove_(HashTable ht, ElemType value); void destoryHashTable(HashTable ht); void clearHashTable(HASH_NODE *p); void clearHashTable(HASH_NODE *p){ HASH_NODE *q; while(p){ q = p->next; free(p); p = q; } } void destoryHashTable(HashTable ht){ int i; for(i = 0; i < P; i++){ clearHashTable(ht[i]); ht[i] = NULL; } } boolean remove_(HashTable ht, ElemType value){ int index = Hash(value); HASH_NODE *q; HASH_NODE *p = find(ht, value); //利用找到函数,求得要找值得地址 if(p == NULL){ //为空,就是没找到,直接退出 return FALSE; } q = ht[index]; if(ht[index] == p){ //是第一个结点 ht[index] = p->next; }else{ while(q->next != p){ //其后结点,也就是再找前驱节点; q = q->next; } q->next = p->next; free(p); } return TRUE; } HASH_NODE* find(HashTable ht, ElemType value){ int index = Hash(value); HASH_NODE *p = ht[index]; while(p && p->data != value){ //没找完并且没找到,特别注意,已免发生内存非法访问! p = p->next; } return p; /* if(p){ return p; }else{ return NULL; } */ } void endInsert(HashTable ht, ElemType value){ int index = Hash(value); HASH_NODE *p = (HASH_NODE *)malloc(sizeof(HASH_NODE)); HASH_NODE *q; p->data = value; p->next = NULL; if(ht[index] == NULL){ //判断是不是第一个结点; ht[index] = p; }else{ q = ht[index]; while(q->next){ q = q->next; } q->next = p; } } void showHashTable(HashTable ht){ int i; HASH_NODE *p; for(i = 0; i < P; i++){ printf("%d : ", i); if(ht[i]){ p = ht[i]; while(p){ printf("%d-->", p->data); p = p->next; } } printf("NULL\n"); } } void prevInsert(HashTable ht, ElemType value){ int index = Hash(value); HASH_NODE *p = (HASH_NODE *)malloc(sizeof(HASH_NODE)); p->data = value; p->next = NULL; //前插,这条语句可以不写; p->next = ht[index]; ht[index] = p; } void initHashTable(HashTable ht){ int i; for(i = 0; i < P; i++){ ht[i] = NULL; } } #endif
(2)、测试代码:
#includetypedef int ElemType; #include"hashTable.h" void main(void){ HashTable ht; int ar[] = {1, 8, 15, 3, 6, 2, 8, 9, 22, 31,}; int n = sizeof(ar) / sizeof(int); int i; HASH_NODE *p; //C语言只能在所有的有效语句之前定义变量; boolean k; initHashTable(ht);//数组名称的传递退化为指针 for(i = 0; i < n; i++){ //prevInsert(ht, ar[i]); endInsert(ht, ar[i]); } showHashTable(ht); p = find(ht, 10); if(p){ printf("找到了,地址为:%p\n", p); }else{ printf("输入的数据不存在,没找到\n"); } k = remove_(ht, 99); if(k){ printf("删除成功\n"); }else{ printf("删除失败\n"); } destoryHashTable(ht); //表空间在栈,数据存放在堆!随着程序结束,堆空间释放。 }
(3)、测试结果: