1.什么是哈希表(Hash Tables)
哈希表可以以极快的速度来查找、添加或删除元素(只需要数次的比较操作。)它比红黑树、二叉搜索树都要快得多。但是哈希表没有排序功能,类似的,如寻找最大值、最小值、中值这些行为都不能在哈希表中实现。
2.实现哈希表的前提条件
要想对一组元素做成哈希表形式的数据结构,这些元素需要满足两个条件:
A. 元素拥有自己的哈希值。
B. 两个元素可以判断出是否相等。
第二个条件比较容易实现,关键是第一个条件。在介绍哈希表前,将先介绍哈希值。
3. 什么是哈希值(hash code)
哈希值是一个int类型的整数。每个元素都应该有自己的哈希值,并且这个值是唯一的。即满足:
A. 如果元素a与元素b相等,则元素a的哈希值与元素b的哈希值相等。
B. 如果元素a与元素b不相等,则元素a的哈希值与元素b的哈希值不相等。
通常情况下,对于int,bool,double,string等语言自带的类型都有自己的哈希值,可以用它们的哈希函数来获取,不同语言的哈希函数可能会不同。
如果是用户自己新建的类型,则需要提供计算此类型元素哈希值的哈希函数。
在哈希表中,我们是通过某元素的哈希值来查找、添加或删除元素的。
4. 如何实现哈希表
有许多方法可以实现哈希表,这里将介绍两种:拉链法(separate chaining)和线性探测法(linear probing)。
5. 拉链法(separate chaining)
从例子入手:
现有整数类型(int)的数组S:7,30,10,9,14,19,15,12,90,94,93,53,70。此数组的元素的哈希值与元素值相同,即7的哈希值是7;30的哈希值是30。
现创建一个有5个元素的数组A:(红色颜色只是为了与数组S的元素区分开来)
然后按顺序把数组S插入数组A中:
插入元素7:元素7的哈希值为7,7%5=2,因此插入到2号元素中;
插入元素30:元素30的哈希值为30,30%5=0,因此插入到0号元素中;
插入元素10:元素10的哈希值为10,10%5=0,因此插入到0号元素中,但0号元素已经有数值30了,故插到30后面,元素30的指针指向元素10。
如此类推,插入元素9:元素9的哈希值为9,9%5=4,因此插入到4号元素中...
全部元素添加完毕后:
现在,如果我们要查找元素19:
元素19的哈希值为19,19%5=4,因此到4号元素去找;
4号元素的值为9,9!=19,去找9的指针指向的元素14;
14!=19,去找14的指针指向的元素19;
19==19,返回数值,查找完毕。
此过程只经历了3次比较!
从此例子中可以看出思路:
对于一个拥有N项元素的数组S,我们需要建立一个含有M项元素的新数组A。
M的值可以自己来定,但如果M过大,则会出现很多空链,如上述例子中的1号链;
如果M过小,则会出现链太长的情况,如果链太长,则意味着比较次数变多。
一般建议M=N/5。
插入元素:
int j=S[i]的哈希值%5;
然后检查A[j]是否为空,如果空,A[j]=S[i]; 如果不为空,temp=A[j], A[j]=S[i], S[i].next=temp;
删除元素:先找出该元素,然后此元素的上一个元素的指针指向此元素的下一个元素,最后删除此元素。
拉链法有一个缺陷,就是很容易有些链过长,有些链过短,甚至是空链。这样会浪费内存和影响搜索速度。
6. 线性探测法(linear probing)
从例子入手:
现有整数类型(int)的数组S:7,30,10,9,14,19,15,29,13,27。此数组的元素的哈希值与元素值相同,即7的哈希值是7;30的哈希值是30。
现创建一个含有14个元素的数组A:(上面那排数字是为了方便看哪个元素是第几项元素)
然后按顺序把数组S插入数组A中:
插入元素7:元素7的哈希值为7,7%14=7,因此插入到7号元素中;
插入元素30:元素30的哈希值为30,30%14=2,因此插入到2号元素中;
插入元素10:元素10的哈希值为10,10%14=10,因此插入到10号元素中;
插入元素9:元素9的哈希值为9,9%14=9,因此插入到9号元素中;
插入元素14:元素14的哈希值为14,14%14=0,因此插入到0号元素中;
插入元素19:元素19的哈希值为19,19%14=5,因此插入到5号元素中;
插入元素15:元素15的哈希值为15,15%14=1,因此插入到1号元素中;
插入元素29:元素29的哈希值为29,29%14=1,但1号元素已经有数值15了,然后检查下一个元素(2号元素),但2号元素已经有数值30了,然后检查下一个元素(3号元素),3号元素为空,插入到3号元素:
插入元素13:元素13的哈希值为13,13%14=13,因此插入到13号元素中:
插入元素27:元素27的哈希值为27,27%14=13,但13号元素已经有数值13了,然后检查下一个元素(0号元素),但0号元素已经有数值14了,如此类推,找到4号元素为空,插入到4号元素:
如果我们要查找元素29,元素29的哈希值为29,29%14=1,检查1号元素,15!=29;然后检查下一个元素(2号元素),30!=29;然后检查下一个元素(3号元素),29=29,返回数值,查找完毕。
从此例子中可以看出思路:
对于一个拥有N项元素的数组S,我们需要建立一个含有M项元素的新数组A。
M一定要比N大。如果M过少,则搜索进行的比较次数增加,影响速度;如果过大,则太多空位没利用,造成内存浪费。
一般建议M=2*N。
插入元素i: 先获取i的哈希值%M,根据这个值插入到相应的位置中,如果位置有元素了,则插入到下一个位置,循环进行,直到位置是空的为止。
搜索元素:类似于插入元素,详细见上述例子。
7.哈希表算法实现
/*********************************************************************
* 哈希表算法实现
* (c)copyright 2013,jdh
* All Right Reserved
*文件名:main.c
*程序员:jdh
**********************************************************************/
#include <stdio.h>
#include <stdlib.h>
/*********************************************************************
* 宏定义
**********************************************************************/
/*********************************************************************
* 数据类型重定义
**********************************************************************/
#define uint8_t unsigned char
#define uint16_t unsigned short
#define uint32_t unsigned long
/*********************************************************************
* 哈希表长度
**********************************************************************/
#define HASH_TABLE_LEN 100
/*********************************************************************
* 数据结构
**********************************************************************/
//链表节点
typedef struct _Link_Node
{
uint16_t id;
uint16_t data;
struct _Link_Node *next;
}Link_Node,*Link_Node_Ptr;
//哈希表头
typedef struct _Hash_Header
{
struct _Link_Node *next;
}Hash_Header,*Hash_Header_Ptr;
/*********************************************************************
* 全局变量
**********************************************************************/
//哈希表
Hash_Header_Ptr Hash_Table[HASH_TABLE_LEN];
/*********************************************************************
* 函数
**********************************************************************/
/*********************************************************************
* 哈希表函数
*说明:
*1.用哈希函数生成id对应的哈希表中的位置
输入:id
返回:位置
**********************************************************************/
uint8_t hash_func(uint16_t id)
{
uint8_t pos = 0;
pos = id % HASH_TABLE_LEN;
return pos;
}
/*********************************************************************
* 初始化节点
*返回:结点指针
**********************************************************************/
Link_Node_Ptr init_link_node(void)
{
Link_Node_Ptr node;
//申请节点
node = (Link_Node_Ptr) malloc(sizeof(Link_Node));
//初始化长度为0
node->next = NULL;
return node;
}
/*********************************************************************
* 初始化哈希表头结点
*返回哈希表头结点指针
**********************************************************************/
Hash_Header_Ptr init_hash_header_node(void)
{
Hash_Header_Ptr node;
//申请节点
node = (Hash_Header_Ptr) malloc(sizeof(Hash_Header));
//初始化长度为0
node->next = NULL;
return node;
}
/*********************************************************************
* 哈希表初始化
*说明:
*1.初始化哈希表Hash_Table
*2.哈希表长度最大不能超过256
**********************************************************************/
void init_hash_table(void)
{
uint8_t i = 0;
for (i = 0;i < HASH_TABLE_LEN;i++)
{
Hash_Table[i] = init_hash_header_node();
Hash_Table[i]->next = NULL;
}
}
/*********************************************************************
* 在哈希表增加节点
*说明:
*1.在哈希表的某个链表末增加数据
输入:new_node:新节点
**********************************************************************/
void append_link_node(Link_Node_Ptr new_node)
{
Link_Node_Ptr node;
uint8_t pos = 0;
//新节点下一个指向为空
new_node->next = NULL;
//用哈希函数获得位置
pos = hash_func(new_node->id);
//判断是否为空链表
if (Hash_Table[pos]->next == NULL)
{
//空链表
Hash_Table[pos]->next = new_node;
}
else
{
//不是空链表
//获取根节点
node = Hash_Table[pos]->next;
//遍历
while (node->next != NULL)
{
node = node->next;
}
//插入
node->next = new_node;
}
}
/*********************************************************************
* 在哈希表查询节点
*说明:
*1.知道在哈希表某处的单链表中,并开始遍历.
*2.返回的是查询节点的前一个节点指针.这么做是为了做删除操作.
输入:pos:哈希表数组位置,从0开始计数
id:所需要查询节点的id
root:如果是根节点,则*root = 1,否则为0
返回:所需查询的节点的前一个节点指针,如果是根节点则返回根节点,失败返回0
**********************************************************************/
Link_Node_Ptr search_link_node(uint16_t id,uint8_t *root)
{
Link_Node_Ptr node;
uint8_t pos = 0;
//用哈希函数获得位置
pos = hash_func(id);
//获取根节点
node = Hash_Table[pos]->next;
//判断单链表是否存在
if (node == NULL)
{
return 0;
}
//判断是否是根节点
if (node->id == id)
{
//是根节点
*root = 1;
return node;
}
else
{
//不是根节点
*root = 0;
//遍历
while (node->next != NULL)
{
if (node->next->id == id)
{
return node;
}
else
{
node = node->next;
}
}
return 0;
}
}
/*********************************************************************
* 在哈希表删除节点
*说明:
*1.删除的不是当前节点,而是当前节点后的一个节点
输入:node:删除此节点后面的一个节点
new_node:新节点
**********************************************************************/
void delete_link_node(Link_Node_Ptr node)
{
Link_Node_Ptr delete_node;
//重定向需要删除的前一个节点
delete_node = node->next;
node->next = delete_node->next;
//删除节点
free(delete_node);
delete_node = NULL;
}
/*********************************************************************
* 在哈希表删除根节点
输入:node:根节点
**********************************************************************/
void delete_link_root_node(Link_Node_Ptr node)
{
uint8_t pos = 0;
//用哈希函数获得位置
pos = hash_func(node->id);
//哈希表头清空
if (node != NULL)
{
Hash_Table[pos]->next = node->next;
//删除节点
free(node);
node = NULL;
}
}
/*********************************************************************
* 获得哈希表中所有节点数
输入:node:根节点
**********************************************************************/
uint16_t get_node_num(void)
{
Link_Node_Ptr node;
uint16_t i = 0;
uint16_t num = 0;
//遍历
for (i = 0;i < HASH_TABLE_LEN;i++)
{
//获取根节点
node = Hash_Table[i]->next;
//遍历
while (node != NULL)
{
num++;
node = node->next;
}
}
return num;
}
/*********************************************************************
* 从哈希表中获得对应序号的节点
*参数:index:序号.从1开始,最大值为节点总数值
* root:如果是根节点,则*root = 1,否则为0
返回:所需查询的节点的前一个节点指针,如果是根节点则返回根节点,失败返回0
**********************************************************************/
Link_Node_Ptr get_node_from_index(uint16_t index,uint8_t *root)
{
Link_Node_Ptr node;
uint16_t i = 0;
uint16_t num = 0;
//遍历
for (i = 0;i < HASH_TABLE_LEN;i++)
{
//获取根节点
node = Hash_Table[i]->next;
//判断单链表是否存在
if (node == NULL)
{
continue;
}
//根节点
num++;
if (num == index)
{
//是根节点
*root = 1;
return node;
}
//遍历
while (node->next != NULL)
{
num++;
if (num == index)
{
//不是根节点
*root = 0;
return node;
}
node = node->next;
}
}
return 0;
}
/*********************************************************************
* 删除hash表中所有节点
**********************************************************************/
void drop_hash()
{
Link_Node_Ptr node;
uint16_t i = 0;
Link_Node_Ptr node_next;
//遍历
for (i = 0;i < HASH_TABLE_LEN;i++)
{
//获取根节点
node = Hash_Table[i]->next;
while (1)
{
//判断单链表是否存在
if (node == NULL)
{
//不存在
Hash_Table[i]->next = NULL;
break;
}
//根节点下一个节点
node_next = node->next;
//删除根节点
free(node);
//重指定根节点
node = node_next;
}
}
}
/*********************************************************************
* 输出所有节点
**********************************************************************/
void printf_hash()
{
Link_Node_Ptr node;
uint8_t root = 0;
uint8_t i = 0;
uint8_t num = 0;
printf("-------------打印hash表-------------\n");
num = get_node_num();
for (i = 1;i <= num;i++)
{
node = get_node_from_index(i,&root);
if (node != 0)
{
if (root)
{
printf("根节点:节点号%d,id为%d\n",i,node->id);
}
else
{
printf("普通节点:节点号%d,id为%d\n",i,node->next->id);
}
}
}
}
/*********************************************************************
* 主函数
*说明:实现对哈希表的新建,建立节点,查询及增加,删除节点的操作
**********************************************************************/
int main()
{
Link_Node_Ptr node;
uint8_t temp = 0;
uint8_t root = 0;
uint8_t i = 0;
init_hash_table();
//插入数据id = 1,data = 2;
node = init_link_node();
node->id = 1;
node->data = 2;
append_link_node(node);
//查询节点数
printf("1节点数为%d\n",get_node_num());
node = init_link_node();
node->id = 100;
node->data = 101;
append_link_node(node);
node = init_link_node();
node->id = 1002;
node->data = 1001;
append_link_node(node);
node = init_link_node();
node->id = 10000;
node->data = 10001;
append_link_node(node);
node = init_link_node();
node->id = 1000;
node->data = 10001;
append_link_node(node);
node = init_link_node();
node->id = 2;
node->data = 10001;
append_link_node(node);
//查询节点数
printf("2节点数为%d\n",get_node_num());
//查询id = 1000;
node = search_link_node(100,&temp);
if (node != 0)
{
if (temp == 0)
{
printf("删除普通节点:所需查询id的值为%d,数据为%d\n",node->next->id,node->next->data);
//删除
delete_link_node(node);
}
else
{
//根节点
printf("删除根节点所需查询id的值为%d,数据为%d\n",node->id,node->data);
//删除
delete_link_root_node(node);
}
}
else
{
printf("查询失败\n");
}
//查询id = 1000;
node = search_link_node(1001,&temp);
if (node != 0)
{
if (temp == 0)
{
printf("所需查询id的值为%d\n",node->next->data);
}
else
{
//根节点
printf("所需查询id的值为%d\n",node->data);
}
}
else
{
printf("查询失败\n");
}
//查询节点数
printf("节点数为%d\n",get_node_num());
printf_hash();
getchar();
return 0;
}