哈希表的基本原理?

Posted

技术标签:

【中文标题】哈希表的基本原理?【英文标题】:The fundamentals of Hash tables? 【发布时间】:2010-09-21 21:57:19 【问题描述】:

我对哈希表的基本概念感到很困惑。如果我要编写一个哈希,我什至会如何开始?哈希表和普通数组有什么区别?

基本上,如果有人回答了这个问题,我想我的所有问题都会得到回答: 如果我有 100 个随机生成的数字(作为键),我将如何实现哈希表,为什么它比数组更有优势?

伪代码或 Java 将被视为一种学习工具...

【问题讨论】:

这仍然是一个集合。您在一组中存储 100 个数字。你在 Set 中查找它们。他们在那里。 HashMap 需要一个键和一个值作为单独的东西;否则它只是一组键。 我对哈希表查找密钥的方式以及密钥的生成方式更感兴趣。 重点是:没有键和值以及合理的用例,哈希表就没有意义。 没有值的哈希表可以很好地实现一个集合。你在哪里需要一个值?找到或未找到密钥。 正如 Zan 所说,HashSet 实际上是由内部的 hashtable/hashMap 支持的。 【参考方案1】:

到目前为止的答案有助于定义哈希表并解释一些理论,但我认为一个示例可以帮助您更好地了解它们。

哈希表和普通数组有什么区别?

哈希表和数组都是允许您存储和检索数据的结构。两者都允许您指定 index 并检索与其关联的值。正如 Daniel Spiewak 所指出的,不同之处在于数组的索引是顺序的,而哈希表的索引是基于与它们关联的数据的值

我为什么要使用哈希表?

哈希表可以提供一种非常有效的方法来搜索大量数据中的项目,尤其是在其他情况下不易搜索的数据。 (这里的“大”是指ginormous,意思是执行顺序搜索需要很长时间)。

如果我要编写一个哈希,我会怎么开始?

没问题。最简单的方法是发明一个可以对数据执行的任意数学运算,它返回一个数字N(通常是一个整数)。然后将该数字用作“桶”数组的索引,并将您的数据存储在桶#N 中。诀窍在于选择一个倾向于将值放在不同存储桶中的操作,以便您以后轻松找到它们。

示例: 一个大型购物中心保留了顾客的汽车和停车位置的数据库,以帮助购物者记住他们的停车地点。数据库存储makecolorlicense plateparking location。在离开商店时,购物者通过输入其品牌和颜色来找到他的汽车。该数据库返回一个(相对较短的)车牌和停车位列表。快速扫描即可找到购物者的汽车。

您可以使用 SQL 查询来实现:

SELECT license, location FROM cars WHERE make="$(make)" AND color="$(color)"

如果数据存储在一个数组中,它本质上只是一个列表,您可以想象通过扫描一个数组以查找所有匹配条目来实现查询。

另一方面,想象一个哈希规则:

将make和color中所有字母的ASCII码相加,除以100,余数作为hash值。

此规则会将每个项目转换为 0 到 99 之间的数字,本质上是将数据排序到 100 个桶中。每次客户需要定位汽车时,您都可以对品牌和颜色进行哈希运算,以从 100 个包含信息的存储桶中找到 一个。您立即将搜索量减少了 100 倍!

现在将示例扩展到海量数据,例如一个包含数百万个条目的数据库,根据数十个标准进行搜索。一个“好的”散列函数将以一种最小化任何额外搜索的方式将数据分配到存储桶中,从而节省大量时间。

【讨论】:

巨大的。小型数据集,比如几千个,我应该期待性能提升吗? 不是真的@akshayb。 Java 在处理小型数据集时非常高效。【参考方案2】:

首先,您必须了解什么是哈希函数。 哈希函数是一个函数,它接受一个键(例如,一个任意长度的字符串)并返回一个尽可能唯一的数字。相同的键必须始终返回相同的散列。 java中一个非常简单的字符串散列函数可能看起来像

public int stringHash(String s) 
    int h = s.length();
    for(char c : s.toCharArray()) 
        h ^= c;
    
    return h;

你可以在http://www.azillionmonkeys.com/qed/hash.html学习一个好的哈希函数

现在,哈希映射使用此哈希值将值放入数组中。简单的java方法:

public void put(String key, Object val) 
    int hash = stringHash(s) % array.length;
    if(array[hash] == null) 
        array[hash] = new LinkedList<Entry<String, Object> >();
    
    for(Entry e : array[hash]) 
        if(e.key.equals(key))
            e.value = val;
            return;
        
    
    array[hash].add(new Entry<String, Object>(key, val));

(此地图强制使用唯一键。并非所有地图都这样做。)

两个不同的键可以散列到同一个值,或者两个不同的散列映射到同一个数组索引。有很多技术可以解决这个问题。最简单的方法是为每个数组索引使用链表(或二叉树)。如果哈希函数足够好,您将永远不需要线性搜索。

现在要查找密钥:

public Object get(String key) 
    int hash = stringHash(key) % array.length;
    if(array[hash] != null) 
        for(Entry e : array[hash]) 
            if(e.key.equals(key))
                return e.value;
        
    

    return null;

【讨论】:

太棒了!我希望我能多次投票给你。这是我一直计划写的(回答我第一个答案的问题)但没有机会。 感谢您的回答。这个运算符是做什么的:^ = ?我以前从未见过它 什么是最好的数组大小或者我应该考虑什么因素来设置这个大小?【参考方案3】:

哈希表是关联的。这与数组有很大的不同,数组只是线性数据结构。使用数组,您可以执行以下操作:

int[] arr = ...
for (int i = 0; i < arr.length; i++) 
    System.out.println(arr[i] + 1);

注意如何通过指定精确的内存偏移量 (i) 从数组中取出一个元素。这与哈希表形成对比,哈希表允许您存储键/值对,然后根据键检索值:

Hashtable<String, Integer> table = new Hashtable<String, Integer>();
table.put("Daniel", 20);
table.put("Chris", 18);
table.put("Joseph", 16);

通过上表,我们可以进行如下调用:

int n = table.get("Chris");

...请放心,n 的价值将为 18

我认为这可能会回答您的大部分问题。哈希表的实现是一个相当有趣的话题,一个which Wikipedia addresses passably well。

【讨论】:

好吧,但是在实际实现中,table.get("Chris") 不是还是要遍历表才能找到Chris吗?它如何知道 Chris 处于“关键”值?当它散列时,“克里斯”到底发生了什么? 好问题。将在单独的答案中解决...如果您不耐烦,请尝试查看 Wikipedia 文章。 @me.yahoo.com:请参阅下面的评论(由于大小限制,无法在这里写) 没有。哈希表永远不会遍历。它计算“Chris”的散列,这是散列表中的物理槽,它将以“Chris”作为键。哈希是对字节值的计算(详见 MD5 算法。) 感谢您的回答 - 但哈希表与字典有何不同?它们都有键/值对。所以我对它们的区别感到困惑。【参考方案4】:

“我对哈希表查找密钥的方式以及密钥的生成方式更感兴趣。”

    散列将键对象转换为数字。这称为“散列”——它对对象进行散列。见Hash Function。例如,对字符串的字节求和是一种标准的散列技术。您计算总和模 232 以将散列保持在可管理的大小。哈希总是给出相同的答案。这是O(1)。

    该数字在哈希表中为您提供了一个“槽”。给定一个任意键对象,哈希值计算一个哈希值。然后哈希值会为您提供表中的插槽。通常mod( hash, table size )。这也是O(1)。

这是一般的解决方案。两个数值计算,你已经从任意对象作为键到任意对象作为值。很少有事情可以这么快。

从对象到哈希值的转换以这些常见方式之一发生。

    如果它是一个 4 字节的“原始”对象,那么该对象的原生值就是一个数字。

    对象的地址是4字节,那么对象的地址可以作为哈希值。

    一个简单的hash function(MD5、SHA1 等)累积对象的字节以创建一个 4 字节的数字。高级哈希不是简单的字节总和,简单的总和并不能充分反映所有原始输入位。

哈希表中的槽位是 mod(number, size of table )。

如果该插槽具有所需的值,那么您就完成了。如果这不是所需的值,则需要寻找其他地方。有几种流行的探测算法可以在表格中寻找空闲位置。线性是对下一个空闲位置的简单搜索。 Quadratic 是一种寻找空闲槽的非线性跳跃。随机数生成器(具有固定种子)可用于生成一系列探针,这些探针将均匀但任意地传播数据。

探测算法不是O(1)。如果桌子足够大,那么碰撞的几率就会很低,探测也无关紧要。如果表太小,则会发生冲突并进行探测。到那时,就需要“调整和调整”来平衡探测和表大小以优化性能。通常我们只是把桌子变大。

见Hash Table。

【讨论】:

谢谢,您的所有回答都对我有很大帮助。但每一个答案都会引出更多问题。探测如何工作?线性探测似乎很简单。直接走到桌子下面,直到有一个空位,对吗?但是二次探测呢?它是如何工作的,为什么或者更好? 二次:“探测之间的间隔与哈希值成正比增加”。为什么更好?经验数据证明它比线性更好。没有比这更多的“为什么”了。 “MD5”和“SHA1”是什么意思?【参考方案5】:

我还没有看到特别指出的东西:

在数组上使用哈希表的关键在于性能。

遍历数组通常需要从 O(1) 到 O(x) 的任何时间,其中 x 是数组中的项目数。然而,找到您的项目的时间将非常可变,尤其是当我们讨论的是数组中的数十万个项目时。

一个适当加权的哈希表通常具有几乎恒定略高于 O(1) 的访问时间,无论哈希表中有多少项。

【讨论】:

哈希总是有一个键,你可以简单地计算槽并且查找不涉及实际搜索。不同之处在于哈希可以使用任何东西作为键。数组只能使用整数。 是的,一个字符串,例如“green”总是会散列到相同的值。此外,Java 将缓存该值,因此它只运行一次哈希算法来生成它。该哈希值用于获取基本上是一个数组的“桶”。然后它迭代地扫描它。理想情况下,每个存储桶有 1 个项目。 @rally25rs [需要引用] :-) 查看大多数类中 hashCode() 的源代码。它通常是动态计算的,而不是记忆的(以避免线程问题)。 Object#hashCode() 实现也没有被缓存,但由于内存模型的工作方式,它是一个常量。 Java 可以缓存字符串值来简化一些事情。它与散列无关。 Java 恰好是这样。 这就是我想要的。猜测它的特定字符串(来自 Java 文档):“从 JDK 版本 1.3 开始,类 java.lang.String 缓存其哈希码,即它只计算一次哈希码并将其存储在实例变量中,并在 hashCode 时返回此值方法被调用。【参考方案6】:

您不会希望对 100 个随机生成的数字使用哈希表。

考虑哈希表的一个好方法是考虑值对。让我们以学生为例,假设每个人都有一个学生证号码。在您的程序中,您存储有关学生的信息(姓名、电话号码、账单等)。您希望仅使用基本信息(例如姓名或学生 ID)来查找有关学生的所有信息。

假设您有 10,000 名学生。如果您将它们全部存储在一个数组中,那么您必须遍历整个数组,将每个条目的学生 ID 与您要查找的学生 ID 进行比较。

如果相反,您将他们的学生 ID 号“散列”(见下文)到数组中的某个位置,那么您只需搜索学生的谁的号码具有相同的散列。找到你想要的东西的工作要少得多。

在此示例中,假设学生 ID 只是 6 位数字。我们的散列函数只能使用数字的后 3 位作为“散列键”。因此 232145 被散列到数组位置 145。那么你只需要一个包含 999 个元素的数组(每个元素都是一个学生列表)。

这对你来说应该是一个好的开始。当然,您应该阅读教科书或***以获取此类信息。但我假设你已经这样做了,并且厌倦了阅读。

【讨论】:

为什么不散列整个学生证? 因为那样它就不是真正的哈希,它只是学生证。那时您可以将其用作数组索引。我认为“散列” ID 是一个更好的例子。 我是一名初级程序员,我正在阅读您的答案。我想到的是:当你在一个有学生的房间里,比如说有 100 名学生,你想称呼其中一个学生时,你会说出他或她的名字。如果你说克里斯,不是每个学生都会站起来。克里斯、克里斯、克里斯和克里斯汀(有时叫克里斯)会站起来。这是因为键 |Kris|、|Chris| 和 |Christine|所有散列到声音 /Chris/!但如果你说“kay ar eye ess”,只有 Kris 会站起来。这是什么意思呢!?我不明白我自己的类比对哈希和数组意味着什么......【参考方案7】:

简而言之,这是哈希表的工作原理。

想象你有一个图书馆,里面装满了书。如果您要将书籍存储在一个阵列中,您会将每本书放在书架上的一个位置,然后当有人要求您找一本书时,您会浏览所有书架 - 非常慢。不过,如果有人说“book #12345”,你可以很容易地找到它。

假设您说,如果书名以“A”开头,则进入第 1 行。如果第二个字母是“B”,则进入第 1 行,第 2 架。如果第三个字母是“C” ',它会出现在第 1 行、第 2 架子、第 3 架子……等等,直到您确定书的位置。然后,根据书名,你就可以确切地知道它应该在哪里。

现在,我描述的简单的“散列”算法存在一些问题 - 一些书架会超载,而另一些书架会空着,一些书将被分配到同一个插槽......所以真正的散列函数是精心构造,尽量避免此类问题。

但这是基本思想。

【讨论】:

【参考方案8】:

我将回答关于哈希表和数组之间区别的那部分......但由于我之前从未实现过任何导入的哈希算法,我将把它留给更有知识的人:)

数组只是对象的有序列表。对象本身并不重要......重要的是,如果您想按插入顺序列出对象,它总是相同的(意味着第一个元素 always 的索引为0).

至于哈希表,它是按键索引的,而不是顺序...我认为对哈希算法的基本搜索会给您提供比我更多的洞察力...***有一个非常不错的...确定键进入的“桶”,以便对用作键的任意对象进行快速检索。

至于优点:如果插入顺序很重要,则需要数组或某种有序列表。如果通过任意键(由各种散列函数作为键)进行快速查找很重要,那么散列表是有意义的。

【讨论】:

您的回答很好,但存在一些事实漏洞。数组实际上是随机访问的(您可以在插入 arr[0] 之前插入 arr[10])。它们在内存中排序(如您所说),但插入顺序无关紧要。 (我想你在考虑一个链表) 要继续,并非所有关联表都使用散列。二叉搜索树是一种非常简单的关联结构(键/值查找),它实际上以完美的排序顺序维护事物。 有趣的是,哈希表总是在表面下使用数组实现的,这进一步强调了数组不强制插入/访问顺序这一事实。 @daniel 实际上,不,我不是......只是表达不好 :) 我不是在谈论插入顺序,只是在哈希表顺序中知道检索顺序并不那么重要作为任意密钥检索...感谢其他人澄清!【参考方案9】:

[这是对上面 me.yahoo.com/a 的评论的回复]

这取决于您的哈希函数。假设您的哈希函数根据单词的长度对单词进行哈希处理,chris 的键为 5。同样,yahoo 的键也为 5。现在,两个值(chris 和 yahoo)都将低于 5(即在由 5 键入的“桶”中)。这样您就不必使数组与数据的大小相等。

【讨论】:

【参考方案10】:

我相信,这个问题现在已经得到了相当清楚的回答,而且有很多不同的方式。

我只想添加另一个观点(这也可能使新读者感到困惑)

在最抽象的层面上,数组只是连续的内存块。给定单个元素的起始地址(startAddress)、大小(sizeOfElement)和index,元素的地址计算为:

elementAddress = startAddress + sizeOfElement * index

这里要注意的有趣的事情是,可以将数组抽象/查看为哈希表,其中index 作为键,上述函数作为哈希函数计算值在 O(1)

【讨论】:

【参考方案11】:

哈希表是一种为快速查找而创建的数据结构。

当条目数非常少时,哈希表无效。

reference

一些例子:

    import java.util.Collection;
    import java.util.Enumeration;
    import java.util.Hashtable;
    import java.util.Set;

    public class HashtableDemo 

    public static void main(String args[]) 

// Creating Hashtable for example

     Hashtable companies = new Hashtable();


// Java Hashtable example to put object into Hashtable
// put(key, value) is used to insert object into map

     companies.put("Google", "United States");
     companies.put("Nokia", "Finland");
     companies.put("Sony", "Japan");


// Java Hashtable example to get Object from Hashtable
// get(key) method is used to retrieve Objects from Hashtable

     companies.get("Google");


// Hashtable containsKey Example
// Use containsKey(Object) method to check if an Object exits as key in
// hashtable

     System.out.println("Does hashtable contains Google as key: "+companies.containsKey("Google"));


// Hashtable containsValue Example
// just like containsKey(), containsValue returns true if hashtable
// contains specified object as value

      System.out.println("Does hashtable contains Japan as value: "+companies.containsValue("Japan"));


// Hashtable enumeration Example
// hashtabl.elements() return enumeration of all hashtable values

      Enumeration enumeration = companies.elements();

      while (enumeration.hasMoreElements()) 
      System.out.println("hashtable values: "+enumeration.nextElement());
      


// How to check if Hashtable is empty in Java
// use isEmpty method of hashtable to check emptiness of hashtable in
// Java

       System.out.println("Is companies hashtable empty: "+companies.isEmpty());


// How to find size of Hashtable in Java
// use hashtable.size() method to find size of hashtable in Java

      System.out.println("Size of hashtable in Java: " + companies.size());


// How to get all values form hashtable in Java
// you can use keySet() method to get a Set of all the keys of hashtable
// in Java

      Set hashtableKeys = companies.keySet();


// you can also get enumeration of all keys by using method keys()

      Enumeration hashtableKeysEnum = companies.keys();


// How to get all keys from hashtable in Java
// There are two ways to get all values form hashtalbe first by using
// Enumeration and second getting values ad Collection

      Enumeration hashtableValuesEnum = companies.elements();


      Collection hashtableValues = companies.values();


// Hashtable clear example
// by using clear() we can reuse an existing hashtable, it clears all
// mappings.

       companies.clear();
      
     

输出:

Does hashtable contains Google as key: true

Does hashtable contains Japan as value: true

hashtable values: Finland

hashtable values: United States

hashtable values: Japan

Is companies hashtable empty: false

Size of hashtable in Java: 3

【讨论】:

请注意,link-only answers are discouraged,SO 答案应该是寻找解决方案的终点(与另一个中途停留的参考相比,它往往会随着时间的推移变得陈旧)。请考虑在此处添加独立的概要,并保留链接作为参考。

以上是关于哈希表的基本原理?的主要内容,如果未能解决你的问题,请参考以下文章

初步学习:hash存储和hash表的原理

数据结构 哈希表建立

Python算法哈希存储哈希表散列表原理

哈希表原理及如何避免键值冲突法?

数据结构:哈希表原理以及面试中的常见考点

HashMap的底层原理