ハッシュ: 高速データ ストレージと検索方法の探求
効率的なデータ格納構造として、ハッシュ テーブルはデータの格納場所とキー コードの間に 1 対 1 のマッピング関係を確立できるため、要素の検索速度が向上します。ただし、ハッシュ方式はハッシュ衝突の問題にも直面します。つまり、異なるキーワードが同じハッシュ関数を通じて同じハッシュ アドレスを計算します。ハッシュの衝突をどのように処理するかが重要な問題になります。
このブログでは、ハッシュの概念、ハッシュ競合の処理、一般的なハッシュ関数の設計原則と具体的な実装方法、およびハッシュのアイデアの関連アプリケーション シナリオを紹介します。このブログを読むことで、ハッシュ テーブルの原理を深く理解し、ハッシュの衝突を解決するために適切なハッシュ関数を選択できるようになります。
ハッシュに興味がある場合、またはハッシュの衝突を解決するために適切なハッシュ関数を設計する方法を知りたい場合は、このブログが貴重な参考資料とガイダンスを提供します。一緒にハッシュの謎を探ってみましょう!
ハッシュの概念
シーケンシャル構造とバランスツリーでは、要素のキーとその格納場所の間に対応関係がないため、要素を検索する際にはキーの複数の比較を経る必要があります。逐次検索の時間計算量は O(N) であり、バランス ツリーのツリーの高さは O(log 2 N)です。検索の効率は、検索プロセス中の要素の比較の数に依存します。
理想的な検索方法:検索対象の要素を比較せずに一度にテーブルから直接取得できます。
格納構造を構築し、ある関数(hashFunc)によって要素の格納場所とキーコードが1対1の対応関係を確立できれば、検索時にこの関数により要素を素早く見つけることができます。 。
この構造に追加する場合:
- 要素の挿入 挿入する要素
のキーコードに従って、この関数を使用して要素の格納場所を計算し、この場所に従って要素を格納します- 要素の検索 要素
のキー コードに対して同じ計算を実行し、取得した関数値を要素の格納場所として使用し、構造内のこの位置に従って要素を比較します。キー コードが等しい場合、検索は成功します。この手法がハッシュ(hash)法であり、ハッシュ法で使用される変換関数をハッシュ(hash)関数と呼び、構築された構造をハッシュテーブル(またはハッシュテーブル)と呼びます。
例: データセット {1, 7, 6, 4, 5, 9};
ハッシュ関数は次のように設定されます: hash(key) = key % Capacity ; Capacity は、ストレージ要素の基礎となるスペースの合計サイズです。この方法で検索すると、キーコードを複数回比較する必要がないため、検索速度が比較的速くなります
質問: 上記のハッシュ方法によると、要素 44 がコレクションに挿入された場合、どのような問題が発生しますか?展開がないと仮定すると、hash(44)=44%10=4 であることが予測されます。この場合、4 の位置にはすでに数値が存在します。44 はどこに配置されるべきでしょうか? これにより、ハッシュの衝突が発生します。
ハッシュ衝突
2 つのデータ要素のキーの合計 (i != j) には
k_i
、!=k_j
がありますが、 Hash( ) == Hash( )、k_i
k_j
k_i
k_j
つまり、異なるキーワードが同じハッシュ番号を通じて同じハッシュ アドレスを計算します。この現象は、ハッシュ衝突またはハッシュ衝突と呼ばれます。
キーは異なるがハッシュ アドレスは同じであるデータ要素は「シノニム」と呼ばれます。
ハッシュの衝突にどう対処するか?マッピング関係をどう扱うか、つまりハッシュ関数の設計に依存します
ハッシュ関数
ハッシュ衝突の理由の 1 つは、ハッシュ関数の設計が十分に合理的ではないことである可能性があります。
ハッシュ関数の設計原則:
- ハッシュ関数のドメインには、保存する必要があるすべてのキー コードが含まれている必要があり、ハッシュ テーブルで m 個のアドレスが許可されている場合、その値の範囲は 0 ~ m-1 である必要があります。
- ハッシュ関数で計算したアドレスを空間全体に均等に分散させることができる
- ハッシュ関数は比較的単純である必要があります
一般的なハッシュ関数
直接アドレス指定方法– (一般的に使用される)
ハッシュ アドレスとしてキーワードの一次関数を使用します: ハッシュ (キー) = A*Key + B
長所: シンプル、均一、ハッシュの競合なし
短所: キーワードの分布を事前に知る必要がある状況
の使用シナリオ: 比較的小規模で継続的な状況を見つけるのに適しています剰余法– (一般的に使用されます)
ハッシュ テーブルで許可されるアドレスの数を m とし、ハッシュ関数に従って、m 以下であるが m に最も近いか等しい素数 p を除数としてとります
。 (key) = key% p (p<=m)、キーコードをハッシュアドレスに変換します。短所:ハッシュの競合が存在するため、ハッシュの競合の解決に重点を置く必要があります。
二乗法 – (理解)
キーワードが 1234 で、その二乗が 1522756 で、中央の 3 桁の 227 がハッシュ アドレスとして抽出されると仮定すると、別の例として、
キーワード 4321、その二乗が 18671041、中央の 3 桁の 227 がハッシュ アドレスとして抽出されます。ハッシュアドレスの2乗法としては、671を抽出(または710)する方が適しています。キーワードの分布が不明で、桁数があまり多くありません。折り畳み方法 – (理解)
折り畳み方法は、キーワードを左から右に同じ桁数でいくつかの部分に分割し(最後の部分は短くてもよい)、これらの部分を重ね合わせて合計し、ハッシュ テーブルに従って長くします。 , 最後の数桁をハッシュアドレスとして取得します。
折り方は、事前に知っておく必要のないキーワードの分散に適しており、キーワードが多い状況に適しています。乱数法 – (理解して)
乱数関数を選択し、キーワードの乱数関数の値をハッシュ アドレスとして取得します。つまり、H(key) = random(key) です。ここで、random は乱数関数です。
この方法は通常、キーワードの長さが等しくない場合に使用されます。数学的分析方法 - (理解)
n d 桁があり、それぞれが r 個の異なるシンボルを持つ可能性があり、各ビットに現れるこれらの r 個の異なるシンボルの頻度は必ずしも同じではなく、それらはいくつかのビットで分散および比較される可能性があります。各シンボルの出現確率は均等ですが、一部のビットでは分布が不均一で、特定のシンボルのみが頻繁に出現します。ハッシュテーブルのサイズに応じて、さまざまなシンボルが均等に分散されるビット数をハッシュ
アドレスとして選択できます。例えば:ある会社の従業員登録フォームを保存したいとします。キーワードとして携帯電話番号を使用すると、最初の 7 桁が一致する可能性が高いため、最後の 4 桁をハッシュ アドレスとして選択できます。このような抽出が簡単な場合や矛盾がある場合は、抽出した番号を逆にしたり (1234 を 4321 に変更するなど)、右のリングをずらして (1234 を 4123 に変更するなど)、左のリングをずらし、最初の 2 つを重ね合わせたりすることもできます。数字と最後の 2 つの数字 (1234 を 12+34=46 に変更するなど) やその他の方法。数値分析手法は、通常、キーワードの数が比較的多い場合、キーワードの分布が事前にわかっていて、キーワードの数ビットの分布が比較的均一である場合に適しています。 注: ハッシュ関数が
高度であればあるほど、
設計上、ハッシュ衝突の可能性が低くなりますが、ハッシュ衝突は避けられません
ハッシュ衝突の解決策
ハッシュの衝突を解決するための2 つの一般的な方法は、クローズド ハッシュとオープン ハッシュです。
クローズドハッシュ
クローズド ハッシュ: オープン アドレス指定方法とも呼ばれます。ハッシュの競合が発生した場合、ハッシュ テーブルがいっぱいではない場合、ハッシュ テーブルに空きスペースが存在する必要があり、キーはハッシュ テーブルの「下部」に格納できます。競合する位置 a" を空のスロットに入れます。では、次の空のポジションを見つけるにはどうすればよいでしょうか
?線形検出と二次検出の 2 つの方法があります。
線形検出
線形検出の概念: 競合が発生した位置から開始して、次の空の位置が見つかるまで順番に後方に探索します。
栗を取ります:
前に述べた図を例に挙げます。要素 44 を挿入する必要があります。まず、ハッシュ関数を通じてハッシュ アドレスを計算します。hashAddr は 4 なので、理論的には 44 がこの位置に挿入されるはずですが、値はすでに配置されています。この位置は 4 要素です。つまり、ハッシュの衝突が発生します。リニアプロービングをどうするか? 次の空の位置が見つかるまで、4 から逆方向にプローブします。
入れる
- ハッシュ関数によりハッシュテーブルに挿入する要素の位置を取得する
- この位置に要素がない場合は、新しい要素を直接挿入します。この位置に要素がある場合は、ハッシュ衝突が発生します。線形検出を使用して次の空の位置を見つけ、新しい要素を挿入します。
消去
クローズド ハッシュを使用してハッシュの競合を処理する場合、ハッシュ テーブル内の既存の要素を物理的に削除することはできず、要素を直接削除すると、他の要素の検索に影響が及びます。たとえば、要素 4 を削除する場合、それを直接削除すると、44 の検索が影響を受ける可能性があります。したがって、線形プローブでは、マークされた擬似削除を使用して要素を削除します。
// 哈希表每个空间给个标记 // EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除 enum State{EMPTY, EXIST, DELETE};
リニアプロービングの実装
// 注意:假如实现的哈希表中元素唯一,即key相同的元素不再进行插入 // 为了实现简单,此哈希表中我们将比较直接与元素绑定在一起 template<class K, class V> class HashTable { struct Elem { pair<K, V> _val; State _state; }; public: HashTable(size_t capacity = 3) : _ht(capacity), _size(0) { for(size_t i = 0; i < capacity; ++i) _ht[i]._state = EMPTY; } bool Insert(const pair<K, V>& val) { // 检测哈希表底层空间是否充足 // _CheckCapacity(); size_t hashAddr = HashFunc(key); // size_t startAddr = hashAddr; while(_ht[hashAddr]._state != EMPTY) { if(_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first== key) return false; hashAddr++; if(hashAddr == _ht.capacity()) hashAddr = 0; /* // 转一圈也没有找到,注意:动态哈希表,该种情况可以不用考虑,哈希表中元 素个数到达一定的数量,哈希冲突概率会增大,需要扩容来降低哈希冲突,因此哈希表中元素是 不会存满的 if(hashAddr == startAddr) return false; */ } // 插入元素 _ht[hashAddr]._state = EXIST; _ht[hashAddr]._val = val; _size++; return true; } int Find(const K& key) { size_t hashAddr = HashFunc(key); while(_ht[hashAddr]._state != EMPTY) { if(_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first== key) return hashAddr; hashAddr++; } return hashAddr; } bool Erase(const K& key) { int index = Find(key); if(-1 != index) { _ht[index]._state = DELETE; _size++; return true; } return false; } size_t Size()const; bool Empty() const; void Swap(HashTable<K, V, HF>& ht); private: size_t HashFunc(const K& key) { return key % _ht.capacity(); } private: vector<Elem> _ht; size_t _size; };
考察: ハッシュ テーブルはどのような状況で拡張されるのでしょうか? どのように拡張するか?
ハッシュ テーブルの負荷係数は、次のように定義されます。a = テーブルに埋められた要素の数 / ハッシュ テーブルの長さ
a は、ハッシュ テーブルの充実度の符号係数です。テーブルの長さは固定値であるため、a は「テーブルに埋められる要素の数」に比例するため、a が大きいほどテーブルに埋められる要素が多くなり、競合する可能性が高くなります。値が小さいほど、テーブルに記入するようにマークされている要素が少なくなり、競合が発生する可能性が低くなります。実際、ハッシュ テーブルの平均ルックアップ長は負荷係数 a の関数ですが、競合に対処する方法が異なれば機能も異なります。オープン アドレス方式の場合、負荷係数は特に重要な要素であり、0. 7 ~ 0. 8 未満に厳密に制限する必要があります。0.8 を超えると、テーブル ルックアップ時の CPU キャッシュ ミス (キャッシュ欠落) が指数関数的に増加します。このため、Java システムライブラリなど、オープンアドレッシング方式を採用した一部のハッシュライブラリでは、負荷係数が 0.75 に制限されており、この値を超えるとハッシュテーブルのサイズが変更されます。
void CheckCapacity() { if(_size * 10 / _ht.capacity() >= 7) { HashTable<K, V, HF> newHt(GetNextPrime(ht.capacity)); for(size_t i = 0; i < _ht.capacity(); ++i) { if(_ht[i]._state == EXIST) newHt.Insert(_ht[i]._val); } Swap(newHt); } }
線形検出の利点: 実装は非常に簡単です。
線形検出の欠点: ハッシュの競合が発生すると、すべての競合が結合され、データの「蓄積」が容易に生成されます。つまり、異なるキー コードが利用可能な空の位置を占有します。特定のキーコードを見つけることが困難になる の位置を何度も比較する必要があり、検索効率が低下します。それを軽減するにはどうすればよいですか?
二次検出
線形検出の欠点は、矛盾するデータが積み重なることです。これは、次の空き位置を見つけることに関係しています。空き位置を見つける方法は、次々に見つけることであるため、この問題を回避するには、2 番目の空の位置の検出方法は、
H_i
= (H_0
+i^2
)%m
またはH_i
= (H_0
-i^2
)%ですm
。=i
1,2,3...、H_0 は要素のキーコード key をハッシュ関数 Hash(x) で計算して得られる位置、m はテーブルのサイズです。
上記の場合、44 を挿入すると競合が発生します。解決策を使用した後の状況は次のとおりです。研究によると、テーブルの長さが素数でテーブルの負荷係数 a が 0.5 を超えない場合、新しいエントリを挿入する必要があり、どの位置も 2 回プローブされることはありません。したがって、テーブルに空きポジションが半分あれば、テーブルがいっぱいになるという問題は発生しません。検索時は表の満杯を無視できますが、挿入時は表の負荷率aが0.5を超えないようにする必要があり、超えた場合は容量の増加を検討する必要があります。
したがって、ハッシュ化の最大の欠点は、スペース使用率が比較的低いことであり、これはハッシュ化の欠点でもあります。それで解決策はあるのでしょうか?
オープンハッシュ
オープンハッシュの概念
オープンハッシュ方式はチェーンアドレス方式(オープンチェーン方式)とも呼ばれ、まずハッシュ関数を用いてキーコードセットのハッシュアドレスを計算し、同じアドレスを持つキーコードは同じサブセットに属します。各サブセットはバケットと呼ばれ、バケット内の要素は単一リンク リストを通じてリンクされ、各リンク リストのヘッド ノードがハッシュ テーブルに格納されます。
上の図からわかるように、オープン ハッシュの各バケットには、ハッシュ衝突のある要素が含まれています。
オープンハッシュの実装
template<class V> struct HashBucketNode { HashBucketNode(const V& data) : _pNext(nullptr), _data(data) {} HashBucketNode<V>* _pNext; V _data; }; // 本文所实现的哈希桶中key是唯一的 template<class V> class HashBucket { typedef HashBucketNode<V> Node; typedef Node* PNode; public: HashBucket(size_t capacity = 3) : _size(0) { _ht.resize(GetNextPrime(capacity), nullptr); } // 哈希桶中的元素不能重复 PNode* Insert(const V& data) { // 确认是否需要扩容。。。 // _CheckCapacity(); // 1. 计算元素所在的桶号 size_t bucketNo = HashFunc(data); // 2. 检测该元素是否在桶中 PNode pCur = _ht[bucketNo]; while(pCur) { if(pCur->_data == data) return pCur; pCur = pCur->_pNext; } // 3. 插入新元素 pCur = new Node(data); pCur->_pNext = _ht[bucketNo]; _ht[bucketNo] = pCur; _size++; return pCur; } // 删除哈希桶中为data的元素(data不会重复),返回删除元素的下一个节点 PNode* Erase(const V& data) { size_t bucketNo = HashFunc(data); PNode pCur = _ht[bucketNo]; PNode pPrev = nullptr, pRet = nullptr; while(pCur) { if(pCur->_data == data) { if(pCur == _ht[bucketNo]) _ht[bucketNo] = pCur->_pNext; else pPrev->_pNext = pCur->_pNext; pRet = pCur->_pNext; delete pCur; _size--; return pRet; } } return nullptr; } PNode* Find(const V& data); size_t Size()const; bool Empty()const; void Clear(); bool BucketCount()const; void Swap(HashBucket<V, HF>& ht); ~HashBucket(); private: size_t HashFunc(const V& data) { return data%_ht.capacity(); } private: vector<PNode*> _ht; size_t _size; // 哈希表中有效元素的个数 };
オープンハッシュ容量の増加
バケットの数は固定されています。要素を継続的に挿入すると、各バケット内の要素の数は増加し続けます。極端な場合には、バケット内に多数のリンク リスト ノードが存在し、バケットのパフォーマンスに影響を与える可能性があります。そのため、特定の条件下ではハッシュテーブルの容量を増やす必要があるのですが、その条件を確認するにはどうすればよいでしょうか?
ハッシュの最良の状況は、各ハッシュ バケットにノードが 1 つだけ存在し、要素の挿入を続けると毎回ハッシュの衝突が発生することです。したがって、要素の数がバケットの数と正確に等しい場合、次のことが可能になります。ハッシュテーブルを拡張します。
void _CheckCapacity() { size_t bucketCount = BucketCount(); if(_size == bucketCount) { HashBucket<V, HF> newHt(bucketCount); for(size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx) { PNode pCur = _ht[bucketIdx]; while(pCur) { // 将该节点从原哈希表中拆出来 _ht[bucketIdx] = pCur->_pNext; // 将该节点插入到新哈希表中 size_t bucketNo = newHt.HashFunc(pCur->_data); pCur->_pNext = newHt._ht[bucketNo]; newHt._ht[bucketNo] = pCur; pCur = _ht[bucketIdx]; } } newHt._size = _size; this->Swap(newHt); } }
オープンハッシュ思考
キーが整数である要素のみを保存できますが、他の型を解決するにはどうすればよいですか?
// 哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为整形的方法 // 整形数据不需要转化 template<class T> class DefHashF { public: size_t operator()(const T& val) { return val; } }; // key为字符串类型,需要将其转化为整形 class Str2Int { public: size_t operator()(const string& s) { const char* str = s.c_str(); unsigned int seed = 131; // 31 131 1313 13131 131313 unsigned int hash = 0; while (*str) { hash = hash * seed + (*str++); } return (hash & 0x7FFFFFFF); } }; // 为了实现简单,此哈希表中我们将比较直接与元素绑定在一起 template<class V, class HF> class HashBucket { // …… private: size_t HashFunc(const V& data) { return HF()(data.first)%_ht.capacity(); } };
残りの剰余法に加えて、素数を剰余するのが最善ですが、毎回 2 倍の関係に似た素数をすばやく取得するにはどうすればよいでしょうか?
size_t GetNextPrime(size_t prime) { const int PRIMECOUNT = 28; static const size_t primeList[PRIMECOUNT] = { 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741, 3221225473, 4294967291 }; size_t i = 0; for (; i < PRIMECOUNT; ++i) { if (primeList[i] > prime) return primeList[i]; } return primeList[i]; }
より具体的な説明については、このブログ ☞文字列ハッシュ アルゴリズムを参照してください。
オープンハッシュとクローズドハッシュの比較
オーバーフローに対処するためにチェーン アドレス方式を適用するには、リンク ポインターを追加する必要があり、ストレージのオーバーヘッドが増加するようです。
実際:
オープン アドレス方式では、検索効率を確保するために大量の空き領域を維持する必要があるため、二次検出方式では a <= 0.7 の負荷係数が必要であり、テーブル エントリが占める領域はポインタよりもはるかに大きくなります。そのため、代わりにチェーン アドレス方式を使用すると、オープン アドレス方式と比較してストレージ スペースを節約できます。
ハッシュの適用
ビットマップ
いわゆるビットマップは、各ビットを使用して特定の状態を保存するもので、大量のデータがあり、データが繰り返されないシナリオに適しています。通常、あるデータが存在するかどうかを判断するために使用されます。
ビットマップアプリケーション
アプリケーション 1
ソートされていない 40 億個の一意の符号なし整数を与えます。符号なし整数が与えられた場合、その数値が 40 億の数値の中にあるかどうかを迅速に判断する方法。【テンセント】
何をすべきか?
まずトピックを分析します。1G=1024MB=1024*1024KB=1024*1024*1024Byte は約 10 億バイトに相当し、40 億個の非繰り返し符号なし整数が占めるスペースは約 15 ~ 16G に相当します。
方法1:二分木やハッシュテーブルを検索するが、データ量が多すぎて計算量はO(n)だがメモリに格納できない
方法 2: 外部ソートを行ってからバイナリ検索を使用する 前と同様に、データが大きすぎてディスクにしか配置できないため、バイナリ検索をサポートするのは簡単ではなく、効率も低くなります。
方法 3: ビットマップ、直接値メソッド、ビットマップされたタグ値、1 は存在することを意味し、0 は存在しないことを意味します
最小バイトのデータ構造を直接使用します: char は単位です
データを位置に対応させるにはどうすればよいでしょうか?
機能を設計する
アイデア:
どの文字にあるかを知るために 8 で割って、どの位置にあるかを知るために 8 の余りを求めます。
次に、対応する位置を取りたい場合は
1
、それと等しいかそれと同じである必要があり(1<<j)
、OR 演算の原理を使用して、1 を J 個の位置だけ左に移動します。或1均为1
void set(size_t x) { size_t i=x/8; size_t j=x%8; _byt[i] | = (1<<j); }
削除するにはどうすればよいですか?
また、その位置を見つけて 0 に設定し、1 を J 個の位置だけ左に移動してから否定し、AND の性質を使用します。
void reset(size_t x) { size_t i=x/8; size_t j=x%8; _byt[i] & = ~(1<<j); }
データが存在するかどうかを確認するにはどうすればよいですか?
その場所を見つけて比較してみましょう
(1<<j)
void test(size_t x) { size_t i=x/8; size_t j=x%8; return _byt[i]&(1<<j); }
アプリケーション 2
100 億の整数が与えられた場合、1 回だけ出現する整数を見つけるアルゴリズムを設計しますか?
1 つのファイルには 100 億の整数、1G のメモリがあり、2 回以内に出現するすべての整数を見つけるアルゴリズムを設計します。
前の質問に基づいて変更を加えることができ、状態を表すために 2 ビットを使用できます。
使用できる配列は 1 つだけです
0回 -> 00
1回 -> 0 1
2回 -> 1 0
3回以上→11回
でも面倒なので配列を2つ使って1対1対応するのもいいかもしれません
コード:
template<size_t N> class bitset { public: bitset() { _bits.resize(N/8+1, 0); } void set(size_t x) { size_t i = x / 8; size_t j = x % 8; _bits[i] |= (1 << j); } void reset(size_t x) { size_t i = x / 8; size_t j = x % 8; _bits[i] &= ~(1 << j); } bool test(size_t x) { size_t i = x / 8; size_t j = x % 8; return _bits[i] & (1 << j); } private: vector<char> _bits; }; template<size_t N> class twobitset { public: void set(size_t x) { bool inset1 = _bs1.test(x);//查询是否为1 bool inset2 = _bs2.test(x); // 00 if (inset1 == false && inset2 == false) { // -> 01 _bs2.set(x); } else if (inset1 == false && inset2 == true) { // ->10 _bs1.set(x); _bs2.reset(x); } //第二个问题是同样的道理,只要加上10-》11的条件判断即可,最后需要判断不超过两次就是00和01即可 /*else if (inset1 == true && inset2 == false) { // ->11 _bs1.set(x); _bs2.set(x); }*/ } void print_once_num() { for (size_t i = 0; i < N; ++i) { if (_bs1.test(i) == false && _bs2.test(i) == true) //如果是01的状态说明只出现一次 { cout << i << endl; } } } private: bitset<N> _bs1; bitset<N> _bs2; };
アプリケーション 3
それぞれ 100 億の整数を持つ 2 つのファイルがあるとすると、メモリは 1G しかありません。2 つのファイルの共通部分を見つけるにはどうすればよいでしょうか? (だいたい)
先ほどと同じ考え方ですが、2 つの配列を設計すれば、相互のマッピングが 1 となる位置が交点になります。
ビットマップ機能
- 直接評価法を使用した高速、省スペース、競合のない
- 比較的制限があり、マッピングとシェーピングのみ
ブルームフィルター
ブルームフィルターが提案する
ニュース クライアントを使用してニュースを視聴すると、継続的に新しいコンテンツが推奨され、推奨されるたびにそれが繰り返され、すでに見たコンテンツは削除されます。ここで、ニュース クライアント レコメンデーション システムはプッシュ重複排除をどのように実現するのかという疑問が生じます。サーバーには、ユーザーが見たすべての履歴記録が記録され、レコメンデーション システムがニュースを推奨する際、各ユーザーの履歴記録がフィルタリングされ、既存の記録が除外されます。素早く見つけるにはどうすればよいでしょうか?
- ハッシュ テーブルを使用してユーザー レコードを保存します。欠点: スペースの無駄です。
- ユーザー レコードを保存するにはビットマップを使用します。欠点: ビットマップは通常、整形のみを処理できます。コンテンツ番号が文字列の場合は処理できません。
- ハッシュとビットマップの組み合わせ、つまりブルームフィルター
ブルームフィルターの概念
ブルーム フィルターは、 1970 年にバートン ハワード ブルームによって提案されたコンパクトで賢い確率的データ構造です。効率的な挿入とクエリが特徴で、「何かが存在してはいけない、または存在する可能性がある」ことを伝えるために使用でき、複数のハッシュ関数を使用します。データをビットマップ構造にマッピングします。この方法では、クエリの効率が向上するだけでなく、メモリ領域も大幅に節約できます。
ブルームフィルターの挿入
ブルームフィルターに挿入:「baidu」
このように 1 つを複数の場所に対応させると、
val
競合の可能性を減らすことができますしかし、この値をどのように計算するのでしょうか?
方法はたくさんありますが、ここでは一般的なハッシュ関数のみを紹介します
struct BKDRHash { size_t operator()(const string& s) { // BKDR size_t value = 0; for (auto ch : s) { value *= 31; value += ch; } return value; } }; struct APHash { size_t operator()(const string& s) { size_t hash = 0; for (long i = 0; i < s.size(); i++) { if ((i & 1) == 0) { hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3)); } else { hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5))); } } return hash; } }; struct DJBHash { size_t operator()(const string& s) { size_t hash = 5381; for (auto ch : s) { hash += (hash << 5) + ch; } return hash; } }; template<size_t N, size_t X = 5, class K = string, class HashFunc1 = BKDRHash, class HashFunc2 = APHash, class HashFunc3 = DJBHash> class BloomFilter { public: void Set(const K& key) { size_t len = X*N; size_t index1 = HashFunc1()(key) % len; size_t index2 = HashFunc2()(key) % len; size_t index3 = HashFunc3()(key) % len; /* cout << index1 << endl; cout << index2 << endl; cout << index3 << endl<<endl;*/ _bs.set(index1); _bs.set(index2); _bs.set(index3); } bool Test(const K& key) { size_t len = X*N; size_t index1 = HashFunc1()(key) % len; if (_bs.test(index1) == false) return false; size_t index2 = HashFunc2()(key) % len; if (_bs.test(index2) == false) return false; size_t index3 = HashFunc3()(key) % len; if (_bs.test(index3) == false) return false; return true; // 存在误判的 } // 不支持删除,删除可能会影响其他值。 void Reset(const K& key); private: bitset<X*N> _bs; };
ブルームフィルタールックアップ
ブルーム フィルターの考え方は、複数のハッシュ関数を使用して要素をビットマップにマッピングすることであるため、マッピングされた位置のビットは 1 でなければなりません。したがって、次の方法で検索できます:各ハッシュ値に対応するビット位置がゼロとして格納されているかどうかを計算します。1 つがゼロである限り、その要素はハッシュ テーブルに存在してはいけないことを意味します。ハッシュ表。
注: ブルーム フィルターで要素が存在しないと判定された場合、その要素は存在してはなりませんが、ハッシュ関数によっては誤った判断が行われる可能性があるため、要素が存在する場合は要素が存在する可能性があります。
例: ブルーム フィルターで検索する場合"alibaba"
、3 つのハッシュ関数によって計算されたハッシュ値が次であるとします。1、3、7
これは他の要素のビットと重複するだけです。このとき、ブルーム フィルターは要素が存在することを示しますが、実際には要素が存在しません。
ブルームフィルターの除去
1 つの要素が削除されると他の要素が影響を受ける可能性があるため、ブルーム フィルターは削除を直接サポートできません。
たとえば、
"tencent"
上図の要素を削除する場合、その要素に対応するバイナリ ビット位置が直接 0 である場合、“baidu”
その要素も削除されます。これは、これら 2 つの要素が複数のハッシュ関数によって計算されたビットで重複しているためです。削除をサポートする方法: ブルーム フィルターの各ビットを小さなカウンタに拡張し、要素を挿入するときに k 個のカウンタ (k 個のハッシュ関数によって計算されたハッシュ アドレス) に 1 を加算し、要素を削除するときに k 個のカウンタを 1 減算し、増加します。削除操作では、より多くのストレージ領域が占有されます。
グラフィック:
欠陥:
- 要素が実際にブルーム フィルター内にあるかどうかを確認できません
- プレゼンスカウントラップ
ブルームフィルターの利点
- 要素の追加とクエリの時間計算量は、データのサイズに関係なく、O(K) (K はハッシュ関数の数で、通常は比較的小さい) です。
- ハッシュ関数は互いに無関係なので、ハードウェアの並列処理に便利です。
- ブルーム フィルターは要素自体を保存する必要がないため、厳しい機密性要件がある場合に大きな利点があります。
- ある程度の誤った判断に耐えることができる場合、ブルーム フィルターは他のデータ構造に比べてスペース面で大きな利点があります。
- データ量が多い場合、ブルーム フィルターは完全なセットを表すことができますが、他のデータ構造では表すことができません。
- 同じハッシュ関数のセットを使用するブルーム フィルターは、交差、和集合、および差分の演算を実行できます。
ブルームフィルターの欠陥
- 偽陽性率が存在します。つまり、偽陽性 (False Position) が存在します。つまり、要素がセット内にあるかどうかを正確に判断することができません (解決策: 誤って判断される可能性のあるデータを保存するホワイト リストを作成します) )
- 要素自体を取得できません
- 通常、ブルーム フィルターから要素を削除することはできません
- 削除にカウントを使用する場合、カウントの折り返しに問題が発生する可能性があります
ブルームフィルターの適用
それぞれ 100 億のクエリを含む 2 つのファイルがある場合、メモリが 1G しかない場合、2 つのファイルの共通部分を見つけるにはどうすればよいでしょうか? 正確なアルゴリズムを与える
1Gは約10億バイトと計算してみました
各クエリが 30 バイトであると仮定すると、100 億のクエリにはどのくらいのスペースが必要でしょうか? ->3,000億バイトは約300Gに相当します
A と B という 2 つのファイルがあるとします。
ファイル A/B のクエリを順番に読み取ります。i=Hash(query)% 1000、このクエリは Ai/Bi の小さなファイルに入力されます。
次に、同じ数字の交差部分を見つけて、それらを 2 つのメモリ セットに入れることができます。
A0 と B0 が検索され、A1 と B1 が検索され、平均カットが機能しないのはなぜですか?
同じハッシュ関数が使用されるため、A と B の同じ番号が同じ番号の小さなファイルに入力される必要があります。
小さなファイルをメモリにロードできない可能性はありますか? 小さなファイルは大きすぎる可能性があります。小さなファイルに対して上記の手順を繰り返し、サブ問題として扱うことができますが、ハッシュ関数を変更する必要があります。
ハッシュ化
IP アドレスがログに保存されているサイズが 100G を超えるログ ファイルがある場合、最も多く出現する IP アドレスを見つけるアルゴリズムを設計しますか? 前の質問と同じ条件で、トップ K の IP を見つけるにはどうすればよいですか?
IP
最も回数が多いアドレスを見つけることは、前述の考え方に似ています。それぞれの を読み取り
IP
、最初に 500 個の部分に分割し、各 IP の対応するハッシュ値を計算します。i=Hash(ip)%500
この IP は最初のi
小さなファイルに入ります。キーポイント: 同じ IP のアドレスはすべて 1 つの小さなファイル内にあります
map<string,int>
小さなファイルごとに回数をカウントするには、順次使用します。
topk
、 K の値<ip,count>
を含む小さなヒープを構築します。
この記事はmdniceマルチプラットフォームによって公開されています