数据结构 散列表

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构 散列表相关的知识,希望对你有一定的参考价值。

求个C程序
试更具全班学生的姓名,构造一个散列表,选择适当的散列函数和解决冲突的方法,设计并实现插入、删除和查找算法,并统计冲击发生的次数(用拉链法解决冲击时装填因子a=2,用开放定址法时a=1/2)
在线等哦

散列表是一种数据结构,通过散列函数(也就是 hash 函数)将输入映射到一个数字,一般用映射出的数字作为存储位置的索引。数组在查找时效率很高,但是插入和删除却很低。而链表刚好反过来。设计合理的散列函数可以集成链表和数组的优点,在查找、插入、删除时实现 O(1) 的效率。散列表的存储结构使用的也是数组加链表。执行效率对比可以看下图 1.3:



散列表的主要特点:1. 将输入映射到数字2. 不同的输入产生不同的输出3. 相同的输入产生相同的输出4. 当填装因子超过阈值时,能自动扩展。填装因子 = 散列表包含的元素数 / 位置总数,当填装因子 =1,即散列表满的时候,就需要调整散列表的长度,自动扩展的方式是:申请一块旧存储容量 X 扩容系数的新内存地址,然后把原内存地址的值通过其中的 key 再次使用 hash 函数计算存储位置,拷贝到新申请的地址。5. 值呈均匀分布。这里的均匀指水平方向的,即数组维度的。如果多个值被映射到同一个位置,就产生了冲突,需要用链表来存储多个冲突的键值。极端情况是极限冲突,这与一开始就将所有元素存储到一个链表中一样。这时候查找性能将变为最差的 O(n),如果水平方向填充因子很小,但某些节点下的链表又很长,那值的均匀性就比较差。

参考技术A // hash.cpp :chain-hash
//

#include <iostream>
#include <math.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#include <string>
#include <sstream>
#define slot_size 10 //散列槽的大小
#define arr_size 20 //动态关键字集合

using namespace std;
struct node

string key;
node* next;
;
string* arr_set;
node link_hash[slot_size];
long str2num(string s)

long num=0;
const char *temp=s.c_str();
int i=0;
while(temp[i]!='\0')

num=num+int(temp[i]);
i++;


return num;

/*
* 散列函数,为方便起见,实验是使用了除法散列函数
*/
long hash_function(string key)


return str2num(key)%slot_size;

/*
* 打印数组函数
*/
void print_arr(long* set,long size)

for(long i=0;i<size;i++)

cout<<set[i]<<endl;


/*
* 链接法插入操作,在散列表指向的链表头前添加一个元素(节点)
*/
void hash_insert(string k)

long temp_index=hash_function(k);
node *new_node=new node;
new_node->key=k;
new_node->next=link_hash[temp_index].next;
link_hash[temp_index].next=new_node;

/*
* 查找函数,计算散列值后,沿着链表一起搜索,知道找到该关键字
* 为止,如果找不到,则返回false
*/
bool hash_find(string k)

long temp_index=hash_function(k);
node *temp_node =new node;
if(link_hash[temp_index].next==NULL)

return false;

else

temp_node=link_hash[temp_index].next;
while(temp_node->key!=k)

if(temp_node->next!=NULL)

temp_node=temp_node->next;

else

return false;



return true;


/*
* 删除函数,如果没有找到就返回false ,如果找到了该关键字,就将前一节点的next指针指向该关键字的
* next指针所指的节点
*/
bool hash_delete(string k)

long temp_index=hash_function(k);
if(link_hash[temp_index].next==NULL)

return false;

else

node *temp_node =new node;
temp_node=link_hash[temp_index].next;
node *pre_node=new node;
pre_node=&link_hash[temp_index];
while(temp_node->key!=k)

if(temp_node->next!=NULL)

pre_node=temp_node;
temp_node=temp_node->next;


else

return false;


pre_node->next=temp_node->next;

return true;


/*
* 打印散列表的函数,可以设置打印的开始索引到结束索引
*/
void print_hash(long start,long end)

node *temp;
long count=0;
for(long j=start;j<end;j++)

cout<<j<<"[--]";
if(link_hash[j].next==NULL)

cout<<endl;

else

temp=&link_hash[j];
//temp=temp->next;
while(temp->next!=NULL)

cout<<"-->"<<temp->next->key;
temp=temp->next;
count++;

cout<<endl;


cout<<endl;
return;


//主函数,程序流程如实验设计的流程图
int main(int argc, char* argv[])


//cout<<"please input the size of the key that you want to store:";
//cin>>arr_size;

string name_set[20]="terry","dos","garry","gerry","lily","poly","lernerd","vincent","biandu,adli","afand","lonsc","cecial","jimmy","cardly","supper","loser","kedny","nody","sendcy";//to generate arr_size from 1 to 1000 random number
arr_set=name_set;
//print_arr(arr_set,arr_size);

//初始化散列表的槽

for(long n=0;n<slot_size;n++)

link_hash[n].next=NULL;
link_hash[n].key="";


//插入操作
DWORD insert_start = GetTickCount();
for(long m=0; m<arr_size;m++)

hash_insert(arr_set[m]);

DWORD insert_time=GetTickCount() - insert_start;
cout<<"the size of n is: "<<arr_size<<endl;
cout<<"the size of m is: "<<slot_size<<endl;
cout<<"the value of a=n/m is: "<<float(arr_size)/float(slot_size)<<endl;
cout<<"the total insert running time is: "<<insert_time<<" milliseconds"<<endl;
cout<<"the average insert time is: "<<float(insert_time)/float(arr_size)<<" milliseconds"<<endl<<endl;
cout<<"***********************************************************"<<endl<<endl;
cout<<"after the insertion:"<<endl<<endl;
print_hash(0,10);

//查找操作
DWORD find_start=GetTickCount();
for(long n=0;n<arr_size;n++)

hash_find(arr_set[n]);


DWORD find_time=GetTickCount()-find_start;
cout<<"the total finding running time is: "<<find_time<<" milliseconds"<<endl;
cout<<"the average finding runnig time is: " <<float(find_time)/float(arr_size)<<" milliseconds"<<endl;

int a;
cin>>a;

return 0;
本回答被提问者采纳

数据结构与算法—散列表

目录

散列表

散列函数

散列冲突解决

1、开放寻址法

1.1 线性探测

1.2 二次探测

1.3 双重散列

2、链表法

使用场景

单词查找

散列表与链表的结合使用LRU

散列表总结

散列表实例


散列表

Word 单词拼写功能,如何实现的?散列表(Hash Table)

散列表用的是数组支持按照下标随机访问数据的特性。散列表其实是数组的一种扩展。

用空间换时间

散列表就是用数组支持按照下标随机访问的时候,时间复杂度O(1)的特性

通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应的下标位置。当查询键值元素时,用散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。

散列函数

Hash(key)  key表示元素的键值,hash(key)的值表示经过散列函数计算得到的散列值

散列函数设计的要求

  1. 散列函数计算得到的散列值是一个非负整数
  2. 如果key1=key2,hash(key1) = hash(key2)
  3. 如果key1不等于key2,那hash(key1)不等于hash(key2) 

散列冲突无法避免,如著名的MD5,SHA,CRC.而且数组的存储空间有限,也会增大散列冲突的概率。

1、设计散列函数

要点1,散列函数的设计不能太复杂;2、散列函数生成的值要尽可能随机并且均匀分布。

方法:直接寻址法,平方取中法,折叠法,随机数法。

Hash("nice") = (("n" - "a") * 26 * 26 *26 + ("i" - "a")*26 * 26 + ("c" - "a")*26 + ("e"-"a"))

散列冲突解决

1、开放寻址法

如果出现散列冲突,重新探测一个空闲位置,将其插入。

探测方法:

1.1 线性探测

插入:线性探测,存储位置被占用,就从当前位置开始,依次往后查找(尾部结束,接着从头找),直到找到空闲位置为止。

查找:通过散列函数求出要查找元素的键值对应的散列值,然后从数组中下标为散列值的元素和查找的元素。如果相等,找到。如果没有,顺序往后依次查找。如果变量数组中空位置,还没有找到,则说明查找元素并没有在散列列表中。

删除:特殊标记deleted。当线性探测查找的时候,遇到标记为deleted空间。并不是停下来,而且继续探测。

缺点,当插入的数据越来越多,散列冲突的可能性越来越大,空闲位置越来越少,探测时间越久。

1.2 二次探测

与线性探测很像,线性探测每次探测的步长是1,hash(key)+0,hash(key)+1, hash(key)+2

二次探测每次探测的步长变成原来的二次方 hash(key)+0 hash(key)+1^2 hash(key)+2^2

1.3 双重散列

不仅仅要使用一个散列函数,使用一组散列函数hash1(key),hash2(key) ,hash3(key)。函数依次使用,直到找到空闲位置。

不管那种探测方法,当散列表中空闲位置不多的时候,散列冲突的概率会大大提高。一般情况下,尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子来表示空位的多少。

        散列表的装载因子 填入表中元素个数 / 散列表的长度

装载因子越大,数目空闲位置越少,冲突越多,散列表的性能会下降。

装载因子过大解决方法

动态扩容,降低装载因子

装载因子的阈值设置要权衡时间、空间复杂度。如果内存空间多,对执行效率要求高,可以降低负载因子的阈值;反之,增加负载因子。

动态扩容,可能需要多次。可以将扩容操作穿插在插入操作过程中,分批完成。当装载因子大于阈值后,值申请空间,并不将老的数据搬移到新散列表中。

动态扩容期间的插入操作

当有新数据要插入时,将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入新散列表。每次插入一个数据到散列表。重复上面的过程,老的数据搬移到新散列表中。插入操作的时间不受影响。时间复杂度O(1)

  • 优点:散列表中的数据都存储在数组中,可以有效利用CPU缓存,加快查询速度。
  • 缺点:删除数据比较麻烦,需要标记已删除的数据

当数据量比较小,装载因子小 

2、链表法

在散列表中,每个会对应一条链表。所有散列值相同的元素我们都放到相同桶位对应的链表中。

当插入时,只需要通过散列函数计算出对应的散列桶位。

将其插入到对应链表中。时间复杂度O(1)

查找,删除一个元素。计算对应的桶,然后遍历链表查找或删除。复杂度与链表长度k成正比O(k)

  • 优点:对内存利用率比较好,需要时申请;对于大装载因子的容忍度更高,只要散列函数的随机值均匀,即使装载因子10,也就是链表的长度变长,查找效率下降,但比顺序表查找快很多。
  • 链表的升华,更加高效的散列表;
  • 缺点:链表因为要存储指针,对于较小的对象,比较耗内存。链表节点零散分布,不连续,对CPU缓存不友好。
  • 使用跳表、红黑树取代链表。即使冲突,在极端情况下,查找时间O(logn)

 

使用场景

1、单词查找

常用英文单词20万左右。假设单词平均长度是10个字母,占用10字节空间。

  •  20万英文单词,大约占用2MB空间。这个大小完全可以放在内存里。
  • 当用户输入某个英文单词时,拿用户输入的单词去散列表中查找。如果查到,则说明拼写正确。

2、10万条URL访问日志

如何按照访问次数给URL排序?

  • 遍历10万条数据,以URL为key,访问次数为value,存入散列表中。同时记录最大访问的次数K。时间复杂度O(n)
  • 如果K不是很大,可以使用桶排序,时间复杂度O(n),如果K很大,(如大于10万),就使用快速排序 O(nlogn) 

3、两个字符串数组比较

有两个字符串数组,每个数组有10万条字符串,如何快速找出两个数组中相同的字符串。

  •  以第一个字符串构建散列表,key为字符串,value表示出现次数。
  • 变量第二个字符串数组,以字符串key在散列表中查找,如果value大于0,则说明存在相同字符串。时间复杂度O(n)

4、散列表与链表的结合使用LRU

借助散列表,可以把LRU缓存淘汰算法的时间复杂度降低为O(1)

  • 当需要缓存数据时,先在链表中查找这个数据,没有找到,直接将数据放到链表尾部;如果找到了,就把它移动到链表的尾部。所以单纯的使用链表实现LRU缓存淘汰算法的时间复杂度很高O(n)
  • 将散列表与链表一起使用,时间复杂度O(1) 

散列表总结

工业级散列表的特性

1,支持快速插入语、删除查找

2、内存占用,不能浪费过多内存;

3、稳定性,极端情况下的退化

实现散列表

1、设计合适的散列函数

2、定义装载因子阈值,并支持动态扩容

3、选择合适的散列冲突解决方法

散列表实例



#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>

#define HASH_SHIFT 4
#define HASH_SIZE (1<<HASH_SHIFT)
#define HASH_MASK (HASH_SIZE - 1)


struct hash_table
	unsigned int used;
	unsigned long entry[HASH_SIZE];
;


void hash_table_reset(struct hash_table *table)

	int i;
	table->used = 0;
	for(i = 0;i<HASH_SIZE;i++)
		table->entry[i] = ~0;


unsigned int hash_function(unsigned long value)

	return value & HASH_MASK;



void dump_hash_table(struct hash_table *table)

	int i;
	for(i = 0;i<HASH_SIZE;i++)
	
		if(table->entry[i] == ~0)
			printf("%2u: ",i);
		else
			printf("%2u: %2u\\n",i,table->entry[i],hash_function(table->entry[i]));
	


void hash_function_test()

	int i;
	srandom(time(NULL));

	for(i = 0;i<10;i++)
	
		unsigned long val = random();
		printf("%10u->%2u",val,hash_function(val));
	


unsigned int next_probe(unsigned int pre_key)

	return(pre_key + 1) & HASH_MASK;


void next_probe_test()

	int i;
	unsigned int key1,key2;

	key1 = 0;
	for(i = 0;i<HASH_SIZE;i++)
	
		key2 = next_probe(key1);
		printf("%2u -> %2u\\n",key1,key2);
		key1 = key2;
	


void hash_table_add(struct hash_table *table,unsigned long value)

	unsigned int key = hash_function(value);

	if(table->used >= HASH_SIZE)
		return;

	while(table->entry[key] != ~0)
		key = next_probe(key);

	table->entry[key] = value;
	table->used++;


unsigned int hash_table_slot(struct hash_table *table,unsigned long value)

	int i;
	unsigned int key = hash_function(value);

	for(i = 0;i<HASH_SIZE;i++)
		if(table->entry[key] == value || table->entry[key] == ~0)
			break;

		key = next_probe(key);
	

	return key;


bool hash_table_find(struct hash_table *table,unsigned long value)

	return table->entry[hash_table_slot(table,value)] == value;



void hash_table_del(struct hash_table *table,unsigned long value)

	unsigned int i,k,j;
	if(!hash_table_find(table,value))
		return;

	//find
	i = j = hash_table_slot(table,value);

	while(true)
	
		table->entry[i] = ~0;
		do
			j = next_probe(j);
			if(table->entry[j] == ~0)
				return;

			k = hash_function(table->entry[j]);
		while((i<=j)?(i<k && k<=j) : (i<k || k<=j));

		table->entry[i] = table->entry[j];
		i = j;
	
	table->used++;


void hash_table_add_test()

	struct hash_table table;
	hash_table_reset(&table);

	hash_table_add(&table,1234);

	int ret;
	ret = hash_table_find(&table,1234);
	printf("ret res=%d\\n",ret);


void hash_table_del_test()

	struct hash_table table;
	int i;
	hash_table_reset(&table);
	for(i = 0;i<HASH_SIZE;i++)
		hash_table_add(&table,i);

	dump_hash_table(&table);

	hash_table_del(&table,5);

	dump_hash_table(&table);


void main()

	//hash_table_add_test();
	hash_table_del_test();
	return;

以上是关于数据结构 散列表的主要内容,如果未能解决你的问题,请参考以下文章

数据结构—— 散列查找:散列表

《数据结构与算法之美》15——散列表如何实现工业级别的散列表

数据结构和算法篇——散列表

数据结构散列表

JavaScript数据结构与算法 - 散列表

数据结构和算法: 散列表