概念: “散列表(Hash table,哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
作用方式: 由关键字通过哈希函数求出一个值来进行数组的快速访问(类似于索引的作用,此处索引类似于数组下标。)
本质: 数组
哈希表如何存储数据
通过哈希函数求出各关键字的索引,例如生活中查找字典时某汉字的拼音首字母可以看成一个索引,然后根据这个索引得知该数据在数组中的位置,最后储存。
哈希冲突
从上面的例子可以知道,不同的关键字可以具有同样的索引,而相应的数组位置只有一个,造成了多个数据都想要储存在一个地方的情况,那么这样的情况就叫做哈希冲突。
处理哈希冲突
处理哈希冲突的方法有很多,这里介绍两种常用的方法。1.开放寻址法。2.拉链法。
1.开放寻址法
当数据想要存放的位置被占时,开放其他位置供其储存。开放哪些位置呢? 回到生活中去,字典中拼英首字母相同的汉字是一个接一个的,所以直接在被占位置的下一个位置储存即可,若下一个位置仍然被占,重复此步骤。这样是否会影响查找的速度呢? 即使位置被占,但是索引小的仍然在索引大的值的前面。忽略数组下标的话,所有数据依旧按照索引小到索引大的顺序被安放。缺点: 受数组长度大小影响较大,当数组长度不够时,需要扩容。
2.拉链法
开放寻址法受数组大小限制,那么有没有一种方法不受数组长度大小的限制呢?这就是我们要讲的拉链法。介绍拉链法之前,先引入一个概念。
2.1 哈希桶
顾名思义,哈希桶就是像一个桶一样盛放着关键字对应的索引。
书接上文,当两个数据想要储存在同一个位置时,后来的数据可以储存在先来的数据的后面,然后两个数据用链表相连,即将索引所对应的数组位置当作一个新链表的表头,储存在这个位置的数据接在这个链表的头部。缺点:如果发生哈希冲突的数据过多,那相应的链表会变得很长,而数组的遍历是一个接一个,会影响到数据的查找速度。
扩容
细心的读者会发现,前面提到了扩容一词。那么,什么是扩容呢?当哈希表所占位置过多时,发生哈希冲突的概率就会增大,这个也很好理解,位置少了冲突自然就多了。所以就需要扩容来解决这个问题。当然,并非要等到哈希表全部被占满时才进行扩容,那样做的话显得太蠢。一般来说会设置一个增长因子(也叫负载因子),简单点说就是已经被占的位置占总位置的一个百分比。例如负载因子为0.7,就是说当所占位置超过总位置的百分之70后,会进行扩容。当然,扩容并不是想怎么扩怎么扩,一般来说,会新建立一个大小是原来两倍的新数组,再重新对所有数据Hash一遍填入新的数组。
哈希表如何读取数据 前面提到利用关键字通过哈希函数得到的索引来进行访问数组的操作即可读取相应位置的数据。如果在发生哈希冲突的情况下,第一次读取的不是我们想要的数据,读取下一个即可。
哈希表的大致内容介绍完毕,在哈希表中,各种数据通过哈希函数得到索引填入数组,数据我们无法改变,哈希函数却可以自己设定,所以,哈希函数的设定也影响这填入数据的顺利程度,设置一个好的哈希函数,可以极大的降低发生哈希冲突的次数。下面介绍几种常见的哈希函数的设定方法。
哈希函数的设定方法
1.直接定址法
直接用关键字或者关键字的某个线性函数值来当哈希表的索引。即y=
x或y=kx+b。(y为索引,x为关键字)
2.除留余数法
取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为索引。即y=x%p(p<m)。
3.平方取中法
取关键字的平方的中间几位作为索引。
4.折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为索引。
5.随机数法
选择一个随机函数,取关键字的随机函数值为它的索引。
键值对: 存放一个关键字key和一个对应的值value。
代码部分(开放寻址法)
a.结构体定义
//键值对
typedef int KeyType;
typedef int ValueType;
//哈希函数指针
typedef int (*HashFunc)(KeyType key); //HashFunc为一个函数指针,函数类型带一个KeyType,返回int
//哈希表中元素状态
typedef struct enum{
EMPTY, //当前点为空
VALUE, //当前点有元素
DELETED, //当前点已删除
}State;
//将键值对存放在结构体中
typedef struct KeyValue{
KeyType key;
ValueType value;
State state;
}KeyValue;
//哈希表
typedef struct HaskTable{
KeyValue data[HashMaxSize];
size_t size;
HashFunc func;
}HashTable;
b.初始化哈希表和销毁哈希表
//哈希函数(除留余数法)
int HashFunction(KeyType Key)
{
return Key%HashMaxSize;
}
//哈希表初始化
void HashInit(HashTable* ht,HashFunc func)
{
if(ht == 0)
return;
ht->size = 0;
ht->func = func;
size_t i = 0;
for(;i<HashMaxSize;i++)
{
ht->data[i].state = EMPTY;
}
}
//销毁哈希表
void HashDestroy (HashTable* ht)
{
if(ht == 0)
return;
ht->size = 0;
ht->func = NULL;
}
b.插入和删除操作
注意到在初始化哈希表的时候将状态初始化为EMPTY;插入后有效数据的状态为VALUE便于区分,DELETED表示该元素已经被删除,可以填入新的元素.
//插入
void HashInsert(HashTable* ht,KeyType key,ValueType value)
{
if(ht == NULL)
return;
size_t size_max = 0.8*HashMaxSize; //负载因子
if(ht->size >= size_max)
return;
size_t offset = ht->func(key) //通过哈希函数计算索引
while(1)
{
if(ht->data[offset].state != VALUE) //要储存的位置无值
{
ht->data[offset].key = key;
ht->data[offset].value = value;
ht->data[offset].state = VALUE;
ht->size ++;
break;
}
else if(ht->data[offset].key == key) //key已存在 略过
break;
else if(offset >= HasnMaxSize) //到达哈希表末尾,从头开始
offset = 0;
offset++;
}
}
//删除
void HashRemove(HashTable ht,KeyType key)
{
if(ht == NULL)
return;
if(ht->size == 0)
return;
size_t offset = ht->func(key); //哈希函数求索引
while(ht->data[offset].state != EMPTY) //索引对应的值存在
{
if(ht->data[offset].state == VALUE && ht->data[offset].key == key)
{
ht->data[offset].state = DELETED; //删除
ht->size--;
}
else //在while的大条件下else 为 所查看的数据状态为DELETED
{
if(offset >= HashMaxSize) //DELETED也要继续查看数据,防止后面有有效数据
offset = 0;
offset++;
}
}
}
c.查找
KeyValue* HashFind(HashTable* ht, KeyType key)
{
if(ht == NULL)
return NULL;
size_t offset = ht->func(key); //通过索引查找
while(ht->data[offset].stage != EMPTY) //与删除操作大体相同
{
if(ht->data[offset].key == key && ht->data{offset}.stage == VALUE)
return &ht->data[offset];
else
{
if(offset >= HashMaxT)
offset = 0;
offset++;
}
}
return NULL; //没有查找到
}
代码部分(拉链法)
a.结构体定义
typedef int KeyType;
typedef int ValueType;
typedef int (*HashFunc)(KeyType key); //同开放寻址法
//键值对结构体
typedef struct KeyValue {
KeyType key;
ValueType value;
struct KeyValue* next;
}KeyValue;
//哈希表
typedef struct HashTable{
KeyValue* data[HashMaxSize];
size_t size;
HashFunc func;
}HashTable;
b.哈希表的初始化和销毁
//初始化
void HashInit(HashTable* ht,HashFunc func)
{
if(ht == NULL)
return;
ht->size = 0;
ht->func = func;
size_t i =0;
for(;i<HashMaxSize;i++)
{
ht->data[i] = NULL;
}
}
//销毁每个节点的链表
void _HashDestroy(KeyValue* to_destroy)
{
KeyValue* cur = to_destroy->next;
free(to_destroy);
if(cur != NULL)
_HashDestroy(cur);
}
//摧毁哈希表
void HashDestroy(HashTable* ht)
{
if(ht == NULL)
{
return;
}
size_t i=0;
for(;i<HashMaxSize;i++)
{
if(ht->data[i] != NULL)
{
_HashDestroy(ht->data[i]);
ht->data[i] = NULL;
}
}
}
插入和删除
//欲插入,先创造节点
KeyValue* CreateNode(KeyType key,ValueType value)
{
KeyValue* new_node = (KeyValue*)malloc(sizeof(KeyValue));
if(new_node == NULL)
return NULL;
new_node->key = key;
new_node->value = value;
new_node->next = NULL;
return new_node;
}
//插入元素
void HashInsert(HashTable* ht,KeyType key,ValueType )
{
if(ht == NULL)
return;
if(ht->size >= HashMaxSize)
return;
size_t offst = ht->func(key); //确定索引
KeyValue* cur = ht->data[offset];
while(cur != NULL)
{
if(cur->key == key )
return; //想要填入的数据已存在,插入失败。
cur = cur -> next;
} //出循环之后,哈希表内没有与该数据重合的数据,可以插入
KeyValue* new_node = CreatNode(key,value);
new_node = ht->data[offset];
ht->data[offset]= new_node;
++ht->size; //头插法,并更新size
}
//删除指定元素
void HashRemove(HashTable* ht,Keytype key)
{
if(ht == NULL)
return;
size_t offset = ht->func(key);
KeyValue* prev = NULL; //辅助指针
KeyValue* cur = ht->data[offset];
while(cur != NULL)
{
if(cur->key == key && prev ==NULL) //要删除的节点就是第一个
{
ht->data[offset] = cur->next;
free(cur);
return;
}
else if(cur->key == key && prev!= NULL) //要删除的节点不是第一个
{
prev->next = cur->next;
free(cur);
return;
}
prev = cur;
cur = cur->next; //移动指针
}
return;
}
查找
KeyValue* Hashfind(HashTable* ht,KeyType key)
{
if(ht == NULL)
return NULL;
size_t offset = ht->func(key);
KeyValue* cur = ht->data[offset]; //求索引
while(cur != NULL) //遍历链表查找
{
if(cur->key == key)
return cur;
cur = cur->next;
}
return NULL; //没有查找到 返回NULL;
}
------------------------------------THE END---------------------------------------
PS:索引为个人称呼,便于理解.