本文主要是介绍数据结构与算法之美-散列表-极客时间笔记,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
散列表或者哈希表,就是数组的一种延伸应用,主要还是利用数组可以方便的理由下标索引进行元素操作。概念的核心点在于原始数据可能是键值对,将Key值通过哈希函数进行转化变成唯一(希望是唯一,但并无法保证,所以才有后面的散列冲突问题)的下标索引,以此来保证Value值。
散列函数的设计有下列三点要求:
1、散列函数计算得到的散列值是一个非负整数;——因为散列的结果对应数组下标,必要要求非负整数。
2、如果 key1 = key2,那 hash(key1) == hash(key2);——相同Key对应的散列值要一致,涉及第一次存储和后续读取
3、如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。——最好的结果是不同的Key对应不同的散列值,这样才能区分
但第三点是没办法实现的,因为无论如何数组大小是有限的,而可能的Key是无限的,这样用有限去对应无限,必然是会出现重复,这就是所谓的散列冲突问题。
处理散列冲突常用的有两种方法:开发寻址法和链表法。
1、开放寻址法是当遇到散列值被使用的情况下就继续做数组中顺序寻找空闲位置,这样会有个缺点,就是如果散列冲突很多的时候,这种顺序去查找的时间就会变得特别长。而且还有个问题就是,如果不是哈希插入,而是哈希查找。一般来说是发现空闲位置前还没有发现目标元素则认为元素不在散列表。但是如果数组之前有元素被删除,相应位置出现空缺,就会错误的认为元素不在散列表。虽然可以用删除标记来表示,但这种方法还是不够方便,实际应用不多。
2、链表法,如果出现散列值相同的情况,就在相应位置建立1条链表,只要散列的分布足够均匀,不让大多数散列值相同,就不会让散列表退化。不然很多元素在一个散列值组成链表就容易变成单链表。
为了尽可能保证散列表的操作效率,一般情况下,我们会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子(load factor)来表示空位的多少。装载因子是目前已装入元素个数除以数组长度。装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
一个能应对各种异常情况的工业级散列表也应该从上述三方面出发:
1、散列函数的设计,不能过于复杂,因为散列值会需要经常计算,复杂计算势必会影响到性能;另外散列函数的结果要尽可能随机且平均分布,减少散列冲突的可能。
2、装载因子可以作为散列表是否需要扩容的因素指标,当到底某个阈值后启动动态扩容,将旧散列表的数据迁移至新散列表。虽然这种操作经过均摊还是O(1),但是对于那一次来说,用户体验还是糟糕。可以将摊还真实的进行分割,旧数据迁移不是一次进行,而是伴随新数据存入同步进行,这样就对于每个用户来说时间都不会太明显。
3、根据使用场景选择合适的散列冲突解决方法。当数据量比较小、装载因子小的时候,适合采用开放寻址法。利用数组的优点,但更加消耗内容,因为散列冲突的影响更加严重。基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。
散列表这种数据结构虽然支持非常高效的数据插入、删除、查找操作,但是散列表中的数据都是通过散列函数打乱之后无规律存储的。也就说,它无法支持按照某种顺序快速地遍历数据。如果希望按照顺序遍历散列表中的数据,那我们需要将散列表中的数据拷贝到数组中,然后排序,再遍历。那效率势必会很低。为了解决这个问题,我们将散列表和链表(或者跳表)结合在一起使用。可以看下图这个缓存系统的数据结构:
上图存在两种链表,一种是表示缓存更新时间的双向链表,另一种是散列表中用于解决散列冲突的链表。可以在O(1)时间里实现查找、删除、增加。散列表用于查找元素,删除和增加利用双向链表的指针也可以快速实现。
这篇关于数据结构与算法之美-散列表-极客时间笔记的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!