查找算法总结(二)散列表

时间复杂度上,红黑树在平均情况下插入,查找以及删除上都达到了lgN的时间复杂度。

那么有没有查找效率更高的数据结构呢,答案就是本文接下来要介绍了散列表,也叫哈希表(Hash Table)

什么是哈希表

哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。

哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。

使用哈希查找有两个步骤:

  1. 使用哈希函数将被查找的键转换为数组的索引。在理想的情况下,不同的键会被转换为不同的索引值,但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况。所以哈希查找的第二个步骤就是处理冲突
  2. 处理哈希碰撞冲突。有很多处理哈希碰撞冲突的方法,本文后面会介绍拉链法和线性探测法。

哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。

哈希函数

哈希查找第一步就是使用哈希函数将键映射成索引。这种映射函数就是哈希函数。如果我们有一个保存0-M数组,那么我们就需要一个能够将任意键转换为该数组范围内的索引(0~M-1)的哈希函数。哈希函数需要易于计算并且能够均匀分布所有键。比如举个简单的例子,使用手机号码后三位就比前三位作为key更好,因为前三位手机号码的重复率很高。再比如使用身份证号码出生年月位数要比使用前几位数要更好。

在实际中,我们的键并不都是数字,有可能是字符串,还有可能是几个值的组合等,所以我们需要实现自己的哈希函数。

1. 正整数

获取正整数哈希值最常用的方法是使用除留余数法。即对于大小为素数M的数组,对于任意正整数k,计算k除以M的余数。M一般取素数。

2. 字符串

将字符串作为键的时候,我们也可以将他作为一个大的整数,采用再采用保留除余法。我们可以将组成字符串的每一个字符取值然后进行哈希,比如

public int GetHashCode(string str)
{
    char[] s = str.ToCharArray();
    int hash = 0;
    for (int i = 0; i < s.Length; i++)
    {
        hash = s[i] + (31 * hash);
    }
    return hash;
}java中String的默认实现就是类似于此。

上面的哈希值是Horner计算字符串哈希值的方法,公式为:

   h = s[0] · 31L–1 + … + s[L – 3] · 312 + s[L – 2] · 311 + s[L – 1] · 310

举个例子,比如要获取”call”的哈希值,字符串c对应的unicode为99,a对应的unicode为97,L对应的unicode为108,所以字符串”call”的哈希值为 3045982 = 99·313 + 97·312 + 108·311 + 108·31= 108 + 31· (108 + 31 · (97 + 31 · (99)))

如果对每个字符去哈希值可能会比较耗时,所以可以通过间隔取N个字符来获取哈西值来节省时间,比如,可以 获取每8-9个字符来获取哈希值:

public int GetHashCode(string str)
{
    char[] s = str.ToCharArray();
    int hash = 0;
    int skip = Math.Max(1, s.Length / 8);
    for (int i = 0; i < s.Length; i+=skip)
    {
        hash = s[i] + (31 * hash);
    }
    return hash;
}

但是,对于某些情况,不同的字符串会产生相同的哈希值,这就是前面说到的哈希冲突(Hash Collisions),比如下面的四个字符串:

如果我们按照每8个字符取哈希的话,就会得到一样的哈希值。所以下面来讲解如何解决哈希碰撞:

避免哈希冲突

拉链法

通过哈希函数,我们可以将键转换为数组的索引(0-M-1),但是对于两个或者多个键具有相同索引值的情况,我们需要有一种方法来处理这种冲突。

一种比较直接的办法就是,将大小为M 的数组的每一个元素指向一个条链表,链表中的每一个节点都存储散列值为该索引的键值对,这就是拉链法。

基于拉链法的散列表的实现简单,在键的顺序并不重要的应用中,它可能是最块的(也是使用最广泛的)符号表实现。

线性探测法

线性探测法是开放寻址法解决哈希冲突的一种方法,基本原理为,使用大小为M的数组来保存N个键值对,其中M>N,我们需要使用数组中的空位解决碰撞冲突。

开放寻址法中最简单的是线性探测法:当碰撞发生时即一个键的散列值被另外一个键占用时,直接检查散列表中的下一个位置即将索引值加1,这样的线性探测会出现三种结果:

  1. 命中,该位置的键和被查找的键相同
  2. 未命中,键为空
  3. 继续查找,该位置和键被查找的键不同。

空元素(记为null)可以作为查找结束的标志。

和拉链法一样,开放地址类的三列表的性能也依赖于a=N/M的比值,我们称为散列表的使用率。

对于拉链法,a是每条链的长度,因此一般大于1;对于基于先行探测的散列表,a是表中已被占用的空间比例,它是不可能大于1的,

为保证性能。我们动态调整数组的大小来保证使用率在1/8和1/2之间。

时间: 2024-10-29 19:11:46

查找算法总结(二)散列表的相关文章

查找算法总结(二分查找/二叉查找树/红黑树/散列表)

1.二分查找 二分查找时,先将被查找的键和子数组的中间键比较.如果被查找的键小于中间键,就在左子数组继续查找,如果大于中间键,就在右子数组中查找,否则中间键就是要找的元素. /** * 二分查找 */ public class BinarySearch { public static int find(int[] array, int key) { int left = 0; int right = array.length - 1; // while (left <= right)不能改为<

js数据结构与算法——字典与散列表

<script> //创建字典 function Dictionary(){ var items = {}; this.set = function(key,value){ //向字典添加一个新的项 items[key] = value; } this.remove = function(key){ //从字典移除一个值 if(this.has(key)){ delete items[key]; return true; } return false; } this.has = functio

Knowledge_SPA——精研查找算法

首先保证这一篇分析查找算法的文章,气质与大部分搜索引擎搜索到的文章不同,主要体现在代码上面,会更加高级,会结合到很多之前研究过的内容,例如设计模式,泛型等.这也与我的上一篇面向程序员编程--精研排序算法不尽相同. 关键字:二分查找树,红黑树,散列表,哈希,索引,泛型,API设计,日志设计,测试设计,重构 查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算. 当今世纪,IT界最重要的词就是"数据!数据!数据!",高效检索这些信息的能力是处理他们的重要前提.数

数据结构(十二)散列表

定义 以下简称hahs 应用场景 适合查找与给定值相同的数据,不适合做范围查找,1对多映射查找 问题 冲突,散列表的理论依据是每个不同的关键字通过散列算法得到的结果都是唯一的,而现实中有可能出现几个结果相同的关键字. hash算法 构造一个散列算法考虑几个方面 直接定址法 按如下公式计算出关键字的hash值,当原始的key不重复,则得到的hash值就不会冲突 数字分析法 抽取关键字的一部分作为hash值 例如手机号,一般可以取后4位或者后4位的变形作为hash值,(公司内部场景) 平方取中法 折

算法导论第十一章 散列表

一.散列表的概念 本章介绍了散列表(or hash table)的概念.散列函数的设计及哈希冲突的处理.散列表(为了形象描述,我们通常叫槽)从表意上看是一种数据结构,但把它归为算法思想更为贴切.对于大部分的查找问题,使用散列表能达到O(1)的效率.现在很多大公司在面试大数据的题目时,解决方案里绝对少不了散列表的思想,例如百度的一道面试题:Top K查找问题: 问题描述: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节. 假设目前有一千万个记录(这

大话数据结构—散列表查找(哈希表)

一.基本概念 散列技术:在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key). f:散列函数/哈希函数: 采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表. 关键字对应的记录存储位置称为散列地址. 散列技术既是一种存储方法,也是一种查找方法. 散列技术适合求解问题是查找与给定值相等的记录.查找速度快. 散列技术不适合范围查找,不适合查找同样关键字的记录,不适合获取记录的排序,最值. 冲突:关键字key1不等于k

数据结构之散列表总结

散列表的概念 注意:    ①由同一个散列函数.不同的解决冲突方法构造的散列表,其平均查找长度是不相同的.     ②散列表的平均查找长度不是结点个数n的函数,而是装填因子α(填入表中的记录个数/散列表的槽数    n/m).因此在设计散列表时可选择α以控制散列表的平均查找长度.(平均查找长度=总查找(插入)/记录个数)          通过链接法解决冲突:成功查找的期望查找长度O(1+a), 不成功查找的平均查找长度也为O(1+a).         开放寻址解决冲突:引入探查序列,对于a<

小橙书阅读指南(十一)——散列表

算法描述:散列表是一种在时间和空间上做出权衡的查找算法.使用查找算法分为两步.第一步是通过散列函数将被查找的键转化未数组的一个索引.理想情况下,不同的键都能转为不同的索引值.当然,这只是理想情况,所以我们需要面对两个或多个键都被散列到相同索引值的情况.因此,散列查找的第二部就是处理碰撞冲突的过程. 一个比较令人满意的散列函数能够均匀并独立地将所有键散布于0到M-1之间. 一.基于拉链法的散列表 算法图示: 拉链散列表算法的本质是将哈希值相同的键保存在一个普通链表中,当我们需要调整数组长度的时候,

数据结构--散列表--散列表的性能分析

散列表的性能分析 平均查找长度(ASL)用来度量散列表查找效率:成功.不成功. 成功:查找的元素在散列表里面 不成功:查找的元素不在散列表里面 主要受三个因素的影响: 散列函数是否均匀 处理冲突的方法 散列表的装填因子 分析: 不同冲突处理方法.装填因子对效率的影响. 上面的只是反应了一般情况下的理论值. 上面的也是反应了一般情况下的理论值. 散列的特点: 散列查找 和问题规模没关系. 适合字符串的管理 散列表,装填因子小的,所用的时间少,因此,散列方法是一个以空间换时间的一种方法. 散列方法的