哈希结构与哈希算法

本文介绍了哈希地址的概念,如何通过取余法计算哈希地址,以及两种解决哈希冲突的方法:开放地址法和邻接表法。同时详细阐述了如何使用数组和散列表结构来存储和插入数据,包括创建哈希表、插入操作和打印哈希结构。
摘要由CSDN通过智能技术生成

1,哈希地址

哈希地址是一个逻辑地址,也就是说,哈希地址并不是真正意义上数据的地址,而是我们为了方便储存数据而自己进行设定的一种“编号”,而哈希地址是由哈希函数来得到的,一般这个函数是由自己决定的。

这里假设用数组来描述哈希的哈希内存,也就是用数组来存储哈希数据,而在数组中,每个数据的下标就是它们的哈希地址,例如在下面这个数组中,40的下标就为0,而0,也就是A的哈希地址。一般情况下,我们使用哈希构造函数,把数据和数据的下标建立联系。

4051473344
01234

在求数据的哈希地址时,我们通常用取余法来作为哈希函数,公式如下:H(key)=key%p,这里的p一般是最大容量,而上表的最大容量就是5。上图就是用取余法得到的每个数的哈希地址。例如,H(40)=40%5=0,H(51)=51%5=1,H(47)=47%5=2。

2,哈希冲突

但是,如果采用这种取余的方法来求每个数据的哈希地址,就会产生一个问题,就是有可能会有多个数据对应一个地址,而这种现象被称为哈希冲突。而我们解决哈希冲突的方法一般有两种,一种是“开放地址法”,另一种是

首先,是开放地址法,所谓开放地址法就是如果当前地址产生了哈希冲突,就向该地址后面寻找空的地址,并将该数据放入,例如下表

3643235
012345

在上图的数组中,如果我们要放入一个数13进去,而通过H(13)=13%6=1求得13本来的哈希地址为1,但是,在1上已经有数据了,这时,我们就向下寻找,先看下标为2的数据,有数据了,再往下看下标为3的位置,发现没有数据,这时,我们就可以得出13的哈希地址为3。

而第二种方式,就是邻接表法。所谓邻接表法,就是以当前位置去创建一个链表出来,把具有冲突的元素来存放在链表中。

3,数组实现哈希结构

首先,构建一个键,来充当哈希地址的求解

typedef struct pair
{
	int key;//构建一个键出来 充当哈希地址的求解
	char element[20];//数据类型

}DATA,*LPDATA;

接下来,是哈希表的构建,这里采用的方式是取余法

typedef struct hashTable
{
	LPDATA* table;//便于初始化,以及判断当前hash地址是否存在冲突
	int divisor;//H(key)=key%p-->就是限定hash地址的数目
	//[0,p-1]
	int curSize;
}HASH,*LPHASH;

创建一个哈希结构

LPHASH createHashTable(int P)//哈希表的创建
{
	LPHASH hash = (LPHASH)malloc(sizeof(HASH));//内存申请
	assert(hash);//断言处理
	hash->curSize = 0;
	hash->divisor = P;//p为取余数
	hash->table = (LPDATA*)malloc(sizeof(LPDATA) * hash->divisor);//容量是由取余数决定
	assert(hash->table);//断言处理
	for (int i = 0; i < hash->divisor; i++)
	{
		hash->table[i] = NULL;对每个一级指针赋值为空
	}
	return hash;
}

在插入元素前,还需要寻找储存当前元素的地址

int search(LPHASH hash, int key)
{
	int pos = key % hash->divisor;//不存在冲突的hash
	//开放地址法
	int curPos = pos;
	do
	{
		//key相同,采用覆盖的数据方式
		if (hash->table[curPos] == NULL||hash->table[curPos]->key==key)
			return curPos;
		curPos = (curPos + 1) % hash->divisor;
	} while (curPos != pos);
	return curPos;
}

在找到可以存放该元素的地址后,便可以开始插入元素了

void insertData(LPHASH hash, DATA data)
{
	//求hash地址
	int pos = search(hash, data.key);
	if (hash->table[pos] == NULL)
	{
		hash->table[pos] = (LPDATA)malloc(sizeof(DATA));
		memcpy(hash->table[pos], &data, sizeof(DATA));
		hash->curSize++;
	}
	else
	{
		if (hash->table[pos]->key == data.key)
		{
			strcpy(hash->table[pos]->element, data.element);
		}
		else
		{
			printf("hash表满了,无法插入\n");
			return;
		}
	}
}

 接下来,就是将这些元素打印下来

void printHash(LPHASH hash)
{
	for (int i = 0; i < hash->divisor; i++)
	{
		if (hash->table[i] == NULL)
		{
			printf("NULL\n");
		}
		else
		{
			printf("%d:%s\n",hash->table[i]->key, hash->table[i]->element);
		}
	}
}

最后就是将前面的函数引入主函数中去

 

int main()
{
	LPHASH hash = createHashTable(10);
	DATA array[5] = { 1,"A",21,"B",13,"C",44,"D",56,"E" };
	for (int i = 0; i < 5; i++)
	{
		insertData(hash, array[i]);
	}
	printHash(hash);
	return 0;
}

4,散列表

散列表是由一个数据域和两个指针域组成的,两个指针域分别为纵向和横向。其中,纵向是一个有序链表,也就是每个元素的地址,而横向则是用来处理哈希冲突的。

102535
2126
37
4353
14

例如上表中,该哈希函数的p为5,所以10,25,35对5取余都是0,则它们产生了哈希冲突,所以就放入一个横向的链表中,而纵向的第一列也是一个链表,而这个链表则是按照哈希地址来进行有序排序的。故而散列表是由一个数据域和两个指针域构成,对于元素“10”来说,它的数据域中则放入“10”所对应的数据,一个指针域指向的是“25”对应的结点,另一个则指向“21”对应的结点。这种结构,就称为散列结构。

5,散列描述哈希结构

通过散列结构描述哈希结构,首先是定义数据

struct dataType
{
	int key;//被取余的数
	char element[20];//数据
};

接下来是创建横向链表的结点

struct Node
{
	struct dataType data;
	struct Node* next;
};
struct Node* createNode(struct dataType data)//创建结点的过程
{
	struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
	assert(newNode);
	newNode->data = data;
	newNode->next = NULL;
	return newNode;
}

然后就是散列结点描述,在该结点中,包括一个用来处理哈希冲突的横向链表,该节点的数据和指向下一个哈希地址的指针,并与下一个哈希地址组成纵向链表,而这个纵向链表就是每个哈希地址的第一个数据,拿上面的数据举例,10,21,37,43,14中的数据组成的链表就是纵向链表。

//第一列结点
struct skipListNode* createSkipList(struct dataType data)//散列结点的创建
{
	struct skipListNode* newNode = (struct skipListNode*)malloc(sizeof(struct skipListNode));
	assert(newNode);
	newNode->data = data;
	newNode->firstNode = NULL;
	newNode->next = NULL;
	return newNode;
}

既然创建了散列结点了,就要开始开始对哈希结构体进行描述,并创建哈希结构

struct listHash
{
	struct skipListNode* headNode;
	int curSize;
	int divisor;//divisor为取余数,作用是求hash地址
};
struct listHash* createHash(int divisor)
{
	struct listHash* hash = (struct listHash*)malloc(sizeof(struct listHash));
	assert(hash);//断言处理
	hash->curSize = 0;
	hash->divisor = divisor;
	hash->headNode = NULL;//这里没有创建表头
	return hash;
}

接下来,就要进行最难也就是最核心的步骤了,也就是插入数据。首先,要通过取余来得到哈希地址,如果不存在这个元素,并不存在哈希冲突,就不要考虑横向链表,就只用创建一个纵向的有序链表这时,我们就要从纵向链表中的第一个结点开始寻找,找到第一次大于该数据余数的结点,并在该结点前插入一个新的结点,如果这个纵向链表本来就不存在或该数据的哈希地址时最小的,就需要将这个新结点当作纵向链表的表头。而在寻找大于该数据的结点时,会有三种情况,一种是找到表尾也没找到,则直接将其插入表尾,一种是找到大于其的结点,则插入该结点前,最后一种就是找到一个与其哈希地址相同的结点,如果该节点的数据与要插入的数据相同,则采用覆盖的方式将其覆盖,如果不同,则在该结点插入横向链表,当然,在插入横向链表是也要判断在横向链表中判断有无冲突,若冲突,则也采用覆盖的方式,若不冲突,则采用表头法插入。代码如下

void insertData(struct listHash* hash, struct dataType data)
{
	int dataHashPos = data.key % hash->divisor;
	struct skipListNode* newSkipNode = createSkipList(data);
	if (hash->headNode == NULL)//第一种情况,表头为空
	{
		hash->headNode = newSkipNode;
		hash->curSize++;
	}
	else//如果表头不为空
	{
		//找相邻的两个结点
		struct skipListNode* pmove = hash->headNode;//移动结点
		struct skipListNode* premove = NULL;//移动的前驱结点
		//1,考虑新结点的hash地址是最小,成为表头
		if (pmove->data.key % hash->divisor > dataHashPos)//如果插入元素的hash地址小于表头的hash地址,则插入的结点为第一结点
		{
			newSkipNode->next = hash->headNode;
			hash->headNode = newSkipNode;
			hash->curSize++;
		}
		else
		{
			//纵向链表寻找相邻的两个结点(找到第一次大于插入元素hash地址位置)
			while (pmove != NULL && (pmove->data.key % hash->divisor < dataHashPos))
			{
				premove = pmove;
				pmove = pmove->next;
			}
			//分析结果
			if (pmove != NULL && (pmove->data.key % hash->divisor) == dataHashPos)//如果等于,则存在冲突
			{
				if(pmove->data.key==data.key)//相等的key,则采用覆盖的方式
				{ 
					strcpy(pmove->data.element, data.element);
				}
				
				else//不相同的key,则采用横向插入
				{
				//横向插入
				struct Node* newNode = createNode(data);
				struct Node* ppmove = pmove->firstNode;
				if (ppmove == NULL)
				{
					newNode->next = pmove->firstNode;
					pmove->firstNode = newNode;
					hash->curSize++;
				}
				else//横向链表是否存在相同键的结点
				{
					while (ppmove != NULL && ppmove->data.key != data.key)
					{
						ppmove = ppmove->next;
					}
					if (ppmove == NULL)//没有找到相同的结点,则采用表头法插入
					{
						newNode->next = pmove->firstNode;
						pmove->firstNode = newNode;
						hash->curSize++;
					}
					else//若key相同
					{
						strcpy(ppmove->data.element, data.element);
					}
				}
				}

			}
			else
			{
				premove->next = newSkipNode;
				newSkipNode->next = pmove;
				hash->curSize++;
			}
		}
	}
}

最后就是散列表的打印了

void printHash(struct listHash* hash)
{
	struct skipListNode* pmove = hash->headNode;
	while (pmove != NULL)
	{
		printf("%d:%s\t\t", pmove->data.key, pmove->data.element);
		struct Node* ppmove = pmove->firstNode;
		while (ppmove != NULL)
		{
			printf("%d:%s\t\t", ppmove->data.key, ppmove->data.element);
			ppmove = ppmove->next;
		}
		pmove = pmove->next;
		printf("\n");
	}
}

把其带入主函数:

int main()
{
	struct listHash* hash = createHash(10);//表示有10个元素,取余得到的最大地址为9
	struct dataType array[7]= { 1,"A",11,"B",23,"张三",44,"老王",56,"OK",54,"666",73,"李四"};
	for (int i = 0; i < 7; i++)
	{
		insertData(hash, array[i]);
	}
	printHash(hash);
	return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值