哈希表——hashset / hashmap

哈希表

哈希表是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。

有两种不同类型的哈希表:哈希集合和哈希映射。

  • 哈希集合集合数据结构的实现之一,用于存储非重复值
  • 哈希映射映射 数据结构的实现之一,用于存储(key, value)键值对。

标准模板库的帮助下,哈希表是易于使用的。大多数常见语言(如Java,C ++ 和 Python)都支持哈希集合和哈希映射。

通过选择合适的哈希函数,哈希表可以在插入和搜索方面实现出色的性能

哈希表的原理

正如我们在介绍中提到的,哈希表是一种数据结构,它使用哈希函数组织数据,以支持快速插入和搜索

哈希表的关键思想是使用哈希函数将键映射到存储桶

  1. 当我们插入一个新的键时,哈希函数将决定该键应该分配到哪个桶中,并将该键存储在相应的桶中;
  2. 当我们想要搜索一个键时,哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。
示例

img

在示例中,我们使用 y = x % 5 作为哈希函数。让我们使用这个例子来完成插入和搜索策略:

  1. 插入:我们通过哈希函数解析键,将它们映射到相应的桶中。
    • 例如,1987 分配给桶 2,而 24 分配给桶 4。
  2. 搜索:我们通过相同的哈希函数解析键,并仅在特定存储桶中搜索。
    • 如果我们搜索 1987,我们将使用相同的哈希函数将1987 映射到 2。因此我们在桶 2 中搜索,我们在那个桶中成功找到了 1987。
    • 例如,如果我们搜索 23,将映射 23 到 3,并在桶 3 中搜索。我们发现 23 不在桶 3 中,这意味着 23 不在哈希表中。

设计哈希表的关键

在设计哈希表时,你应该注意两个基本因素。

1. 哈希函数

哈希函数是哈希表中最重要的组件,该哈希表用于将键映射到特定的桶。在示例中,我们使用 y = x % 5 作为散列函数,其中 x 是键值,y 是分配的桶的索引。

散列函数将取决于键值的范围桶的数量。

下面是一些哈希函数的示例:

img

哈希函数的设计是一个开放的问题。其思想是尽可能将键分配到桶中,理想情况下,完美的哈希函数将是键和桶之间的一对一映射。然而,在大多数情况下,哈希函数并不完美,它需要在桶的数量和桶的容量之间进行权衡。

2. 冲突解决

理想情况下,如果我们的哈希函数是完美的一对一映射,我们将不需要处理冲突。不幸的是,在大多数情况下,冲突几乎是不可避免的。

例如,在我们之前的哈希函数(y = x % 5)中,1987 和 2 都分配给了桶 2,这是一个冲突

冲突解决算法应该解决以下几个问题:

  1. 如何组织在同一个桶中的值?
  2. 如果为同一个桶分配了太多的值,该怎么办?
  3. 如何在特定的桶中搜索目标值?

根据我们的哈希函数,这些问题与桶的容量和可能映射到同一个桶键的数目有关。

让我们假设存储最大键数的桶有 N 个键。

通常,如果 N 是常数且很小,我们可以简单地使用一个数组将键存储在同一个桶中。如果 N 是可变的或很大,我们可能需要使用高度平衡的二叉树来代替。

训练

到目前为止,您应该能够实现基本的哈希表。我们为您提供了实现哈希集和哈希映射的练习。阅读需求确定哈希函数并在需要时解决冲突

如果你不熟悉哈希集或是哈希映射的概念,可以返回介绍部分找出答案。.

插入搜索是哈希表中的两个基本操作。

此外,还有基于这两个操作的操作。例如,当我们删除元素时,我们将首先搜索元素,然后在元素存在的情况下从相应位置移除元素。

哈希集 - 用法

哈希集是集合的实现之一,它是一种存储不重复值的数据结构。

#include <unordered_set>                // 0. include the library

int main() {
    // 1. initialize a hash set
    unordered_set<int> hashset;   
    // 2. insert a new key
    hashset.insert(3);
    hashset.insert(2);
    hashset.insert(1);
    // 3. delete a key
    hashset.erase(2);
    // 4. check if the key is in the hash set
    if (hashset.count(2) <= 0) {
        cout << "Key 2 is not in the hash set." << endl;
    }
    // 5. get the size of the hash set
    cout << "The size of hash set is: " << hashset.size() << endl; 
    // 6. iterate the hash set
    for (auto it = hashset.begin(); it != hashset.end(); ++it) {
        cout << (*it) << " ";
    }
    cout << "are in the hash set." << endl;
    // 7. clear the hash set
    hashset.clear();
    // 8. check if the hash set is empty
    if (hashset.empty()) {
        cout << "hash set is empty now!" << endl;
    }
}

使用哈希集查重

插入新值并检查值是否在哈希集中是简单有效的。因此,通常使用哈希集来检查该值是否已经出现过。

让我们来看一个例子:

给定一个整数数组,查找数组是否包含任何重复项。

你可以简单地迭代每个值并将值插入集合中。 如果值已经在哈希集中,则存在重复。

在这里,我们为你提供了解决此类问题的模板:

/*
 * Template for using hash set to find duplicates.
 */
bool findDuplicates(vector<Type>& keys) {
    // Replace Type with actual type of your key
    unordered_set<Type> hashset;
    for (Type key : keys) {
        if (hashset.count(key) > 0) {
            return true;
        }
        hashset.insert(key);
    }
    return false;
}

哈希映射 - 用法

哈希映射是用于存储 (key, value) 键值对的一种实现。

#include <unordered_map>                // 0. include the library

int main() {
    // 1. initialize a hash map
    unordered_map<int, int> hashmap;
    // 2. insert a new (key, value) pair
    hashmap.insert(make_pair(0, 0));
    hashmap.insert(make_pair(2, 3));
    // 3. insert a new (key, value) pair or update the value of existed key
    hashmap[1] = 1;
    hashmap[1] = 2;
    // 4. get the value of a specific key
    cout << "The value of key 1 is: " << hashmap[1] << endl;
    // 5. delete a key
    hashmap.erase(2);
    // 6. check if a key is in the hash map
    if (hashmap.count(2) <= 0) {
        cout << "Key 2 is not in the hash map." << endl;
    }
    // 7. get the size of the hash map
    cout << "the size of hash map is: " << hashmap.size() << endl; 
    // 8. iterate the hash map
    for (auto it = hashmap.begin(); it != hashmap.end(); ++it) {
        cout << "(" << it->first << "," << it->second << ") ";
    }
    cout << "are in the hash map." << endl;
    // 9. clear the hash map
    hashmap.clear();
    // 10. check if the hash map is empty
    if (hashmap.empty()) {
        cout << "hash map is empty now!" << endl;
    }
}

场景 I - 提供更多信息

使用哈希映射的第一个场景是,我们需要更多的信息,而不仅仅是键。然后通过哈希映射建立密钥与信息之间的映射关系

让我们来看一个例子:

给定一个整数数组,返回两个数字的索引,使它们相加得到特定目标。

在这个例子中,如果我们只想在有解决方案时返回 true,我们可以使用哈希集合来存储迭代数组时的所有值,并检查 target - current_value 是否在哈希集合中。

但是,我们被要求返回更多信息,这意味着我们不仅关心值,还关心索引。我们不仅需要存储数字作为键,还需要存储索引作为值。因此,我们应该使用哈希映射而不是哈希集合。

更重要的是

在某些情况下,我们需要更多信息,不仅要返回更多信息,还要帮助我们做出决策

在前面的示例中,当我们遇到重复的键时,我们将立即返回相应的信息。但有时,我们可能想先检查键的值是否可以接受。

在这里,我们为您提供了解决此类问题的模板:

/*
 * Template for using hash map to find duplicates.
 * Replace ReturnType with the actual type of your return value.
 */
ReturnType aggregateByKey_hashmap(vector<Type>& keys) {
    // Replace Type and InfoType with actual type of your key and value
    unordered_map<Type, InfoType> hashtable;
    for (Type key : keys) {
        if (hashmap.count(key) > 0) {
            if (hashmap[key] satisfies the requirement) {
                return needed_information;
            }
        }
        // Value can be any information you needed (e.g. index)
        hashmap[key] = value;
    }
    return needed_information;
}

场景 II - 按键聚合

另一个常见的场景是按键聚合所有信息。我们也可以使用哈希映射来实现这一目标。

这是一个例子:

给定一个字符串,找到它中的第一个非重复字符并返回它的索引。如果它不存在,则返回 -1。

解决此问题的一种简单方法是首先计算每个字符的出现次数。然后通过结果找出第一个与众不同的角色。

因此,我们可以维护一个哈希映射,其键是字符,而值是相应字符的计数器。每次迭代一个字符时,我们只需将相应的值加 1。

更重要的是

解决此类问题的关键是在遇到现有键时确定策略

在上面的示例中,我们的策略是计算事件的数量。有时,我们可能会将所有值加起来。有时,我们可能会用最新的值替换原始值。策略取决于问题,实践将帮助您做出正确的决定。

在这里,我们为您提供了解决此类问题的模板:

/*
 * Template for using hash map to find duplicates.
 * Replace ReturnType with the actual type of your return value.
 */
ReturnType aggregateByKey_hashmap(vector<Type>& keys) {
    // Replace Type and InfoType with actual type of your key and value
    unordered_map<Type, InfoType> hashtable;
    for (Type key : keys) {
        if (hashmap.count(key) > 0) {
            update hashmap[key];
        }
        // Value can be any information you needed (e.g. index)
        hashmap[key] = value;
    }
    return needed_information;
}

设计键

在以前的问题中,键的选择相对简单。不幸的是,有时你必须考虑在使用哈希表时设计合适的键

我们来看一个例子:

给定一组字符串,将字母异位词组合在一起。

众所周知,哈希映射可以很好地按键分组信息。但是我们不能直接使用原始字符串作为键。我们必须设计一个合适的键来呈现字母异位词的类型。例如,有字符串 “eat” 和 “ate” 应该在同一组中。但是 “eat” 和 “act” 不应该组合在一起。

解决方案

实际上,设计关键是在原始信息和哈希映射使用的实际键之间建立映射关系。设计键时,需要保证:

  1. 属于同一组的所有值都将映射到同一组中。
  2. 需要分成不同组的值不会映射到同一组。

此过程类似于设计哈希函数,但这是一个本质区别。哈希函数满足第一个规则但可能不满足第二个规则。但是你的映射函数应该满足它们。

在上面的示例中,我们的映射策略可以是:对字符串进行排序并使用排序后的字符串作为键。也就是说,“eat” 和 “ate” 都将映射到 “aet”。

设计键 - 总结

  1. 当字符串 / 数组中每个元素的顺序不重要时,可以使用排序后的字符串 / 数组作为键。img
  2. 如果只关心每个值的偏移量,通常是第一个值的偏移量,则可以使用偏移量作为键。img
  3. 在树中,你有时可能会希望直接使用 TreeNode 作为键。 但在大多数情况下,采用子树的序列化表述可能是一个更好的主意。img
  4. 在矩阵中,你可能希望使用行索引列索引作为键。
  5. 在数独中,可以将行索引和列索引组合来标识此元素属于哪个img
    每个值的偏移量,通常是第一个值的偏移量,则可以使用偏移量作为键。[外链图片转存中…(img-kVHyOmRM-1589299271190)]
  6. 在树中,你有时可能会希望直接使用 TreeNode 作为键。 但在大多数情况下,采用子树的序列化表述可能是一个更好的主意。[外链图片转存中…(img-xKP4SGlm-1589299271191)]
  7. 在矩阵中,你可能希望使用行索引列索引作为键。
  8. 在数独中,可以将行索引和列索引组合来标识此元素属于哪个。[外链图片转存中…(img-cqeZQjVr-1589299271193)]
  9. 有时,在矩阵中,您可能希望将值聚合在同一对角线中。img
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章