散列表的基本原理和实现

        散列表是实现字典操作的一种有效的数据结构,尽管在最坏情况下查找一个元素时间和链表相同。但实际应用中,散列表的性能是极好的,合理情况下,平均时间复杂度为O(1)。

typedef struct Datatype {
	int key;    
	USER_TYPE value;
}Datatype;

typedef struct HashTable {
	int times;
	Datatype data;
}HashTable;

        散列表是一个在时间和空间上做出权衡的数据结构。如果空间允许,可以把每个关键字当做数组下标;如果时间允许,不考虑时间消耗的话,顺序查找即可。散列表恰恰值介于这二者之间,散列表使用一个长度与实际存储数目成比例的数组来存储。

        常见的构造哈希函数是取余构造,就是把余数当做哈希值,除此之外还有:

int HashFunc(int key) {
//除留取余的构造哈希函数方法
	return key % HashTableSize;
}

        平方取中散列法:通过次方放大关键字,再取中间几位作为哈希值,通过次方放大关键字减少各关键字之间的碰撞。

        基数转换法:把十进制数看成其他进制,再按照这个进制转换为十进制,提取若干位作为哈希值。

        在构造散列表的时候,通过哈希函数计算哈希值,如果出现关键字不同但是哈希值相同时,就是产生哈希碰撞,需要给出解决哈希碰撞的方案。解决哈希碰撞的方法有:

          1、链地址法:把一个哈希值产生冲突关键字放进一个链表里面,某个哈希值产生冲突了,就把这个关键字放到这个哈希值槽的链表里面。Java中的HashMap就是这样解决冲突的,不过现在一旦同一个哈希值冲突超过7个还是8个就会把链表转换为红黑树,查找的时候效率更高。

           2、开放寻址法:大体的思想是一旦产生哈希碰撞,接着找一下空的哈希槽位,直到找到合适的位置,其中方法包括:线性探查,二次探查、伪随机探查。

            线性探查就沿着产生冲突的位置接着找它紧挨着下一个,直到不冲突了。

            二次探查看上去像在冲突的位置左右跳跃的找不冲突的位置。

            伪随机探查:建立一个随机数发生器,并给定一个随机数作为起始点,在冲突的位置借助这个随机数发生器生成随机数,去探查下一个合适的位置。

扫描二维码关注公众号,回复: 1839585 查看本文章

        3、完全散列(算法导论156页):我的理解是散列的散列来解决冲突,不用链表也不用线性再探测找一个不冲突的位置。每一个哈希桶里面放的还是一个散列表,第一次算出在外层哪个哈希桶里面,接着计算在这个哈希桶的散列的第几个。重点就砸要保证第二次的散列计算不产生冲突,书里面写了一堆证明,生日悖论、马尔科夫啥的,我没看懂。这种二级散列可以用指针数组做到,数组(一级散列表)中每一个指针指向一个一维数组(二级散列表)



           一个散列表应该满足插入、删除、查找的基本功能。   

1、散列表的插入

        插入的时候首先要判断通过哈希函数计算出来的哈希值是否产生冲突,冲突了就通过解决冲突的函数,直到找到一个不冲的位置,将关键字放进去。

int HashInsert(HashTable *ht, Datatype x) {
	int address;
//这里的插入算法首先得找到一个空的散列地址 如果找到地址已经存在一个元素
//说明产生了哈希碰撞,需要解决碰撞,

	address = HashFunc(x.key);
	if(address >= 0) {
		if(x.key == ht[address].data.key) {
			printf("%d is existed \n", x.key);
			return false;
		}else {
//说明发生了哈希碰撞当前这个位置就不行
//需要找到一个空地址
//给老子找!
			while(ht[address].data.key != ADDRESSTEMP) {
				address = Collision(address);
			}			
		}
	}

	ht[address].data = x;     //计算的ht[address]赋值存放
	ht[address].times = 1;

	return true;
}

2、散列表的构造

        散列表的构造借助插入函数,创建算法是基于插入算法的,对于每个关键字,调用插入函数将给定的记录的挂件自序列依次插入到哈希表中去。       

void CreateHashTable(HashTable *ht, Datatype *L, int L_length) {
	int i;

	for(i = 0; i < HashTableSize; i++) {
		ht[i].data.key = ADDRESSTEMP;
		ht[i].times = 0;    //说明每个哈希槽都是没有放入元素的
	}

	for(i = 0; i < L_length; i++) {
		HashInsert(ht, L[i]);
	}
}

3、散列表的查找

            根据待查记录的关键字,通过构造哈希表时的哈希函数计算散列地址。

            若计算出来的地址对应的哈希槽位空,说明此项不存在在散列表中,查找失败。

            若不为空,则比较该哈希槽的关键字与待查关键字是否相同,相同的话,就是该关键字,不相同的话就说明此项此哈希值的槽有发生过哈希碰撞,借助构造散列表时解决冲突的函数查找下一个哈希槽。直到某个哈希槽为空,查找失败;或者待查记录的关键字比较并相同,查找成功。

int HashSearch(HashTable *ht, Datatype x) {
	int address;

	address = HashFunc(x.key);
	while(ht[address].data.key != ADDRESSTEMP && ht[address].data.key != x.key) {
		address = Collision(address);  //解决冲突
	}
	if(ht[address].data.key == x.key) {
		return address;
	}else {
		return NOT_FOUND;
	}
}

4、散列表的删除

        基于开放定址的散列表并不能做到真正内存上的删除,只能在要删除的哈希槽做一个删除标志,这个槽已经没有数据存放了,下一次插入新数据的时候,可以往这这个槽放。开放定址不像链地址法,链地址法解决冲突是通过链表完成,在链表中可以做到真正内存上的删除一个数据 free,开放地址的散列表本质上是数组,并不能像链表那样轻松的移除一个节点。如果必须对哈希表做真正的删除操作,最好采用链地址法处理冲突的哈希表。

int HashTableDel(HashTable *ht, Datatype x) {
	int address;

//删除的时候还是要根据传递进来x找到对应的hash键值
//通过第一次取余计算哈希地址有可能不是真正的哈希地址,发生碰撞的情况
//就需要解决哈希碰撞问题
//先找到要删除x的对应哈希地址
	address = HashSearch(ht, x);
	if(address != NOT_FOUND) {
		ht[address].data.key = ADDRESSTEMP;
		return true;
	}else {
		return false;	//说明要删除的x不存在哈希表中
	}
}

        在构造一个散列表的时候,产生冲突的问题是无法避免的,因此,ASL=0还是理想中的情况,基于散列的方法仍然需要多次与关键字比较。影响关键字比较次数的因素有:哈希构造函数、处理冲突的方法、填充因子。

        过段时间用链地址法实现一下散列表,可以尝试用AVL树代替掉链表,提高查询速度,未完待续。      

2018.5.25.17:40

链地址法比起开放定址法,更加方便了,数组和链表的结合,插入的时候判断是否发生了哈希碰撞,碰撞了就就应该放进这个哈希槽里面的链表里,是按照头插法放入,放之前要先判断这个关键字是否存在,不允许重复存在。

还需要注意的一点是,在哈希表删除一个关键字的时候,有两种可能性,删除的是哈希槽里面的元素,还是删除的是哈希槽里面链表的元素,如果删除链表的元素好办,如果删除的是哈希槽里面的元素,链表不为空的话,把链表的第一个元素放进来。

#include<stdio.h>
#include<malloc.h>

#define HASHTABLESIZE	20
#define true			1
#define false			0

typedef unsigned char boolean;

typedef int DataType;

typedef struct LinkList {
	DataType data;
	struct LinkList *next;
}LinkList;

typedef struct HASHTABLE {
	boolean addressTemp;
	DataType value;
	LinkList *head;				//用来解决发生哈希碰撞了的元素
	int collisionCount;			//记录每个哈希槽发生哈希碰撞元素的个数 也就是链表的长度
}HASHTABLE;

void createHashTable(DataType*, int, HASHTABLE*);

boolean putDataToHashTable(DataType, HASHTABLE*);

//取余构造哈希函数 
int Hash(DataType);

boolean dealCollision(int, DataType, HASHTABLE *);

void destoryHashTable(HASHTABLE **);

boolean removeKeyWord(HASHTABLE *, DataType);

boolean containsKeyword(HASHTABLE *, DataType);

void showHashTable(HASHTABLE *);

//哈希表里面是否存在此关键字targetData
boolean containsKeyword(HASHTABLE *hashTable, DataType targetData) {
	int address = Hash(targetData);

	if(hashTable[address].addressTemp == true) {
//不存在此关键词		
		return false;
	}

	if(hashTable[address].value == targetData) {

		return true;
	}else {
		LinkList *p = hashTable[address].head->next;
		while(p != NULL && p->data != targetData) {
			p = p->next;
		}

		return p == NULL ? false : true;
	}
}

//由于使用链地址法解决的哈希碰撞 
//从哈希表中删除一个关键字项的时候 如果直接将该哈希槽处的值置为空
//那么在打印的时候 会带来一系列的问题 需要考虑链表是否还有元素
//有元素的话 就把链表第一个元素放进来 删除一个链表结点
boolean removeKeyWord(HASHTABLE *hashTable, DataType targetData) {
	int address = Hash(targetData);

	if(hashTable[address].addressTemp == true) {
		printf("keyword %d is not exist hashTable\n", targetData);
		return false;
	}

	if(hashTable[address].head->next == NULL) {
		hashTable[address].addressTemp = true;
	}else {
		LinkList *temp = hashTable[address].head->next;
		hashTable[address].value = temp->data;
		hashTable[address].head->next = temp->next;
		free(temp);
	}

	return true;

}


void showHashTable(HASHTABLE *hashTable) {
	int i;
	LinkList *p;

	for(i = 0; i < HASHTABLESIZE; i++) {
		if(hashTable[i].addressTemp == false) {
			printf("hash= %d: %d ",i, hashTable[i].value);
			if(hashTable[i].head != NULL) {
				p = hashTable[i].head->next;
				while(p != NULL) {
					printf("%d ", p->data);
					p = p->next;
				}
				printf("\n");
			}
		}	
	}
	printf("\n");

}


void destoryHashTable(HASHTABLE **hashTable) {
	int i;
	LinkList *temp;
	HASHTABLE *list = *hashTable;

	for(i = 0; i < HASHTABLESIZE; i++) {
		while(list[i].head != NULL) {
			temp = list[i].head;
			list[i].head = list[i].head->next;
			free(temp);
		}
		free(list[i].head);
		list[i].head = NULL;
	}

}

boolean dealCollision(int hashIndex, DataType data, HASHTABLE* hashTable) {
	// 以头插法的放入放入到这个哈希槽中的链表去 在插入数据应该先判断是否存在这个元素
	LinkList *p = hashTable[hashIndex].head->next;
	LinkList *node;

	while(p != NULL && p->data != data) {
		if(data == p->data) {
			return false;
		}
	}
	node = (LinkList *)calloc(sizeof(LinkList), 1);
	node->data = data;
	node->next = hashTable[hashIndex].head->next;
	hashTable[hashIndex].head->next = node;
	hashTable[hashIndex].collisionCount++;

	return true;
}

int Hash(DataType data) {
	return data % HASHTABLESIZE;
}


boolean putDataToHashTable(DataType data, HASHTABLE* hashTable) {
	int hashIndex = Hash(data);

	if(hashTable[hashIndex].addressTemp == true) {
		hashTable[hashIndex].value = data;
		hashTable[hashIndex].addressTemp = false;

		return true;
	}else {
//说明产生了哈希碰撞 data不同但是hashIndex相同 映射到同一个哈希槽里面 直 
		if(hashTable[hashIndex].value == data) {
// 关键字相同
			return false;
		}else {
			boolean insertResult = dealCollision(hashIndex, data, hashTable);
			if(insertResult == false) {
				printf("warning : keyword %d had existed hashTable\n", data);
			}

			return insertResult;
		}
	}
}


void createHashTable(DataType *array, int array_length, HASHTABLE *hashTable) {
	int i;

	for(i = 0; i < HASHTABLESIZE; i++) {
		hashTable[i].addressTemp = true;
		hashTable[i].collisionCount = 0;
		hashTable[i].head = (LinkList *)calloc(sizeof(LinkList), 1);
		hashTable[i].head->next = NULL;
	}

	for(i = 0; i < array_length; i++) {
		insertDataToHashTable(array[i], hashTable);
	}
}

int main(void) {
	DataType array[] = {2,3,4,65,23,54,12,24,25};
	int array_length = sizeof(array) / sizeof(DataType);
	HASHTABLE *hashTable = NULL;

	hashTable = (HASHTABLE *)calloc(sizeof(HASHTABLE), HASHTABLESIZE);
	createHashTable(array, array_length, hashTable);
	showHashTable(hashTable);
	removeKeyWord(hashTable, 65);
	showHashTable(hashTable);
	printf("isContains = [%c]\n", containsKeyword(hashTable, 23));
	destoryHashTable(&hashTable);

	return 0;
}

/*
实现一种自动扩容的哈希表 给定初始容量最大容量为 1 <<31
问题的难点在于扩容时候的搬运元素
KWENHASHTABLE *hashHead;
hashHead = (KWENHASHTABLE *)malloc(sizeof(KWENHASHTABLE));
hashHead->hashTable = (HASHTABLE *)calloc(sizeof(HASHTABLE), hashMaxLoad);
hashHead->hashSize = 0;
hashHead->hasMaxLoad = 16;
hashHead->LoadFactor = 0.75;
需要扩容的时候先申请一个更大的空间
temp = hashHead->hashMaxLoad;
hashHead->hashMaxLoad < 1<<31 && hashHead->hashMaxLoad <<= 1 ;
HASHTABLE *array = (HASHTABLE *)calloc(sizeof(HASHTABLE), hashHead->hashMaxLoad);
for(i = 0; i < temp; i++) {
	if(hashHead->hashTable[i].addressTemp == false) {
		array[i] = hashHead->hashTable[i];
	}
}
free(hashHead->hashTable);
hashHead->hashTable = array;
*/

猜你喜欢

转载自blog.csdn.net/yvken_zh/article/details/80169236
今日推荐