所有的数据结构都有自己的特点,例如,当需要快速搜索元素(在 log(n) 中)时使用 BST。当需要在恒定时间内获取最小或最大元素时,使用堆或优先级队列。类似地,哈希表用于在恒定时间内获取、添加和删除元素。在进入实现方面之前,任何人都必须清楚哈希表的工作原理。所以这里是关于哈希表工作的简要背景,还应该注意的是,我们将交替使用哈希映射和哈希表术语,尽管在Java哈希表是线程安全的,而哈希映射不是。
我们将要实现的代码可在链接 1 和链接 2 中找到
但是强烈建议你必须完整地阅读这篇博客并尝试破译实现哈希映射的细节,然后尝试自己编写代码。
背景
每个哈希表都以(键,值)组合的形式存储数据。有趣的是,哈希表中的每个键都是唯一的,但值可以重复,这意味着其中存在的不同键的值可以相同。现在,当我们在数组中观察以获取值时,我们提供与该数组中的值对应的位置/索引。在哈希表中,我们使用键来获取与该键对应的值,而不是索引。现将整个过程描述如下
每次生成一个密钥。密钥被传递给散列函数。每个哈希函数都有两部分:哈希码和压缩器。
哈希码是一个整数(随机或非随机)。在Java,每个对象都有自己的哈希码。我们将在我们的哈希函数使用 JVM 生成的哈希码,并通过哈希表的大小对我们对哈希码进行模(%)的哈希码进行压缩。所以模运算符在我们的实现中是一个压缩器。
整个过程保证了对于任意一个key,我们都得到了Hash Table大小内的一个整数位置来插入对应的值。
所以这个过程很简单,用户给出一个(键,值)对设置为输入,并根据哈希函数生成的值生成索引,以存储特定键对应的值。因此,每当我们需要获取与键对应的值时,只需 O(1)。
当引入哈希冲突的概念时,这张图就不再那么美好和完美了。想象一下,对于不同的键值,哈希表的同一块现在被分配到它们之前存储与某个其他先前键对应的值的位置。我们当然无法取代它。那将是灾难性的!为了解决这个问题,我们将使用分离链接技术,请注意还有其他开放寻址技术,如双散列和线性探测,其效率与分离链接的效率几乎相同,您可以在链接 1 链接 2 阅读更多关于它们的信息链接3
现在我们要做的是制作一个与哈希表的特定桶对应的链表,以容纳映射到同一桶的不同键对应的所有值。
现在可能有一种情况,所有的键都映射到同一个桶,我们有一个来自一个桶的 n(哈希表的大小)大小的链表,所有其他桶都是空的,这是最坏的情况其中哈希表充当链接列表并且搜索是 O(n)。那么我们该怎么办?
负载系数
如果 n 是我们最初决定填充的桶的总数,比如说 10,假设现在有 7 个被填充,那么负载因子是 7/10=0.7。
在我们的实现中,每当我们向哈希表添加键值对时,我们都会检查负载因子,如果它大于 0.7,我们就会将哈希表的大小加倍。
执行
哈希节点数据类型
我们将尝试制作一个通用映射,而不会对键和值的数据类型施加任何限制。此外,每个哈希节点都需要知道它在链表中指向的下一个节点,因此还需要下一个指针。
我们计划保留在哈希图中的函数是
- 得到(K键):对应于该键的值,如果密钥是存在于HT返回(H ASTŤ能够)
- getSize() : 返回 HT 的大小
- add() :向 HT 添加新的有效键值对,如果已经存在则更新值
- remove() : 删除键值对
- isEmpty() :如果大小为零则返回真
ArrayList> bucket = new ArrayList<>();
实现了一个辅助函数来获取键的索引,以避免在其他函数(如 get、add 和 remove)中出现冗余。该函数使用内置的Java函数生成哈希码,我们将哈希码按HT的大小进行压缩,使索引在HT的大小范围内
得到()
get函数只将一个键作为输入,如果该键存在于表中,则返回相应的值,否则返回 null。步骤是:
- 检索输入键以找到HT中的索引
- 如果找到值则遍历对应于 HT 的喜欢列表,否则返回它,否则如果完全遍历列表而不返回它意味着该值不存在于表中且无法获取,因此返回 null
消除()
- 使用辅助函数获取输入键对应的索引
- 链表的遍历类似于get(),但这里的特殊之处在于,需要在找到键的同时删除键,出现两种情况
- 如果要删除的键存在于链表的头部
- 如果要移除的钥匙不在头部而是在其他地方
添加()
现在是整个实现中最有趣和最具挑战性的函数。有趣的是,当加载因子高于我们指定的值时,我们需要动态增加列表的大小。
- 就像删除步骤直到遍历和添加一样,两种情况(在头点或非头点添加)保持不变。
- 接近尾声时,如果负载因子大于 0.7
- 我们将数组列表的大小加倍,然后对现有键递归调用 add函数,因为在我们的例子中,生成的哈希值使用数组的大小来压缩我们使用的内置 JVM 哈希代码,因此我们需要为现有的键获取新索引键。理解这一点非常重要,请重新阅读本段,直到您了解 add函数发生的事情。
如果对应于特定桶的链表往往变得太长, Java在其自己的哈希表实现中会使用二叉搜索树。
Java
// Java program to demonstrate implementation of our
// own hash table with chaining for collision detection
import java.util.ArrayList;
import java.util.Objects;
// A node of chains
class HashNode {
K key;
V value;
final int hashCode;
// Reference to next node
HashNode next;
// Constructor
public HashNode(K key, V value, int hashCode)
{
this.key = key;
this.value = value;
this.hashCode = hashCode;
}
}
// Class to represent entire hash table
class Map {
// bucketArray is used to store array of chains
private ArrayList > bucketArray;
// Current capacity of array list
private int numBuckets;
// Current size of array list
private int size;
// Constructor (Initializes capacity, size and
// empty chains.
public Map()
{
bucketArray = new ArrayList<>();
numBuckets = 10;
size = 0;
// Create empty chains
for (int i = 0; i < numBuckets; i++)
bucketArray.add(null);
}
public int size() { return size; }
public boolean isEmpty() { return size() == 0; }
private final int hashCode (K key) {
return Objects.hashCode(key);
}
// This implements hash function to find index
// for a key
private int getBucketIndex(K key)
{
int hashCode = hashCode(key);
int index = hashCode % numBuckets;
// key.hashCode() coule be negative.
index = index < 0 ? index * -1 : index;
return index;
}
// Method to remove a given key
public V remove(K key)
{
// Apply hash function to find index for given key
int bucketIndex = getBucketIndex(key);
int hashCode = hashCode(key);
// Get head of chain
HashNode head = bucketArray.get(bucketIndex);
// Search for key in its chain
HashNode prev = null;
while (head != null) {
// If Key found
if (head.key.equals(key) && hashCode == head.hashCode)
break;
// Else keep moving in chain
prev = head;
head = head.next;
}
// If key was not there
if (head == null)
return null;
// Reduce size
size--;
// Remove key
if (prev != null)
prev.next = head.next;
else
bucketArray.set(bucketIndex, head.next);
return head.value;
}
// Returns value for a key
public V get(K key)
{
// Find head of chain for given key
int bucketIndex = getBucketIndex(key);
int hashCode = hashCode(key);
HashNode head = bucketArray.get(bucketIndex);
// Search key in chain
while (head != null) {
if (head.key.equals(key) && head.hashCode == hashCode)
return head.value;
head = head.next;
}
// If key not found
return null;
}
// Adds a key value pair to hash
public void add(K key, V value)
{
// Find head of chain for given key
int bucketIndex = getBucketIndex(key);
int hashCode = hashCode(key);
HashNode head = bucketArray.get(bucketIndex);
// Check if key is already present
while (head != null) {
if (head.key.equals(key) && head.hashCode == hashCode) {
head.value = value;
return;
}
head = head.next;
}
// Insert key in chain
size++;
head = bucketArray.get(bucketIndex);
HashNode newNode
= new HashNode(key, value, hashCode);
newNode.next = head;
bucketArray.set(bucketIndex, newNode);
// If load factor goes beyond threshold, then
// double hash table size
if ((1.0 * size) / numBuckets >= 0.7) {
ArrayList > temp = bucketArray;
bucketArray = new ArrayList<>();
numBuckets = 2 * numBuckets;
size = 0;
for (int i = 0; i < numBuckets; i++)
bucketArray.add(null);
for (HashNode headNode : temp) {
while (headNode != null) {
add(headNode.key, headNode.value);
headNode = headNode.next;
}
}
}
}
// Driver method to test Map class
public static void main(String[] args)
{
Map map = new Map<>();
map.add("this", 1);
map.add("coder", 2);
map.add("this", 4);
map.add("hi", 5);
System.out.println(map.size());
System.out.println(map.remove("this"));
System.out.println(map.remove("this"));
System.out.println(map.size());
System.out.println(map.isEmpty());
}
}
完整代码可在 https://github.com/ishaan007/Data-Structures/blob/master/HashMaps/Map 获得。Java
如果您希望与专家一起参加现场课程,请参阅DSA 现场工作专业课程和学生竞争性编程现场课程。