著者:力を渇望するアース・ドッグ
ブログのホームページ:電力に飢えたダートドッグのブログのホームページ
コラム:データ構造とアルゴリズム
労働者が良い仕事をしたいのなら、まず道具を研ぐ必要があります.大工場のオファーを勝ち取るための超素晴らしい道具を紹介しましょう—Niuke.com
目次
4.8 Conflict-Resolution-Open Hash/Hash Bucket (Key Mastery)
1. マップ&セットの紹介
1.1 コンセプトとシナリオ
マップとセットは、検索用に設計されたコンテナーまたはデータ構造です。Map と Set は、動的ルックアップに適したコレクション コンテナーです。
1.2 モデル
一般に、検索されたデータはキー (Key) と呼ばれ、対応するキーは値 (Value) と呼ばれ、これはキーと値のキーと値のペアと呼ばれるため、モデルには次の 2 種類があります。 1. 純粋なキー
モデル、例:
英語の辞書があり、単語が辞書にあるかどうかをすばやく見つけ、
名前がアドレス帳にあるかどうかをすばやく見つける2.
Key-Value モデル。
ファイル、統計結果はそれぞれ、各単語には対応する回数があります
。
2. 地図の利用
2.1 地図についての説明
Map はインターフェイス クラスです。このクラスは Collection を継承しません。このクラスは <K, V> 構造のキーと値のペアを格納します。K は一意である必要があり、繰り返すことはできません。
2.2 Map.Entry<K, V>についての説明
Map.Entry<K, V> は、Map 内部に実装され、<key, value> のキーと値のペア間のマッピング関係を格納する内部クラスです. この内部クラスは、主に <key, value> の取得、値の設定、比較の主な方法。
方法 | 説明 |
K getKey() | エントリのキーを返します |
V getValue() | エントリの値を返します |
V setValue(V値) | キーと値のペアの値を指定された値に置き換えます |
注: Map.Entry<K,V> は Key を設定するメソッドを提供しません
2.3 Map の共通メソッドの説明
方法 | 説明 |
V get(オブジェクトキー) | キーに対応する値を返す |
V getOrDefault(オブジェクトキー, V defaultValue) | キーに対応する値を返し、キーが存在しない場合はデフォルト値を返します |
V put(Kキー、V値) | キーに対応する値を設定します |
V remove(オブジェクトキー) | キーに対応するマッピング関係を削除します |
Set<K> keySet() | すべてのキーの一意のセットを返します |
Collection<V> 値() | すべての値の反復可能なコレクションを返します |
Set<Map.Entry<K, V>> entrySet() | すべてのキーと値のマッピングを返す |
boolean containsKey(オブジェクトキー) | キーを含めるかどうかを決定する |
boolean containsValue(オブジェクト値) | 値を含めるかどうかを決定する |
注:
1. Map はインターフェースであり、オブジェクトを直接インスタンス化することはできません. オブジェクトをインスタンス化する場合は、その実装クラス TreeMap または HashMap のみをインスタンス化できます 2. Map にキーと値のペアを格納するキーは一意です
.値は
繰り返すことができます3 . マップ内のキーはすべて分離して、アクセスのためにセットに格納できます (キーを繰り返すことはできないため)。
4. マップ内のすべての値を分離して、コレクションの任意のサブコレクションに格納できます (値は重複する可能性があります)。
5. マップ内のキーと値のペアのキーを直接変更することはできませんが、値は変更できます. キーを変更する場合は、最初にキーを削除してから再挿入することしかできません
.
6. TreeMap と HashMap の違い
基礎となる構造をマッピングする | ツリーマップ | ハッシュマップ |
基礎構造 | 赤黒い木 | ハッシュバケット |
挿入/削除/検索時間の 複雑さ |
O(1) | |
順調ですか | キーオーダーについて | 故障中 |
スレッドセーフ | 安全でない | 安全でない |
挿入/削除/違いを見つける | 要素比較が必要です | ハッシュ関数を使用してハッシュ アドレスを計算する |
比較して上書きする | キーは同等である必要があります。そうでない場合、 ClassCastExceptionがスローされます |
カスタム型は equals メソッドと hashCode メソッドをオーバーライドする必要があります |
適用シナリオ | 鍵の注文が必要なシーンに | キーが整っているかどうかは関係なく、より高い 時間パフォーマンスが必要です |
3. セット内容
Set と Map には主に 2 つの違いがあります。Set は Collection から継承されたインターフェイス クラスであり、Set には Key のみが格納されます。
3.1 一般的な方法の説明
方法 | 説明 |
boolean add(E e) | 要素を追加しますが、重複する要素は正常に追加されません |
ボイドクリア() | 空のコレクション |
boolean contains(オブジェクト o) | o がセット内にあるかどうかを判別する |
Iterator<E> イテレータ() | 反復子を返す |
boolean remove(Object o) | セットから o を削除します |
整数サイズ() | セット内の要素の数を返します |
ブール値 isEmpty() | セットが空かどうかを確認し、空の場合は true を返し、そうでない場合は false を返します |
Object[] toArray() | セット内の要素を配列に変換して返す |
boolean containsAll(Collection<?> c) | セット c のすべての要素がセット内に存在するかどうかは true を返し、そうでない場合は falseを返します。 |
boolean addAll(Collection<? extends E> c) |
セット c の要素をセットに追加すると、重複排除の効果が得られます |
注:
1. Set は Collection から継承されたインターフェイス クラスです
2. Set にはキーのみが格納され、キーは一意である必要があります
3. Set の基になるレイヤーは Map を使用して実装されます。キーと値のペアが Map に挿入されます.
4. Set の最大の機能は、セット内の要素を重複排除することです.
5. Set インターフェイスを実装する一般的なクラスは TreeSet と HashSet で、LinkedHashSet もあります. LinkedHashSet は
要素の挿入順序を記録するための HashSet 双方向リンク リストに基づいて維持されます。
6. セット内のキーは変更できません。変更する場合は、最初に元のキーを削除してから、再度挿入してください。
7. セット内にヌル キーを挿入することはできません。
8. TreeSet と HashSet の違い
基礎となる構造を設定する | ツリーセット | ハッシュセット |
基礎構造 | 赤黒い木 | ハッシュバケット |
挿入/削除/検索時間の 複雑さ |
O(1) | |
順調ですか | キーオーダーについて | 必ずしも整然としているわけではない |
スレッドセーフ | 安全でない | 安全でない |
挿入/削除/違いを見つける | 按照红黑树的特性来进行插入和删除 | 1. 先计算key哈希地址 2. 然后进行 插入和删除 |
比较与覆写 | key必须能够比较,否则会抛出 ClassCastException异常 |
自定义类型需要覆写equals和 hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的 时间性能 |
4. 哈希表
4.1 概念
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:插入元素,根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。搜索元素,对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快,但是如果数字的余数相同的时候就容易产生冲突(都在同一个位置了)
4.2 冲突-概念
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
4.3 冲突-避免
首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率。
4.4冲突-避免的方法
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
所以有几种常见的设计哈希函数的方法:
直接定制法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况 面试题:字符串中第一个只出现一次字符
除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
还有几种不常用的方法也列举一下:平方取中法,折叠法,随机数法,数学分析法。
4.5 冲突-避免-负载因子调节(重点掌握)
负载因子和冲突率的关系粗略演示
所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。
4.6 冲突-解决
解决哈希冲突两种常见的方法是:闭散列和开散列
4.7 冲突-解决-闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
存放到下一个位置有两种方式比较常用:
1. 线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他
元素的搜索。
2. 二次探测
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
4.8 冲突-解决-开散列/哈希桶(重点掌握)
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
链地址法的图示:当有哈希桶在某个下标下,则哈希表对应下标的值为链表头结点的地址,如果没有则为null。每个结点都由key,value与next组成。
因为是用哈希桶来解决冲突,因此要创建一个每个下标底下都是结点组成的链表的数组Node[] array;,并且用size来记录数组中结点的个数,目的是求出负载因子。而每个哈希桶都是由结点组成的,因此结点类可以定义到哈希桶类里面。put、resize方法:先根据设计的哈希函数求出对应的下标,找到对应下标后去遍历该下标的链表,如果已经有相同的key值的结点,则更新完key值中的value后退出。如果没有,则创建一个哈希桶,设置cur结点从头结点开始,用头插法插入新的结点。当然,插入后要计算出负载因子是否超过了0.75,如果超过就扩容。扩容后数组的长度会变长,因此每个哈希桶根据新的哈希函数的计算结果跟原来的不同,则要重新哈希。(resize方法)等全部哈希桶重新哈希完之后,新的数组的引用要赋值给旧的数组的引用。
get方法:首先要根据哈希函数求出对应的下标,再从下标当中去找是否有相同的值的哈希桶,若有,则返回该哈希桶的value值,否则返回-1。
实现代码:
// key-value 模型
public class HashBucket {
private static class Node {
private int key;
private int value;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Node[] array;
public HashBucket() {
// write code here
array=new Node[DEFAULT_SIZE];
}
private int size; // 当前的数据个数
private static final double LOAD_FACTOR = 0.75;
private static final int DEFAULT_SIZE = 8;//默认桶的大小
public void put(int key, int value) {
// write code here
//获取下标
int index=key%array.length;
Node cur=array[index];
//找位置,如果有相同的就覆盖
while (cur!=null){
if(cur.key==key){
cur.value=value;
return;
}
cur=cur.next;
}
//进行插入(头插)
Node head=new Node(key,value);
head.next=array[index];
array[index]=head;
size++;
//负载因子大于0.75时扩容
if(loadFactor()>=0.75){
resize();
}
}
private void resize() {
// write code here
Node[]newArray=new Node[2*array.length];
//对原array中的每一个数据进行重新哈希
for(int i=0;i<array.length;i++){
Node cur=array[i];
while(cur!=null){
Node curNext=cur.next; //保存后续结点
int newIndex=cur.key%newArray.length;
cur.next=newArray[newIndex];
newArray[newIndex]=cur;
cur=curNext;
}
}
array=newArray;//重新赋值
}
private double loadFactor() {
return size * 1.0 / array.length;
}
public int get(int key) {
// write code here
int index=key%array.length;
Node cur=array[index];
//找位置
while (cur!=null){
if(cur.key==key){
return cur.value;
}
cur=cur.next;
}
return -1;
}
}
虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是O(1) 。
一些要注意的点:
1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
2. java 中使用的是哈希桶方式解决冲突的
3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的
5.哈希表查找成功与查找失败的求法
5.1 查找成功的求法
一个哈希表如下图:
假设现在要查找4,则根据哈希函数Hash(key)=key%array.length,找到了4下标,因为4下标下的数据就是4,则查找一次就成功,1和9数据的查找成功次数求法也是一致。
假设此时要查找24,则先根据哈希函数求出下标为4,但是4下标的数据是4,而不是24,算查找一次。此时往后移一个单位,发现是44不是我们查找的24,则算查找第二次。此时再往后移一个单位,发现是24,算是查找的次数是第三次。因此上图中查找成功的平均查找长度:(1+1+2+3+1)/5=8/5。
如果找9下标的值查找的不是要查找的数据,则返回到0下标开始找,并且找9下标失败后算查找1次。
5.2 查找失败的求法
一个哈希表如下图:
第一次假如找数据1,则根据哈希函数Hash(key)=key%array.length求得下标为1,假设下标1中的数据不是我们找的数据1,算查找失败了一次,则向后移一个单位去找。此时下标2的值为空,则说明查找失败,算查找失败了两次。
この時点でデータ 44 を探している場合、ハッシュ関数によると添字は 4 ですが、この時点で添字 4 の下のデータが 44 ではない場合、検索が成功する保証はありません。そこで、1単位戻って検索すると、添え字5のデータが44個であることがわかり、ここから検索失敗回数のカウントが開始されます。現在の 5 つの添字の検索が失敗したと仮定する必要があります。検索が 1 回失敗した場合は、1 ユニット戻って検索します。同じように、添字 6 の値が 44 ではなく、検索が失敗したと仮定すると、1 単位戻って検索します。添字 7 に達した後、添字のデータが空であることが判明した場合、検索は失敗したに違いなく、今回は検索としてカウントされるため、検索失敗の長さは合計で 3 回になります。
注: 失敗を見つける操作は、実際に見つかった値から逆方向に次の空の場所を見つけることです。