我对哈希表的基本概念感到很困惑。如果我要编写一个哈希,我什至会如何开始?哈希表和普通数组有什么区别?
基本上,如果有人回答了这个问题,我想我的所有问题都会得到回答:如果我有 100 个随机生成的数字(作为键),我将如何实现哈希表,为什么这比数组更有利?
伪代码或 Java 将被视为一种学习工具......
到目前为止的答案有助于定义哈希表并解释一些理论,但我认为一个示例可以帮助您更好地了解它们。
哈希表和普通数组有什么区别?
哈希表和数组都是允许您存储和检索数据的结构。两者都允许您指定索引并检索与其关联的值。正如 Daniel Spiewak 所指出的,不同之处在于数组的索引是顺序的,而哈希表的索引是基于与它们关联的数据的值。
为什么要使用哈希表?
哈希表可以提供一种非常有效的方式来搜索大量数据中的项目,尤其是在其他情况下不易搜索的数据。(这里的“Large”是指巨大的,意思是执行顺序搜索需要很长时间)。
如果我要编写一个哈希,我什至会如何开始?
没问题。最简单的方法是发明一个可以对数据执行的任意数学运算,它返回一个数字N
(通常是整数)。然后将该数字用作“桶”数组的索引,并将您的数据存储在桶 #N
中。诀窍在于选择一个倾向于将值放置在不同存储桶中的操作,以便您以后轻松找到它们。
示例: 一家大型购物中心保留了顾客汽车和停车位置的数据库,以帮助购物者记住他们的停车地点。数据库存储make
、color
、license plate
和parking location
。在离开商店时,购物者通过输入其品牌和颜色来找到他的汽车。该数据库返回一个(相对较短的)车牌和停车位列表。快速扫描定位购物者的汽车。
您可以使用 SQL 查询来实现这一点:
SELECT license, location FROM cars WHERE make="$(make)" AND color="$(color)"
如果数据存储在一个数组中,它本质上只是一个列表,您可以想象通过扫描一个数组以查找所有匹配条目来实现查询。
另一方面,想象一个哈希规则:
将make和color中所有字母的ASCII码相加,除以100,余数作为hash值。
此规则会将每个项目转换为 0 到 99 之间的数字,本质上将数据分类到 100 个桶中。每次客户需要定位汽车时,您都可以对品牌和颜色进行哈希运算,从 100个存储桶中找出包含该信息的存储桶。您立即将搜索量减少了 100 倍!
现在将示例扩展到大量数据,例如一个包含数百万个条目的数据库,根据数十个标准进行搜索。一个“好的”散列函数将以最小化任何额外搜索的方式将数据分配到存储桶中,从而节省大量时间。
首先,您必须了解哈希函数是什么。哈希函数是一个函数,它接受一个键(例如,任意长度的字符串)并返回一个尽可能唯一的数字。相同的键必须始终返回相同的散列。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;
}
哈希表是关联的。这与数组有很大的不同,数组只是线性数据结构。使用数组,您可能会执行以下操作:
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
.
我想这可能会回答你的大部分问题。哈希表的实现是一个相当有趣的话题,维基百科很好地解决了这个话题。
“我对哈希表查找密钥的方式以及密钥的生成方式更感兴趣。”
散列将关键对象转换为数字。这称为“散列”——它对对象进行散列。请参阅散列函数。例如,对字符串的字节求和是一种标准的散列技术。您计算总和模 2 32以将散列保持在可管理的大小。哈希总是给出相同的答案。这是O (1)。
该数字在哈希表中为您提供了一个“插槽”。给定一个任意键对象,哈希值计算一个哈希值。然后哈希值会为您提供表中的插槽。通常mod( hash, table size )
. 这也是O (1)。
这是一般的解决方案。两个数值计算,你已经从任意对象作为键到任意对象作为值。很少有事情可以这么快。
从对象到哈希值的转换以这些常见方式之一发生。
如果它是 4 字节的“原始”对象,则该对象的本机值是一个数字。
对象的地址是4个字节,那么对象的地址可以作为哈希值。
一个简单的散列函数(MD5、SHA1 等等)累积对象的字节以创建一个 4 字节的数字。高级哈希不是简单的字节总和,简单的总和并不能充分反映所有原始输入位。
哈希表中的槽是 mod( number, size of table )。
如果该插槽具有所需的值,那么您就完成了。如果这不是所需的值,则需要寻找其他地方。有几种流行的探测算法可以在表格中寻找空闲位置。线性是对下一个空闲位置的简单搜索。Quadratic 是一种寻找空闲槽的非线性跳跃。随机数生成器(具有固定种子)可用于生成一系列探针,这些探针将均匀但任意地传播数据。
探测算法不是O (1)。如果桌子足够大,那么碰撞的几率就会很低,探测也无关紧要。如果表太小,则会发生冲突并进行探测。到那时,就需要“调整和调整”来平衡探测和表大小以优化性能。通常我们只是把桌子变大。
请参阅哈希表。
我还没有看到特别指出的东西:
在数组上使用哈希表的重点是性能。
遍历数组通常需要从 O(1) 到 O(x) 的任何时间,其中 x 是数组中的项目数。但是,找到您的项目的时间会非常多变,特别是如果我们谈论的是数组中的数十万个项目。
一个适当加权的哈希表通常具有几乎恒定的访问时间,略高于 O(1),无论哈希表中有多少项。
您不希望对 100 个随机生成的数字使用哈希表。
考虑哈希表的一个好方法是考虑值对。让我们以学生为例,假设每个人都有一个学生证号码。在您的程序中,您存储有关学生的信息(姓名、电话号码、账单等)。您希望仅使用基本信息(例如姓名或学生 ID)来查找有关学生的所有信息。
假设您有 10,000 名学生。如果您将它们全部存储在一个数组中,那么您必须遍历整个数组,将每个条目的学生 ID 与您要查找的学生 ID 进行比较。
相反,如果您将他们的学生 ID 号“散列”(见下文)到数组中的某个位置,那么您只需搜索学生的编号具有相同的散列即可。找到你想要的东西的工作要少得多。
在此示例中,假设学生 ID 只是 6 位数字。我们的哈希函数只能使用数字的后 3 位作为“哈希键”。因此 232145 被散列到数组位置 145。所以你只需要一个包含 999 个元素的数组(每个元素都是一个学生列表)。
这对你来说应该是一个好的开始。当然,您应该阅读教科书或维基百科以获取此类信息。但我假设你已经这样做了,并且厌倦了阅读。
简而言之,这是哈希表的工作原理。
想象一下,你有一个图书馆,里面装满了书。如果您要将书籍存储在一个阵列中,您会将每本书放在书架上的一个位置,然后当有人要求您找一本书时,您会浏览所有书架 - 非常慢。如果有人说“book #12345”,你可以很容易地找到它。
假设您说,如果书名以“A”开头,则位于第 1 行。如果第二个字母为“B”,则位于第 1 行,第 2 架。如果第三个字母为“C”,则进入第 1 行、第 2 架子、第 3 架子……以此类推,直到您确定书的位置。然后,根据书名,您可以确切地知道它应该在哪里。
现在,我描述的简单的“散列”算法存在一些问题——一些书架会超载,而另一些书架会空着,一些书会被分配到同一个插槽..所以真正的散列函数是经过精心构造的尽量避免此类问题。
但这是基本思想。
我将回答关于哈希表和数组之间区别的那部分......但由于我以前从未实现过任何导入的哈希算法,我将把它留给更有知识的人:)
数组只是对象的有序列表。对象本身并不重要......重要的是,如果您想按插入顺序列出对象,它始终是相同的(意味着第一个元素的索引始终为 0)。
至于哈希表,它是按键索引的,而不是顺序...我认为对哈希算法的基本搜索会给您比我更多的洞察力...维基百科有一个非常不错的...确定“桶" 用于快速检索用作键的任意对象的键。
至于优点:如果插入顺序很重要,则需要数组或某种有序列表。如果通过任意键(由各种散列函数作为键)进行快速查找很重要,那么散列表是有意义的。
[这是对上面me.yahoo.com/a的评论的回复]
这取决于您的哈希函数。假设您的哈希函数根据单词的长度对单词进行哈希处理,chris 的键为 5。同样,yahoo 的键也为 5。现在,两个值(chris 和 yahoo)都将低于 5(即在由 5 键入的“桶”中)。这样,您不必使数组等于数据的大小。
我相信,这个问题现在已经得到了非常清楚的回答,而且有很多不同的方式。
我只想添加另一个观点(这也可能使新读者感到困惑)
在最抽象的层次上,数组只是连续的内存块。给定单个元素的起始地址 ( startAddress
)、大小 ( sizeOfElement
) 和 ,index
元素的地址计算如下:
elementAddress = startAddress + sizeOfElement * index
这里要注意的有趣的事情是,可以将数组抽象/视为具有index
as 键的哈希表,并将上述函数作为哈希函数计算O(1)中值的位置
哈希表是一种为快速查找而创建的数据结构。
当条目数非常小时,哈希表无效。
一些例子:
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