建一个网站首先要怎么做有没有专门做名片的网站
建一个网站首先要怎么做,有没有专门做名片的网站,网站建设 样板,wordpress 文章浏览数目录 前言 1 哈希表 哈希表常用操作 哈希表简单实现 哈希冲突与扩容 2 哈希冲突 链式地址 开放寻址 线性探测 平方探测 多次哈希 编程语言的选择 3 哈希算法 哈希算法的目标 哈希算法的设计 常见哈希算法 数据结构的哈希值 总结 前言
秋招复习之哈希表。 1 哈希表
「哈希表 h… 目录 前言 1 哈希表 哈希表常用操作 哈希表简单实现 哈希冲突与扩容 2 哈希冲突 链式地址 开放寻址 线性探测 平方探测 多次哈希 编程语言的选择 3 哈希算法 哈希算法的目标 哈希算法的设计 常见哈希算法 数据结构的哈希值 总结 前言
秋招复习之哈希表。 1 哈希表
「哈希表 hash table」又称「散列表」它通过建立键 key 与值 value 之间的映射实现高效的元素查询。具体而言我们向哈希表中输入一个键 key 则可以在 O(1) 时间内获取对应的值 value 。 除哈希表外数组和链表也可以实现查询功能它们的效率对比如表所示。
添加元素仅需将元素添加至数组链表的尾部即可使用 O(1) 时间。查询元素由于数组链表是乱序的因此需要遍历其中的所有元素使用 O(n) 时间。删除元素需要先查询到元素再从数组链表中删除使用O(n) 时间。 观察发现在哈希表中进行增删查改的时间复杂度都是 O(1) 非常高效。
哈希表常用操作
哈希表的常见操作包括初始化、查询操作、添加键值对和删除键值对等示例代码如下
/* 初始化哈希表 */
MapInteger, String map new HashMap();/* 添加操作 */
// 在哈希表中添加键值对 (key, value)
map.put(12836, 小哈);
map.put(15937, 小啰);
map.put(16750, 小算);
map.put(13276, 小法);
map.put(10583, 小鸭);/* 查询操作 */
// 向哈希表中输入键 key 得到值 value
String name map.get(15937);/* 删除操作 */
// 在哈希表中删除键值对 (key, value)
map.remove(10583);
/* 初始化哈希表 */
unordered_mapint, string map;/* 添加操作 */
// 在哈希表中添加键值对 (key, value)
map[12836] 小哈;
map[15937] 小啰;
map[16750] 小算;
map[13276] 小法;
map[10583] 小鸭;/* 查询操作 */
// 向哈希表中输入键 key 得到值 value
string name map[15937];/* 删除操作 */
// 在哈希表中删除键值对 (key, value)
map.erase(10583);
哈希表有三种常用的遍历方式遍历键值对、遍历键和遍历值。示例代码如下
/* 遍历哈希表 */
// 遍历键值对 key-value
for (Map.Entry Integer, String kv: map.entrySet()) {System.out.println(kv.getKey() - kv.getValue());
}
// 单独遍历键 key
for (int key: map.keySet()) {System.out.println(key);
}
// 单独遍历值 value
for (String val: map.values()) {System.out.println(val);
}
/* 遍历哈希表 */
// 遍历键值对 key-value
for (auto kv: map) {cout kv.first - kv.second endl;
}
// 使用迭代器遍历 key-value
for (auto iter map.begin(); iter ! map.end(); iter) {cout iter-first - iter-second endl;
}
哈希表简单实现
我们先考虑最简单的情况仅用一个数组来实现哈希表。在哈希表中我们将数组中的每个空位称为「桶 bucket」每个桶可存储一个键值对。因此查询操作就是找到 key 对应的桶并在桶中获取 value 。
那么如何基于 key 定位对应的桶呢这是通过「哈希函数 hash function」实现的。哈希函数的作用是将一个较大的输入空间映射到一个较小的输出空间。在哈希表中输入空间是所有 key 输出空间是所有桶数组索引。换句话说输入一个 key 我们可以通过哈希函数得到该 key 对应的键值对在数组中的存储位置。
输入一个 key 哈希函数的计算过程分为以下两步。
通过某种哈希算法 hash() 计算得到哈希值。将哈希值对桶数量数组长度capacity 取模从而获取该 key 对应的数组索引 index 。
index hash(key) % capacity
随后我们就可以利用 index 在哈希表中访问对应的桶从而获取 value 。
设数组长度 capacity 100、哈希算法 hash(key) key 易得哈希函数为 key % 100 。图 以 key 学号和 value 姓名为例展示了哈希函数的工作原理。 以下代码实现了一个简单哈希表。其中我们将 key 和 value 封装成一个类 Pair 以表示键值对。
/* 键值对 */
class Pair {public int key;public String val;public Pair(int key, String val) {this.key key;this.val val;}
}/* 基于数组实现的哈希表 */
class ArrayHashMap {private ListPair buckets;public ArrayHashMap() {// 初始化数组包含 100 个桶buckets new ArrayList();for (int i 0; i 100; i) {buckets.add(null);}}/* 哈希函数 */private int hashFunc(int key) {int index key % 100;return index;}/* 查询操作 */public String get(int key) {int index hashFunc(key);Pair pair buckets.get(index);if (pair null)return null;return pair.val;}/* 添加操作 */public void put(int key, String val) {Pair pair new Pair(key, val);int index hashFunc(key);buckets.set(index, pair);}/* 删除操作 */public void remove(int key) {int index hashFunc(key);// 置为 null 代表删除buckets.set(index, null);}/* 获取所有键值对 */public ListPair pairSet() {ListPair pairSet new ArrayList();for (Pair pair : buckets) {if (pair ! null)pairSet.add(pair);}return pairSet;}/* 获取所有键 */public ListInteger keySet() {ListInteger keySet new ArrayList();for (Pair pair : buckets) {if (pair ! null)keySet.add(pair.key);}return keySet;}/* 获取所有值 */public ListString valueSet() {ListString valueSet new ArrayList();for (Pair pair : buckets) {if (pair ! null)valueSet.add(pair.val);}return valueSet;}/* 打印哈希表 */public void print() {for (Pair kv : pairSet()) {System.out.println(kv.key - kv.val);}}
}
/* 键值对 */
struct Pair {public:int key;string val;Pair(int key, string val) {this-key key;this-val val;}
};/* 基于数组实现的哈希表 */
class ArrayHashMap {private:vectorPair * buckets;public:ArrayHashMap() {// 初始化数组包含 100 个桶buckets vectorPair *(100);}~ArrayHashMap() {// 释放内存for (const auto bucket : buckets) {delete bucket;}buckets.clear();}/* 哈希函数 */int hashFunc(int key) {int index key % 100;return index;}/* 查询操作 */string get(int key) {int index hashFunc(key);Pair *pair buckets[index];if (pair nullptr)return ;return pair-val;}/* 添加操作 */void put(int key, string val) {Pair *pair new Pair(key, val);int index hashFunc(key);buckets[index] pair;}/* 删除操作 */void remove(int key) {int index hashFunc(key);// 释放内存并置为 nullptrdelete buckets[index];buckets[index] nullptr;}/* 获取所有键值对 */vectorPair * pairSet() {vectorPair * pairSet;for (Pair *pair : buckets) {if (pair ! nullptr) {pairSet.push_back(pair);}}return pairSet;}/* 获取所有键 */vectorint keySet() {vectorint keySet;for (Pair *pair : buckets) {if (pair ! nullptr) {keySet.push_back(pair-key);}}return keySet;}/* 获取所有值 */vectorstring valueSet() {vectorstring valueSet;for (Pair *pair : buckets) {if (pair ! nullptr) {valueSet.push_back(pair-val);}}return valueSet;}/* 打印哈希表 */void print() {for (Pair *kv : pairSet()) {cout kv-key - kv-val endl;}}
};
哈希冲突与扩容
从本质上看哈希函数的作用是将所有 key 构成的输入空间映射到数组所有索引构成的输出空间而输入空间往往远大于输出空间。因此理论上一定存在“多个输入对应相同输出”的情况。
对于上述示例中的哈希函数当输入的 key 后两位相同时哈希函数的输出结果也相同。例如查询学号为 12836 和 20336 的两个学生时我们得到
12836 % 100 36
20336 % 100 36如图所示两个学号指向了同一个姓名这显然是不对的。我们将这种多个输入对应同一输出的情况称为「哈希冲突 hash collision」。 容易想到哈希表容量 n越大多个 key 被分配到同一个桶中的概率就越低冲突就越少。因此我们可以通过扩容哈希表来减少哈希冲突。
扩容前键值对 (136, A) 和 (236, D) 发生冲突扩容后冲突消失。 类似于数组扩容哈希表扩容需将所有键值对从原哈希表迁移至新哈希表非常耗时并且由于哈希表容量 capacity 改变我们需要通过哈希函数来重新计算所有键值对的存储位置这进一步增加了扩容过程的计算开销。为此编程语言通常会预留足够大的哈希表容量防止频繁扩容。
「负载因子 load factor」是哈希表的一个重要概念其定义为哈希表的元素数量除以桶数量用于衡量哈希冲突的严重程度也常作为哈希表扩容的触发条件。例如在 Java 中当负载因子超过 0.75 时系统会将哈希表扩容至原先的 2 倍。
2 哈希冲突
上一节提到通常情况下哈希函数的输入空间远大于输出空间因此理论上哈希冲突是不可避免的。比如输入空间为全体整数输出空间为数组容量大小则必然有多个整数映射至同一桶索引。
哈希冲突会导致查询结果错误严重影响哈希表的可用性。为了解决该问题每当遇到哈希冲突时我们就进行哈希表扩容直至冲突消失为止。此方法简单粗暴且有效但效率太低因为哈希表扩容需要进行大量的数据搬运与哈希值计算。为了提升效率我们可以采用以下策略。
改良哈希表数据结构使得哈希表可以在出现哈希冲突时正常工作。仅在必要时即当哈希冲突比较严重时才执行扩容操作。
哈希表的结构改良方法主要包括“链式地址”和“开放寻址”。
链式地址
在原始哈希表中每个桶仅能存储一个键值对。「链式地址 separate chaining」将单个元素转换为链表将键值对作为链表节点将所有发生冲突的键值对都存储在同一链表中。图展示了一个链式地址哈希表的例子。 基于链式地址实现的哈希表的操作方法发生了以下变化。
查询元素输入 key 经过哈希函数得到桶索引即可访问链表头节点然后遍历链表并对比 key 以查找目标键值对。添加元素首先通过哈希函数访问链表头节点然后将节点键值对添加到链表中。删除元素根据哈希函数的结果访问链表头部接着遍历链表以查找目标节点并将其删除。
链式地址存在以下局限性。
占用空间增大链表包含节点指针它相比数组更加耗费内存空间。查询效率降低因为需要线性遍历链表来查找对应元素。
以下代码给出了链式地址哈希表的简单实现需要注意两点。
使用列表动态数组代替链表从而简化代码。在这种设定下哈希表数组包含多个桶每个桶都是一个列表。以下实现包含哈希表扩容方法。当负载因子超过 2/3 时我们将哈希表扩容至原先的 2 倍。
/* 链式地址哈希表 */
class HashMapChaining {int size; // 键值对数量int capacity; // 哈希表容量double loadThres; // 触发扩容的负载因子阈值int extendRatio; // 扩容倍数ListListPair buckets; // 桶数组/* 构造方法 */public HashMapChaining() {size 0;capacity 4;loadThres 2.0 / 3.0;extendRatio 2;buckets new ArrayList(capacity);for (int i 0; i capacity; i) {buckets.add(new ArrayList());}}/* 哈希函数 */int hashFunc(int key) {return key % capacity;}/* 负载因子 */double loadFactor() {return (double) size / capacity;}/* 查询操作 */String get(int key) {int index hashFunc(key);ListPair bucket buckets.get(index);// 遍历桶若找到 key 则返回对应 valfor (Pair pair : bucket) {if (pair.key key) {return pair.val;}}// 若未找到 key 则返回 nullreturn null;}/* 添加操作 */void put(int key, String val) {// 当负载因子超过阈值时执行扩容if (loadFactor() loadThres) {extend();}int index hashFunc(key);ListPair bucket buckets.get(index);// 遍历桶若遇到指定 key 则更新对应 val 并返回for (Pair pair : bucket) {if (pair.key key) {pair.val val;return;}}// 若无该 key 则将键值对添加至尾部Pair pair new Pair(key, val);bucket.add(pair);size;}/* 删除操作 */void remove(int key) {int index hashFunc(key);ListPair bucket buckets.get(index);// 遍历桶从中删除键值对for (Pair pair : bucket) {if (pair.key key) {bucket.remove(pair);size--;break;}}}/* 扩容哈希表 */void extend() {// 暂存原哈希表ListListPair bucketsTmp buckets;// 初始化扩容后的新哈希表capacity * extendRatio;buckets new ArrayList(capacity);for (int i 0; i capacity; i) {buckets.add(new ArrayList());}size 0;// 将键值对从原哈希表搬运至新哈希表for (ListPair bucket : bucketsTmp) {for (Pair pair : bucket) {put(pair.key, pair.val);}}}/* 打印哈希表 */void print() {for (ListPair bucket : buckets) {ListString res new ArrayList();for (Pair pair : bucket) {res.add(pair.key - pair.val);}System.out.println(res);}}
}
/* 链式地址哈希表 */
class HashMapChaining {private:int size; // 键值对数量int capacity; // 哈希表容量double loadThres; // 触发扩容的负载因子阈值int extendRatio; // 扩容倍数vectorvectorPair * buckets; // 桶数组public:/* 构造方法 */HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {buckets.resize(capacity);}/* 析构方法 */~HashMapChaining() {for (auto bucket : buckets) {for (Pair *pair : bucket) {// 释放内存delete pair;}}}/* 哈希函数 */int hashFunc(int key) {return key % capacity;}/* 负载因子 */double loadFactor() {return (double)size / (double)capacity;}/* 查询操作 */string get(int key) {int index hashFunc(key);// 遍历桶若找到 key 则返回对应 valfor (Pair *pair : buckets[index]) {if (pair-key key) {return pair-val;}}// 若未找到 key 则返回空字符串return ;}/* 添加操作 */void put(int key, string val) {// 当负载因子超过阈值时执行扩容if (loadFactor() loadThres) {extend();}int index hashFunc(key);// 遍历桶若遇到指定 key 则更新对应 val 并返回for (Pair *pair : buckets[index]) {if (pair-key key) {pair-val val;return;}}// 若无该 key 则将键值对添加至尾部buckets[index].push_back(new Pair(key, val));size;}/* 删除操作 */void remove(int key) {int index hashFunc(key);auto bucket buckets[index];// 遍历桶从中删除键值对for (int i 0; i bucket.size(); i) {if (bucket[i]-key key) {Pair *tmp bucket[i];bucket.erase(bucket.begin() i); // 从中删除键值对delete tmp; // 释放内存size--;return;}}}/* 扩容哈希表 */void extend() {// 暂存原哈希表vectorvectorPair * bucketsTmp buckets;// 初始化扩容后的新哈希表capacity * extendRatio;buckets.clear();buckets.resize(capacity);size 0;// 将键值对从原哈希表搬运至新哈希表for (auto bucket : bucketsTmp) {for (Pair *pair : bucket) {put(pair-key, pair-val);// 释放内存delete pair;}}}/* 打印哈希表 */void print() {for (auto bucket : buckets) {cout [;for (Pair *pair : bucket) {cout pair-key - pair-val , ;}cout ]\n;}}
};
值得注意的是当链表很长时查询效率 O(n) 很差。此时可以将链表转换为“AVL 树”或“红黑树”从而将查询操作的时间复杂度优化至 O(logn) 。 开放寻址
「开放寻址 open addressing」不引入额外的数据结构而是通过“多次探测”来处理哈希冲突探测方式主要包括线性探测、平方探测和多次哈希等。
下面以线性探测为例介绍开放寻址哈希表的工作机制。 线性探测
采用固定步长的线性搜索来进行探测其操作方法与普通哈希表有所不同。
插入元素通过哈希函数计算桶索引若发现桶内已有元素则从冲突位置向后线性遍历步长通常为 1 直至找到空桶将元素插入其中。查找元素若发现哈希冲突则使用相同步长向后进行线性遍历直到找到对应元素返回 value 即可如果遇到空桶说明目标元素不在哈希表中返回 None 。
图展示了开放寻址线性探测哈希表的键值对分布。根据此哈希函数最后两位相同的 key 都会被映射到相同的桶。而通过线性探测它们被依次存储在该桶以及之下的桶中。 然而线性探测容易产生“聚集现象”。具体来说数组中连续被占用的位置越长这些连续位置发生哈希冲突的可能性越大从而进一步促使该位置的聚堆生长形成恶性循环最终导致增删查改操作效率劣化。
值得注意的是我们不能在开放寻址哈希表中直接删除元素。这是因为删除元素会在数组内产生一个空桶 None 而当查询元素时线性探测到该空桶就会返回因此在该空桶之下的元素都无法再被访问到程序可能误判这些元素不存在。 为了解决该问题我们可以采用「懒删除 lazy deletion」机制它不直接从哈希表中移除元素而是利用一个常量 TOMBSTONE 来标记这个桶。在该机制下None 和 TOMBSTONE 都代表空桶都可以放置键值对。但不同的是线性探测到 TOMBSTONE 时应该继续遍历因为其之下可能还存在键值对。
然而懒删除可能会加速哈希表的性能退化。这是因为每次删除操作都会产生一个删除标记随着 TOMBSTONE 的增加搜索时间也会增加因为线性探测可能需要跳过多个 TOMBSTONE 才能找到目标元素。
为此考虑在线性探测中记录遇到的首个 TOMBSTONE 的索引并将搜索到的目标元素与该 TOMBSTONE 交换位置。这样做的好处是当每次查询或添加元素时元素会被移动至距离理想位置探测起始点更近的桶从而优化查询效率。
以下代码实现了一个包含懒删除的开放寻址线性探测哈希表。为了更加充分地使用哈希表的空间我们将哈希表看作一个“环形数组”当越过数组尾部时回到头部继续遍历。
/* 开放寻址哈希表 */
class HashMapOpenAddressing {private int size; // 键值对数量private int capacity 4; // 哈希表容量private final double loadThres 2.0 / 3.0; // 触发扩容的负载因子阈值private final int extendRatio 2; // 扩容倍数private Pair[] buckets; // 桶数组private final Pair TOMBSTONE new Pair(-1, -1); // 删除标记/* 构造方法 */public HashMapOpenAddressing() {size 0;buckets new Pair[capacity];}/* 哈希函数 */private int hashFunc(int key) {return key % capacity;}/* 负载因子 */private double loadFactor() {return (double) size / capacity;}/* 搜索 key 对应的桶索引 */private int findBucket(int key) {int index hashFunc(key);int firstTombstone -1;// 线性探测当遇到空桶时跳出while (buckets[index] ! null) {// 若遇到 key 返回对应的桶索引if (buckets[index].key key) {// 若之前遇到了删除标记则将键值对移动至该索引处if (firstTombstone ! -1) {buckets[firstTombstone] buckets[index];buckets[index] TOMBSTONE;return firstTombstone; // 返回移动后的桶索引}return index; // 返回桶索引}// 记录遇到的首个删除标记if (firstTombstone -1 buckets[index] TOMBSTONE) {firstTombstone index;}// 计算桶索引越过尾部则返回头部index (index 1) % capacity;}// 若 key 不存在则返回添加点的索引return firstTombstone -1 ? index : firstTombstone;}/* 查询操作 */public String get(int key) {// 搜索 key 对应的桶索引int index findBucket(key);// 若找到键值对则返回对应 valif (buckets[index] ! null buckets[index] ! TOMBSTONE) {return buckets[index].val;}// 若键值对不存在则返回 nullreturn null;}/* 添加操作 */public void put(int key, String val) {// 当负载因子超过阈值时执行扩容if (loadFactor() loadThres) {extend();}// 搜索 key 对应的桶索引int index findBucket(key);// 若找到键值对则覆盖 val 并返回if (buckets[index] ! null buckets[index] ! TOMBSTONE) {buckets[index].val val;return;}// 若键值对不存在则添加该键值对buckets[index] new Pair(key, val);size;}/* 删除操作 */public void remove(int key) {// 搜索 key 对应的桶索引int index findBucket(key);// 若找到键值对则用删除标记覆盖它if (buckets[index] ! null buckets[index] ! TOMBSTONE) {buckets[index] TOMBSTONE;size--;}}/* 扩容哈希表 */private void extend() {// 暂存原哈希表Pair[] bucketsTmp buckets;// 初始化扩容后的新哈希表capacity * extendRatio;buckets new Pair[capacity];size 0;// 将键值对从原哈希表搬运至新哈希表for (Pair pair : bucketsTmp) {if (pair ! null pair ! TOMBSTONE) {put(pair.key, pair.val);}}}/* 打印哈希表 */public void print() {for (Pair pair : buckets) {if (pair null) {System.out.println(null);} else if (pair TOMBSTONE) {System.out.println(TOMBSTONE);} else {System.out.println(pair.key - pair.val);}}}
}
/* 开放寻址哈希表 */
class HashMapOpenAddressing {private:int size; // 键值对数量int capacity 4; // 哈希表容量const double loadThres 2.0 / 3.0; // 触发扩容的负载因子阈值const int extendRatio 2; // 扩容倍数vectorPair * buckets; // 桶数组Pair *TOMBSTONE new Pair(-1, -1); // 删除标记public:/* 构造方法 */HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {}/* 析构方法 */~HashMapOpenAddressing() {for (Pair *pair : buckets) {if (pair ! nullptr pair ! TOMBSTONE) {delete pair;}}delete TOMBSTONE;}/* 哈希函数 */int hashFunc(int key) {return key % capacity;}/* 负载因子 */double loadFactor() {return (double)size / capacity;}/* 搜索 key 对应的桶索引 */int findBucket(int key) {int index hashFunc(key);int firstTombstone -1;// 线性探测当遇到空桶时跳出while (buckets[index] ! nullptr) {// 若遇到 key 返回对应的桶索引if (buckets[index]-key key) {// 若之前遇到了删除标记则将键值对移动至该索引处if (firstTombstone ! -1) {buckets[firstTombstone] buckets[index];buckets[index] TOMBSTONE;return firstTombstone; // 返回移动后的桶索引}return index; // 返回桶索引}// 记录遇到的首个删除标记if (firstTombstone -1 buckets[index] TOMBSTONE) {firstTombstone index;}// 计算桶索引越过尾部则返回头部index (index 1) % capacity;}// 若 key 不存在则返回添加点的索引return firstTombstone -1 ? index : firstTombstone;}/* 查询操作 */string get(int key) {// 搜索 key 对应的桶索引int index findBucket(key);// 若找到键值对则返回对应 valif (buckets[index] ! nullptr buckets[index] ! TOMBSTONE) {return buckets[index]-val;}// 若键值对不存在则返回空字符串return ;}/* 添加操作 */void put(int key, string val) {// 当负载因子超过阈值时执行扩容if (loadFactor() loadThres) {extend();}// 搜索 key 对应的桶索引int index findBucket(key);// 若找到键值对则覆盖 val 并返回if (buckets[index] ! nullptr buckets[index] ! TOMBSTONE) {buckets[index]-val val;return;}// 若键值对不存在则添加该键值对buckets[index] new Pair(key, val);size;}/* 删除操作 */void remove(int key) {// 搜索 key 对应的桶索引int index findBucket(key);// 若找到键值对则用删除标记覆盖它if (buckets[index] ! nullptr buckets[index] ! TOMBSTONE) {delete buckets[index];buckets[index] TOMBSTONE;size--;}}/* 扩容哈希表 */void extend() {// 暂存原哈希表vectorPair * bucketsTmp buckets;// 初始化扩容后的新哈希表capacity * extendRatio;buckets vectorPair *(capacity, nullptr);size 0;// 将键值对从原哈希表搬运至新哈希表for (Pair *pair : bucketsTmp) {if (pair ! nullptr pair ! TOMBSTONE) {put(pair-key, pair-val);delete pair;}}}/* 打印哈希表 */void print() {for (Pair *pair : buckets) {if (pair nullptr) {cout nullptr endl;} else if (pair TOMBSTONE) {cout TOMBSTONE endl;} else {cout pair-key - pair-val endl;}}}
}; 平方探测
平方探测与线性探测类似都是开放寻址的常见策略之一。当发生冲突时平方探测不是简单地跳过一个固定的步数而是跳过“探测次数的平方”的步数即 1,4,9,… 步。
平方探测主要具有以下优势。
平方探测通过跳过探测次数平方的距离试图缓解线性探测的聚集效应。平方探测会跳过更大的距离来寻找空位置有助于数据分布得更加均匀。
然而平方探测并不是完美的。
仍然存在聚集现象即某些位置比其他位置更容易被占用。由于平方的增长平方探测可能不会探测整个哈希表这意味着即使哈希表中有空桶平方探测也可能无法访问到它。
多次哈希
顾名思义多次哈希方法使用多个哈希函数 f1(x)、f2(x)、f3(x)、… 进行探测。
插入元素若哈希函数 f1(x) 出现冲突则尝试 f2(x) 以此类推直到找到空位后插入元素。查找元素在相同的哈希函数顺序下进行查找直到找到目标元素时返回若遇到空位或已尝试所有哈希函数说明哈希表中不存在该元素则返回 None 。
与线性探测相比多次哈希方法不易产生聚集但多个哈希函数会带来额外的计算量。
请注意开放寻址线性探测、平方探测和多次哈希哈希表都存在“不能直接删除元素”的问题。
编程语言的选择
各种编程语言采取了不同的哈希表实现策略下面举几个例子。
Python 采用开放寻址。字典 dict 使用伪随机数进行探测。Java 采用链式地址。自 JDK 1.8 以来当 HashMap 内数组长度达到 64 且链表长度达到 8 时链表会转换为红黑树以提升查找性能。Go 采用链式地址。Go 规定每个桶最多存储 8 个键值对超出容量则连接一个溢出桶当溢出桶过多时会执行一次特殊的等量扩容操作以确保性能。 3 哈希算法
无论是开放寻址还是链式地址它们只能保证哈希表可以在发生冲突时正常工作而无法减少哈希冲突的发生。
对于链式地址哈希表理想情况下键值对均匀分布在各个桶中达到最佳查询效率最差情况下所有键值对都存储到同一个桶中时间复杂度退化至O(n) 。 键值对的分布情况由哈希函数决定。回忆哈希函数的计算步骤先计算哈希值再对数组长度取模
index hash(key) % capacity观察以上公式当哈希表容量 capacity 固定时哈希算法 hash() 决定了输出值进而决定了键值对在哈希表中的分布情况。
这意味着为了降低哈希冲突的发生概率我们应当将注意力集中在哈希算法 hash() 的设计上。
哈希算法的目标
为了实现“既快又稳”的哈希表数据结构哈希算法应具备以下特点。
确定性对于相同的输入哈希算法应始终产生相同的输出。这样才能确保哈希表是可靠的。效率高计算哈希值的过程应该足够快。计算开销越小哈希表的实用性越高。均匀分布哈希算法应使得键值对均匀分布在哈希表中。分布越均匀哈希冲突的概率就越低。
实际上哈希算法除了可以用于实现哈希表还广泛应用于其他领域中。
密码存储为了保护用户密码的安全系统通常不会直接存储用户的明文密码而是存储密码的哈希值。当用户输入密码时系统会对输入的密码计算哈希值然后与存储的哈希值进行比较。如果两者匹配那么密码就被视为正确。数据完整性检查数据发送方可以计算数据的哈希值并将其一同发送接收方可以重新计算接收到的数据的哈希值并与接收到的哈希值进行比较。如果两者匹配那么数据就被视为完整。
对于密码学的相关应用为了防止从哈希值推导出原始密码等逆向工程哈希算法需要具备更高等级的安全特性。
单向性无法通过哈希值反推出关于输入数据的任何信息。抗碰撞性应当极难找到两个不同的输入使得它们的哈希值相同。雪崩效应输入的微小变化应当导致输出的显著且不可预测的变化。
请注意“均匀分布”与“抗碰撞性”是两个独立的概念满足均匀分布不一定满足抗碰撞性。例如在随机输入 key 下哈希函数 key % 100 可以产生均匀分布的输出。然而该哈希算法过于简单所有后两位相等的 key 的输出都相同因此我们可以很容易地从哈希值反推出可用的 key 从而破解密码。
哈希算法的设计
哈希算法的设计是一个需要考虑许多因素的复杂问题。然而对于某些要求不高的场景我们也能设计一些简单的哈希算法。
加法哈希对输入的每个字符的 ASCII 码进行相加将得到的总和作为哈希值。乘法哈希利用乘法的不相关性每轮乘以一个常数将各个字符的 ASCII 码累积到哈希值中。异或哈希将输入数据的每个元素通过异或操作累积到一个哈希值中。旋转哈希将每个字符的 ASCII 码累积到一个哈希值中每次累积之前都会对哈希值进行旋转操作。
/* 加法哈希 */
int addHash(String key) {long hash 0;final int MODULUS 1000000007;for (char c : key.toCharArray()) {hash (hash (int) c) % MODULUS;}return (int) hash;
}/* 乘法哈希 */
int mulHash(String key) {long hash 0;final int MODULUS 1000000007;for (char c : key.toCharArray()) {hash (31 * hash (int) c) % MODULUS;}return (int) hash;
}/* 异或哈希 */
int xorHash(String key) {int hash 0;final int MODULUS 1000000007;for (char c : key.toCharArray()) {hash ^ (int) c;}return hash MODULUS;
}/* 旋转哈希 */
int rotHash(String key) {long hash 0;final int MODULUS 1000000007;for (char c : key.toCharArray()) {hash ((hash 4) ^ (hash 28) ^ (int) c) % MODULUS;}return (int) hash;
}
/* 加法哈希 */
int addHash(string key) {long long hash 0;const int MODULUS 1000000007;for (unsigned char c : key) {hash (hash (int)c) % MODULUS;}return (int)hash;
}/* 乘法哈希 */
int mulHash(string key) {long long hash 0;const int MODULUS 1000000007;for (unsigned char c : key) {hash (31 * hash (int)c) % MODULUS;}return (int)hash;
}/* 异或哈希 */
int xorHash(string key) {int hash 0;const int MODULUS 1000000007;for (unsigned char c : key) {hash ^ (int)c;}return hash MODULUS;
}/* 旋转哈希 */
int rotHash(string key) {long long hash 0;const int MODULUS 1000000007;for (unsigned char c : key) {hash ((hash 4) ^ (hash 28) ^ (int)c) % MODULUS;}return (int)hash;
}
常见哈希算法 数据结构的哈希值
我们知道哈希表的 key 可以是整数、小数或字符串等数据类型。编程语言通常会为这些数据类型提供内置的哈希算法用于计算哈希表中的桶索引。以 Python 为例我们可以调用 hash() 函数来计算各种数据类型的哈希值。
整数和布尔量的哈希值就是其本身。浮点数和字符串的哈希值计算较为复杂之后可以自学一下。元组的哈希值是对其中每一个元素进行哈希然后将这些哈希值组合起来得到单一的哈希值。对象的哈希值基于其内存地址生成。通过重写对象的哈希方法可实现基于内容生成哈希值。
不同编程语言的内置哈希值计算函数的定义和方法不同。
int num 3;
int hashNum Integer.hashCode(num);
// 整数 3 的哈希值为 3boolean bol true;
int hashBol Boolean.hashCode(bol);
// 布尔量 true 的哈希值为 1231double dec 3.14159;
int hashDec Double.hashCode(dec);
// 小数 3.14159 的哈希值为 -1340954729String str Hello 算法;
int hashStr str.hashCode();
// 字符串“Hello 算法”的哈希值为 -727081396Object[] arr { 12836, 小哈 };
int hashTup Arrays.hashCode(arr);
// 数组 [12836, 小哈] 的哈希值为 1151158ListNode obj new ListNode(0);
int hashObj obj.hashCode();
// 节点对象 utils.ListNode7dc5e7b4 的哈希值为 2110121908 总结
输入 key 哈希表能够在 O(1) 时间内查询到 value 效率非常高。常见的哈希表操作包括查询、添加键值对、删除键值对和遍历哈希表等。哈希函数将 key 映射为数组索引从而访问对应桶并获取 value 。两个不同的 key 可能在经过哈希函数后得到相同的数组索引导致查询结果出错这种现象被称为哈希冲突。哈希表容量越大哈希冲突的概率就越低。因此可以通过扩容哈希表来缓解哈希冲突。与数组扩容类似哈希表扩容操作的开销很大。负载因子定义为哈希表中元素数量除以桶数量反映了哈希冲突的严重程度常用作触发哈希表扩容的条件。链式地址通过将单个元素转化为链表将所有冲突元素存储在同一个链表中。然而链表过长会降低查询效率可以通过进一步将链表转换为红黑树来提高效率。开放寻址通过多次探测来处理哈希冲突。线性探测使用固定步长缺点是不能删除元素且容易产生聚集。多次哈希使用多个哈希函数进行探测相较线性探测更不易产生聚集但多个哈希函数增加了计算量。不同编程语言采取了不同的哈希表实现。例如Java 的 HashMap 使用链式地址而 Python 的 Dict 采用开放寻址。在哈希表中我们希望哈希算法具有确定性、高效率和均匀分布的特点。在密码学中哈希算法还应该具备抗碰撞性和雪崩效应。哈希算法通常采用大质数作为模数以最大化地保证哈希值均匀分布减少哈希冲突。常见的哈希算法包括 MD5、SHA-1、SHA-2 和 SHA-3 等。MD5 常用于校验文件完整性SHA-2 常用于安全应用与协议。编程语言通常会为数据类型提供内置哈希算法用于计算哈希表中的桶索引。通常情况下只有不可变对象是可哈希的。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/89265.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!