数据结构 散列表
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;
本回答被提问者采纳
数据结构与算法—散列表
目录
散列表
Word 单词拼写功能,如何实现的?散列表(Hash Table)
散列表用的是数组支持按照下标随机访问数据的特性。散列表其实是数组的一种扩展。
用空间换时间
散列表就是用数组支持按照下标随机访问的时候,时间复杂度O(1)的特性。
通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应的下标位置。当查询键值元素时,用散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。
散列函数
Hash(key) key表示元素的键值,hash(key)的值表示经过散列函数计算得到的散列值
散列函数设计的要求
- 散列函数计算得到的散列值是一个非负整数
- 如果key1=key2,hash(key1) = hash(key2)
- 如果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;
以上是关于数据结构 散列表的主要内容,如果未能解决你的问题,请参考以下文章