数据结构与算法—散列表
Posted 为了维护世界和平_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法—散列表相关的知识,希望对你有一定的参考价值。
目录
散列表
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;
以上是关于数据结构与算法—散列表的主要内容,如果未能解决你的问题,请参考以下文章