比較マップ・セット
map/set の違いを比較してください:
1. map と set のトラバーサルは順序付けられ、順序付けられていないシリーズは無秩序です.
2. Map と set は双方向の双方向反復子です.
上記の比較に基づいて、順序付けられていないシリーズは一方向です. . map/set と比べてより強力なのに、なぜ順序付けられていないシリーズがあるのですか?
回答: 大量のデータがある場合、追加、削除、チェック、および変更の効率が向上します。
#include<iostream>
#include<vector>
#include<set>
#include<unordered_set>
#include<time.h>
using namespace std;
void test_op()
{
int n = 100000; // 通过改变数据量,可查看两者的区别
vector<int> v;
v.reserve(n);
srand(time(0));
for (int i = 0; i < n; ++i)
{
//v.push_back(i);
//v.push_back(rand() + i); // 重复少
v.push_back(rand()); // 重复多
}
size_t begin1 = clock();
set<int> s;
for (auto e : v)
{
s.insert(e);
}
size_t end1 = clock();
size_t begin2 = clock();
unordered_set<int> us;
for (auto e : v)
{
us.insert(e);
}
size_t end2 = clock();
cout << "size:" << s.size() << endl;
cout << "set insert:" << end1 - begin1 << endl;
cout << "unordered_set insert:" << end2 - begin2 << endl;
size_t begin3 = clock();
for (auto e : v)
{
s.find(e);
}
size_t end3 = clock();
size_t begin4 = clock();
for (auto e : v)
{
us.find(e);
}
size_t end4 = clock();
cout << "set find:" << end3 - begin3 << endl;
cout << "unordered_set find:" << end4 - begin4 << endl;
size_t begin5 = clock();
for (auto e : v)
{
s.erase(e);
}
size_t end5 = clock();
size_t begin6 = clock();
for (auto e : v)
{
us.erase(e);
}
size_t end6 = clock();
cout << "set erase:" << end5 - begin5 << endl;
cout << "unordered_set erase:" << end6 - begin6 << endl;
}
int main()
{
test_op();
return 0;
}
1.連想コンテナの順序付けられていないシリーズ
C++98 では、STL は最下層が赤黒のツリー構造である一連の連想コンテナーを提供し、クエリ効率はlog 2 N log_2Nに達する可能性があります。ログ_ _2N , つまり、最悪の場合、赤黒木の高さの時間を比較する必要があります. 木に多くのノードがある場合、クエリの効率は理想的ではありません. 最良のクエリは, 少数の比較で要素を見つけることです. したがって, C++11 では, STL は連想コンテナの順序付けされていない 4 つのシリーズを提供します. これらの 4 つのコンテナと赤黒ツリー構造の間の関連性 タイプの使用メソッドコンテナは基本的に似ていますが、その下の構造が異なります. この記事では、unordered_map と unordered_set のみを紹介します. unordered_multimap と unordered_multiset については、ドキュメントの紹介を参照してください.
1.1 unordered_map
- unordered_map は、<key, value> キーと値のペアを格納する連想コンテナーです。これにより、キーを使用して対応する値に高速にインデックスを付けることができます。
- unordered_map では、キー値は通常、要素を一意に識別するために使用され、マップ値はコンテンツがこのキーに関連付けられているオブジェクトです。キーとマップ値は異なるタイプの場合があります。
- 内部的には、unordered_map は特定の順序で <kye, value> をソートしません.一定の範囲内でキーに対応する値を見つけるために、unordered_map は同じハッシュ値を持つキーと値のペアを同じバケットに入れます.
- unordered_map コンテナーは、キーによって個々の要素にアクセスするためのマップよりも高速ですが、一般に、要素のサブセットに対する範囲反復では効率が低下します。
- unordered_maps は直接アクセス演算子 (operator[]) を実装します。これにより、キーを引数として使用して値に直接アクセスできます。
- その反復子は、少なくとも前方反復子です。
operator[]
// 返回与key对应的value,没有一个默认值
// 该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,
// 如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,将key对应的value返回。
size_t count(const K& key)
// size_t count(const K& key)
// unordered_map中key是不能重复的,因此count函数的返回值最大为1
size_t bucket_count()const
// 返回哈希桶中桶的总个
size_t bucket_size(size_t n)const
// 返回n号桶中有效元素的总个数
size_t bucket(const K& key)
// 返回元素key所在的桶号
2. 基礎構造
順不同の一連の連想コンテナーがより効率的である理由は、下層のレイヤーがハッシュ構造を使用しているためです。
2.1 ハッシュの概念
逐次構造と平衡木では、要素のキーとその格納場所に対応関係がないため、要素を探すときはキーを複数回比較する必要があります。逐次探索の計算量は O(N) であり、平衡木における木の高さは O( log 2 N log_2 Nログ_ _2N )、検索の効率は、検索中の要素の比較の数に依存します。
理想的な検索方法:検索対象の要素は、テーブルから一度に比較なしで直接取得できます。
格納構造が構築されていて、要素の格納場所とそのキーコードが特定の関数 (hashFunc) によって 1 対 1 のマッピング関係を確立できる場合、検索時にこの関数によって要素をすばやく見つけることができます。 .
この構造に追加する場合:
- 要素
を挿入する 挿入する要素のキー コードに従って、この関数を使用して要素の格納場所を計算し、この場所に従って格納します (負の数を考慮する必要はありません。挿入されるすべての値はハッシュ関数によって符号なし整数として処理されます) その場所を計算し、キーを挿入します) - 要素の検索 要素
のキー コードに対して同じ計算を実行し、得られた関数値を要素の格納場所として使用し、構造内のこの位置に従って要素を比較し、キー コードが等しい場合、検索は成功です。
この方式をハッシュ(ハッシュ)方式、ハッシュ方式で使用する変換関数をハッシュ(ハッシュ)関数、構築した構造をハッシュテーブル(またはハッシュテーブル)と呼びます。
- 例: データセット {1, 7, 6, 4, 5, 9};
ハッシュ関数は次のように設定されます: hash(key) = key % capacity ; capacity は、ストレージ要素の基礎となるスペースの合計サイズです。
この方法で検索すると、キーコードの多重比較が不要なため、検索速度が比較的速い
質問: 上記のハッシュ法によると、要素 44 をコレクションに挿入すると、どのような問題が発生しますか?
2.2 ハッシュ衝突
- 異なる値は同じ場所にマップされます
2 つのデータ要素のキーki k_iについてk私およびkj k_jkじ(i != j),有 k i k_i k私!= kj k_jkじ、しかしある: Hash( ki k_ik私) ==ハッシュ( kj k_jkじ)、つまり、異なるキーワードが同じハッシュ番号を介して同じハッシュアドレスを計算します。この現象は、ハッシュ衝突またはハッシュ衝突と呼ばれます。キーが異なるがハッシュアドレスが同じデータ要素は「シノニム」と呼ばれます。
ハッシュの衝突にどう対処するか?
2.3 ハッシュ関数
ハッシュ衝突の理由の 1 つは、ハッシュ関数の設計が十分に合理的でないことです。
ハッシュ関数の設計原則:
- ハッシュ関数のドメインには、格納する必要があるすべてのキー コードが含まれている必要があります。ハッシュ テーブルで m 個のアドレスが許可されている場合、値の範囲は 0 から m-1 の間である必要があります。
- ハッシュ関数によって計算されたアドレスは、空間全体に均等に分散できます
- ハッシュ関数は比較的単純である必要があります
一般的なハッシュ関数
2.4 ハッシュ競合の解決
ハッシュ衝突を解決する 2 つの一般的な方法は次のとおりです。
-
クローズド ハッシング – オープン アドレッシング
a. 線形プロービング
b. 二次プロービング -
オープンハッシュ – ジッパー/ハッシュバケット
2.4.1 クローズドハッシュ
クローズドハッシュ: オープンアドレッシング方式とも呼ばれ、ハッシュの競合が発生した場合、ハッシュテーブルがいっぱいでない場合、ハッシュテーブルに空きスペースが必要であることを意味し、キーを「下部」に格納できます競合する位置 a" を空のスロットに移動します。では、次の空の位置を見つけるにはどうすればよいでしょうか?
-
線形検出: 競合が発生した位置から開始し、次の空の位置が見つかるまで逆方向にプローブします。
-
挿入
1. ハッシュ関数を介してハッシュ テーブルに挿入する要素の位置を取得します
2. その位置に要素がない場合は、新しい要素を直接挿入します その位置にハッシュの衝突がある場合は、線形検出を使用します次の空白の位置を見つけるには、新しい要素を挿入します
-
消去
クローズド ハッシュを使用してハッシュの競合を処理する場合、ハッシュ テーブル内の既存の要素を物理的に簡単に削除することはできません. 要素を直接削除すると、他の要素の検索に影響します. たとえば、要素 4 を削除する場合、それを直接削除すると、44 のルックアップが影響を受ける可能性があります。そのため、線形プローブでは、マーク付き疑似削除を使用して要素を削除します。
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{
EMPTY, EXIST, DELETE};
線形プロービングと二次プロービング
- 線形検出の利点: 実装が非常に簡単 – hashi = key % size(), hashi = hashi + 1; (++hashi)
- 線形検出の短所: ハッシュの競合が発生すると、すべての競合が結合され、データの「パイルアップ」が発生しやすくなります。つまり、異なるキーが使用可能な空の位置を占有するため、ハッシュの位置を見つけるために多くの比較が必要になります。検索効率が低下します。軽減する方法は? (二次検出)
- 二次検出 – hashi = key % size(), hashi = hashi + i * i;
線形検出の欠点は、競合するデータが一緒に蓄積されることです。これは、次の空の位置を見つけることと関係があります。空き位置は 1 つずつ検索するため、2 回目の検出でこの問題を回避するために、次の空き位置を見つける方法は次のとおりです。H私= ( H 0 H_0H0+ i 2 i^2私2 )% m、または:H i H_iH私= ( H 0 H_0H0- i 2 i^2私2 )% m. ここで: i =1,2,3...,H 0 H_0H0は要素のキーコードキーをハッシュ関数 Hash(x) で計算した位置、m はテーブルのサイズです。 - 調査によると、テーブルの長さが素数で、テーブルの負荷係数 a が 0.5 を超えない場合、新しいエントリを挿入する必要があり、任意の位置が 2 回プローブされることはありません。したがって、テーブルに空いているポジションの半分があれば、テーブルがいっぱいになっても問題はありません。検索時はテーブルの満杯を無視できますが、挿入時はテーブルの負荷率aが0.5を超えないようにする必要があり、超える場合は容量を増やすことを検討する必要があります。
- したがって、ハッシュの最大の欠点は、スペース使用率が比較的低いことです。これは、ハッシュの欠点でもあります。
膨張率(負荷率)
ハッシュ テーブルを展開する必要があるのはどのような状況ですか? 展開する方法は?
クローズドハッシュによって実装されたハッシュ
namespace CloseHash
{
enum State
{
EMPTY,
EXIST,
DELETE
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
/
// key 映射 需要模(%)一下; 必须是一个整形 像 int char 指针 强转一下就可以了
// 但是 像string 这样的该如何转为无符号整形呢?
// unordered_set给我们提供了一个接口,可以传一个仿函数进去, 用于将key转为无符号整形
// 下面有两个仿函数,对于string会走特化的仿函数
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 特化 (用了字符串哈希算法)
template<>
struct HashFunc<string>
{
// BKDR
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
/*
下面的方法是将字符串每个字符相加得到一个数值,但是重复的概率还是太高
比如"abab" "aabb" "bbaa"数值是一样的,所以就用到了上面的字符串哈希算法
*/
//struct HashFuncString
//{
// size_t operator()(const string& key)
// {
// size_t val = 0;
// for (auto ch : key)
// {
// val += ch;
// }
//
// return val;
// }
//};
/
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
bool Insert(const pair<K, V>& kv)
{
// 如果存在就不再插入
if (Find(kv.first))
return false;
// 负载因子到了就扩容
if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 扩容
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2; // 注意起始size为0
HashTable<K, V, Hash> newHT;
newHT._tables.resize(newSize);
// 旧表的数据映射到新表
//复用inser,因为扩容后不会再进入扩容里面,会直接走下面的代码,进行探测插入
for (auto e : _tables)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
// 交换,newT为局部变量,会自己被销毁(不需要写析构,会去调用自定义类型vector自己的析构函数)
_tables.swap(newHT._tables);
}
// 调用hash,将key转为整形:hash(kv.first)
Hash hash;
size_t hashi = hash(kv.first) % _tables.size();
// 线性探测
while (_tables[hashi]._state == EXIST)
{
hashi++;
hashi %= _tables.size(); // 从hashi后面走完,前面也要探测
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_size;
//Hash hash;
//size_t start = hash(kv.first) % _tables.size();
//size_t i = 0;
//size_t hashi = start;
二次探测 -- 前面是一次再走一步, 这里走 前一步的平方步
//while (_tables[hashi]._state == EXIST)
//{
// ++i;
// hashi = start + i*i;
// hashi %= _tables.size();
//}
//_tables[hashi]._kv = kv;
//_tables[hashi]._state = EXIST;
//++_size;
return true;
}
HashData<K, V>* Find(const K& key)
{
// 注意开始要判断size,表为空时的情况
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t start = hash(key) % _tables.size();
size_t hashi = start;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
hashi++;
hashi %= _tables.size();
// Find()这里有一个极端情况,当不断进行插入和删除时,表中可能全为DELETE,这时会造成死循环
// 当从hashi 开始 完后走 当又走到 hashi,代表走完了一圈 没有找到 结束循环
if (hashi == start)
{
break;
}
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_size;
return true;
}
else
{
return false;
}
}
void Print()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._state == EXIST)
{
printf("[%d:%d] ", i, _tables[i]._kv.first);
}
else
{
printf("[%d:*] ", i);
}
}
cout << endl;
}
private:
//vector<pair<K, V>> _table;
vector<HashData<K, V>> _tables;
size_t _size = 0; // 存储多少个有效数据
};
void TestHT1()
{
//int a[] = { 1, 11, 4, 15, 26, 7, 44, 9 };
int a[] = {
1, 11, 4, 15, 26, 7, 44 };
HashTable<int, int> ht;
for (auto e : a)
{
ht.Insert(make_pair(e, e));
}
ht.Print();
ht.Erase(4);
cout << ht.Find(44)->_kv.first << endl;
cout << ht.Find(4) << endl;
ht.Print();
ht.Insert(make_pair(-2, -2));
ht.Print();
cout << ht.Find(-2)->_kv.first << endl;
}
void TestHT2()
{
string arr[] = {
"苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
//HashTable<string, int, HashFuncString> countHT;
HashTable<string, int> countHT;
for (auto& str : arr)
{
auto ptr = countHT.Find(str);
if (ptr)
{
ptr->_kv.second++;
}
else
{
countHT.Insert(make_pair(str, 1));
}
}
}
void TestHT3()
{
HashFunc<string> hash;
cout << hash("abcd") << endl;
cout << hash("bcad") << endl;
cout << hash("eat") << endl;
cout << hash("ate") << endl;
cout << hash("abcd") << endl;
cout << hash("aadd") << endl << endl;
cout << hash("abcd") << endl;
cout << hash("bcad") << endl;
cout << hash("eat") << endl;
cout << hash("ate") << endl;
cout << hash("abcd") << endl;
cout << hash("aadd") << endl << endl;
}
}
2.4.2 オープンハッシュ
コンセプト
オープン ハッシュ方式は、チェーン アドレス方式とも呼ばれます (オープン チェーン方式) まず、ハッシュ関数を使用してキー コード セットのハッシュ アドレスを計算し、同じアドレスを持つキー コードは同じサブセットに属します。各サブセットはバケットと呼ばれ、バケット内の要素は単一リンク リストを介してリンクされ、各リンク リストのヘッド ノードはハッシュ テーブルに格納されます。
上の図からわかるように、開いているハッシュの各バケットには、ハッシュの衝突を持つ要素が含まれています。
オープンハッシュ思考
- ハッシュ拡張
バケットの数は固定されています. 要素を連続的に挿入すると, 各バケット内の要素の数は増加し続けます. 極端な場合, バケット内に多くの連結リストノードが発生する可能性があります.ギリシア表の性能低下のため、特定の条件下ではハッシュ表の容量を増やす必要がありますが、その条件を確認するにはどうすればよいですか? ハッシュの最適な状況は: 各ハッシュ バケットにちょうど 1 つのノードがあり、要素を挿入し続けると、毎回ハッシュの衝突が発生します. したがって、要素の数がバケットの数と正確に等しい場合、次
のことができます。ハッシュテーブルの容量を増やしてください。 - キーが整数である要素のみを格納できます。他の型を解決するにはどうすればよいですか?
文字列ハッシュ アルゴリズム - 剰余法に加えて、素数を法とするのが一番ですが、毎回倍増関係に似た素数を素早く得るにはどうすればよいでしょうか?
コードの実装を見てください。素数配列であることが示されています - オープン ハッシュとクローズド ハッシュの比較
チェーン アドレス方式を適用してオーバーフローを処理するには、リンク ポインターを追加する必要があり、ストレージのオーバーヘッドが増加するようです。実際: オープン アドレス方式では、検索効率を確保するために大量の空き領域を維持する必要があるため、2 次検出方式では負荷係数 a <= 0.7 が必要であり、テーブル エントリが占める領域はポインタよりもはるかに大きくなります。オープンアドレス方式と比較して、ストレージスペースを節約できます。
達成
namespace Bucket
{
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
// 开散列: 需要写析构,释放每个桶里面的链表
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
// 扩容, 通常是每次是上次的2倍
// 这里是调用一个函数,每次扩容都是素数个,这样可以减少哈希冲突
inline size_t __stl_next_prime(size_t n)
{
static const size_t __stl_num_primes = 28;
static const size_t __stl_prime_list[__stl_num_primes] =
{
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
};
for (size_t i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return -1;
}
bool Insert(const pair<K, V>& kv)
{
// 去重
if (Find(kv.first))
{
return false;
}
Hash hash;
// 负载因子到1就扩容
if (_size == _tables.size())
{
//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newTables;
//newTables.resize(newSize, nullptr);
newTables.resize(__stl_next_prime(_tables.size()), nullptr);
// 旧表中节点移动映射新表
// 这里没有用闭散列的方式,建立一个新的表进行插入,而是建立新表后复用原来的节点
for (size_t i = 0; i < _tables.size(); ++i)
{
// 遍历旧表的每个节点,重新计算映射到新表
Node* cur = _tables[i];
while (cur)
{
// 需要先保留节点的下一个
Node* next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newTables.size();
// 头插
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
// 将原来的旧表置空
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hash(kv.first) % _tables.size();
// 头插
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return true;
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
// 1、头删
// 2、中间删
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
size_t Size()
{
return _size;
}
// 表的长度
size_t TablesSize()
{
return _tables.size();
}
// 桶的个数
size_t BucketNum()
{
size_t num = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
++num;
}
}
return num;
}
size_t MaxBucketLenth()
{
size_t maxLen = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
size_t len = 0;
Node* cur = _tables[i];
while (cur)
{
++len;
cur = cur->_next;
}
//if (len > 0)
//printf("[%d]号桶长度:%d\n", i, len);
if (len > maxLen)
{
maxLen = len;
}
}
return maxLen;
}
private:
vector<Node*> _tables;
size_t _size = 0; // 存储有效数据个数
};
void TestHT1()
{
int a[] = {
1, 11, 4, 15, 26, 7, 44, 55, 99, 78 };
HashTable<int, int> ht;
for (auto e : a)
{
ht.Insert(make_pair(e, e));
}
ht.Insert(make_pair(22, 22));
ht.Erase(44);
ht.Erase(4);
}
void TestHT2()
{
string arr[] = {
"苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
//HashTable<string, int, HashFuncString> countHT;
HashTable<string, int> countHT;
for (auto& str : arr)
{
auto ptr = countHT.Find(str);
if (ptr)
{
ptr->_kv.second++;
}
else
{
countHT.Insert(make_pair(str, 1));
}
}
}
void TestHT3()
{
int n = 19000000;
vector<int> v;
v.reserve(n);
srand(time(0));
for (int i = 0; i < n; ++i)
{
//v.push_back(i);
v.push_back(rand() + i); // 重复少
//v.push_back(rand()); // 重复多
}
size_t begin1 = clock();
HashTable<int, int> ht;
for (auto e : v)
{
ht.Insert(make_pair(e, e));
}
size_t end1 = clock();
cout << "数据个数:" << ht.Size() << endl;
cout << "表的长度:" << ht.TablesSize() << endl;
cout << "桶的个数:" << ht.BucketNum() << endl;
cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;
cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
cout << "负载因子:" << (double)ht.Size() / (double)ht.TablesSize() << endl;
}
}
シミュレーションの実装
テンプレート パラメータ リストの変更
2.イテレータ操作を増やす
変更されたハッシュ コード
namespace Bucket
{
节点
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{
}
};
/ 迭代器 /
// 前置声明
template<class K, class T, class Hash, class KeyOfT>
class HashTable;
template<class K, class T, class Hash, class KeyOfT>
struct __HashIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HT;
typedef __HashIterator<K, T, Hash, KeyOfT> Self;
Node* _node;
HT* _pht;
__HashIterator(Node* node, HT* pht)
:_node(node)
, _pht(pht)
{
}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
Self& operator++()
{
if (_node->_next)
{
// 当前桶中迭代
_node = _node->_next;
}
else
{
// 找下一个桶
Hash hash;
KeyOfT kot;
size_t i = hash(kot(_node->_data)) % _pht->_tables.size();
++i;
for (; i < _pht->_tables.size(); ++i)
{
if (_pht->_tables[i])
{
_node = _pht->_tables[i];
break;
}
}
// 说明后面没有有数据的桶了
if (i == _pht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
};
hash //
template<class K, class T, class Hash, class KeyOfT>
class HashTable
{
typedef HashNode<T> Node;
template<class K, class T, class Hash, class KeyOfT>
friend struct __HashIterator;
public:
typedef __HashIterator<K, T, Hash, KeyOfT> iterator;
iterator begin()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
return iterator(_tables[i], this);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
inline size_t __stl_next_prime(size_t n)
{
static const size_t __stl_num_primes = 28;
static const size_t __stl_prime_list[__stl_num_primes] =
{
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
};
for (size_t i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return -1;
}
pair<iterator, bool> Insert(const T& data)
{
Hash hash;
KeyOfT kot;
// 去重
iterator ret = Find(kot(data));
if (ret != end())
{
return make_pair(ret, false);
}
// 负载因子到1就扩容
if (_size == _tables.size())
{
//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newTables;
//newTables.resize(newSize, nullptr);
newTables.resize(__stl_next_prime(_tables.size()), nullptr);
// 旧表中节点移动映射新表
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(kot(cur->_data)) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hash(kot(data)) % _tables.size();
// 头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return make_pair(iterator(newnode, this), true);
}
iterator Find(const K& key)
{
if (_tables.size() == 0)
{
return end();
}
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur, this);
}
cur = cur->_next;
}
return end();
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
// 1、头删
// 2、中间删
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
size_t Size()
{
return _size;
}
// 表的长度
size_t TablesSize()
{
return _tables.size();
}
// 桶的个数
size_t BucketNum()
{
size_t num = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
++num;
}
}
return num;
}
size_t MaxBucketLenth()
{
size_t maxLen = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
size_t len = 0;
Node* cur = _tables[i];
while (cur)
{
++len;
cur = cur->_next;
}
//if (len > 0)
//printf("[%d]号桶长度:%d\n", i, len);
if (len > maxLen)
{
maxLen = len;
}
}
return maxLen;
}
private:
vector<Node*> _tables;
size_t _size = 0; // 存储有效数据个数
};
}
unordered_map
namespace sz
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename Bucket::HashTable<K, pair<K, V>, Hash, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair<iterator, bool> Insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
private:
Bucket::HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
};
void test_map()
{
unordered_map<string, string> dict;
dict.Insert(make_pair("sort", ""));
dict.Insert(make_pair("string", "ַ"));
dict.Insert(make_pair("left", ""));
unordered_map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
unordered_map<string, int> countMap;
string arr[] = {
"ƻ", "", "ƻ", "", "ƻ", "ƻ", "", "ƻ", "㽶", "ƻ", "㽶" };
for (auto e : arr)
{
countMap[e]++;
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
}
}
unordered_set
namespace sz
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename Bucket::HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair<iterator, bool> insert(const K& key)
{
return _ht.Insert(key);
}
private:
Bucket::HashTable<K, K, Hash, SetKeyOfT> _ht;
};
void test_set()
{
unordered_set<int> s;
s.insert(2);
s.insert(3);
s.insert(1);
s.insert(2);
s.insert(5);
unordered_set<int>::iterator it = s.begin();
//auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
}
ハッシュの適用
ビットマップ
ビットマップのトピックの紹介
空間コンピューティング
答え
コード
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;
};
void test_bit_set1()
{
bitset<100> bs1;
bs1.set(8);
bs1.set(9);
bs1.set(20);
cout << bs1.test(8) << endl;
cout << bs1.test(9) << endl;
cout << bs1.test(20) << endl;
bs1.reset(8);
bs1.reset(9);
bs1.reset(20);
cout << bs1.test(8) << endl;
cout << bs1.test(9) << endl;
cout << bs1.test(20) << endl;
}
void test_bit_set2()
{
bitset<-1> bs1;
//bitset<0xffffffff> bs2;
}
ビットマップの質問
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
bool inset1 = _bs1.test(x);
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);
}
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)
{
cout << i << endl;
}
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
void test_bit_set3()
{
int a[] = {
3, 4, 5, 2, 3, 4, 4, 4, 4, 12, 77, 65, 44, 4, 44, 99, 33, 33, 33, 6, 5, 34, 12 };
twobitset<100> bs;
for (auto e : a)
{
bs.set(e);
}
bs.print_once_num();
}
ビットマップ アプリケーション
- 特定のデータがコレクションに含まれているかどうかをすばやく確認する
- 並べ替え + 重複排除
- 2 つの集合の交点、和集合などを求める
- オペレーティング システムでのディスク ブロックのマーキング
ブルームフィルター
ブルームフィルターが提案する
ニュース クライアントを使用してニュースを視聴すると、新しいコンテンツが継続的に推奨され、推奨されるたびにそれが繰り返され、既に表示されたコンテンツが削除されます。ここで質問があります。ニュース クライアントのレコメンデーション システムは、プッシュの重複排除をどのように実現するのでしょうか? サーバーは、ユーザーが見たすべての履歴レコードを記録します. レコメンデーション システムがニュースを推奨する場合、各ユーザーの履歴レコードをフィルター処理し、既存のレコードを除外します. それをすばやく見つける方法は?
- ハッシュ テーブルを使用してユーザー レコードを保存する、欠点: スペースの無駄
- ビットマップを使用してユーザー レコードを保存します。利点: 高速でスペースを節約できます (直接アドレッシング方式、競合なし); 欠点: ビットマップは整形のみを処理でき、コンテンツ番号が文字列の場合は処理できません。
- ハッシュとビットマップの組み合わせ、つまりブルーム フィルター
ブルーム フィルターの概念
ブルーム フィルターは、 1970 年に Burton Howard Bloom によって提案されたコンパクトで巧妙な確率的データ構造です。効率的な挿入とクエリが特徴で、「何かが存在してはならない、または存在する可能性がある」ことを伝えるために使用できます。複数のハッシュ関数を使用します。データをビットマップ構造にマップします。この方法は、クエリの効率を向上させるだけでなく、多くのメモリ スペースを節約することもできます。
ブルームフィルター記事
ブルームフィルター挿入
ブルーム フィルターはビット ベクトルまたはビット配列です
値をブルーム フィルターにマップする場合は、複数の異なるハッシュ関数を使用して複数のハッシュ値を生成する必要があり、生成されたハッシュごとに、値が指すビット位置が に設定されます。 1.
たとえば、値「baidu」と 3 つの異なるハッシュ関数に対してそれぞれハッシュ値 1、4、および 7 が生成されると、上の図は次のように変わります。
別の値「tencent」を保存します。ハッシュ関数が 3、4、8 を返す場合、グラフは次のようになります。
両方の値のハッシュ関数がこのビットを返すため、4 ビットが上書きされることに注意してください。値「dianping」が存在するかどうかを確認する場合、ハッシュ関数は 1、5、および 8 の 3 つの値を返します。その結果、ビット 5 の値が 0 であり、値がないことを示します。はこのビットにマッピングされるため、値「dianping」は存在しないと確信できます。そして、値「baidu」が存在するかどうかを確認する必要がある場合、ハッシュ関数は必然的に 1、4、7 を返し、これら 3 つのビットの値がすべて 1 であることを確認してから、 「百度」と言えますか?答えはノーです。"baidu" という値だけが存在する可能性があります。
どうしてこれなの?答えは簡単です。なぜなら、値が追加されるにつれて、より多くのビットが 1 に設定されるため、特定の値「taobao」が格納されていなくても、ハッシュ関数が 3 ビットの場合他の値によってすべて 1 に設定されている場合でも、プログラムは値「taobao」が存在すると判断します。
ハッシュ関数の数とブルーム フィルターの長さを選択する方法
コード
struct HashBKDR
{
// BKDR
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
struct HashAP
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 5381;
for (auto ch : key)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
// N表示准备要映射N个值
template<size_t N,
class K = string, class Hash1 = HashBKDR, class Hash2 = HashAP, class Hash3 = HashDJB>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
//cout << hash1 << endl;
_bits->set(hash1);
size_t hash2 = Hash2()(key) % (_ratio * N);
//cout << hash2 << endl;
_bits->set(hash2);
size_t hash3 = Hash3()(key) % (_ratio * N);
//cout << hash3 << endl;
_bits->set(hash3);
}
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
if (!_bits->test(hash1))
return false; // 准确的
size_t hash2 = Hash2()(key) % (_ratio * N);
if (!_bits->test(hash2))
return false; // 准确的
size_t hash3 = Hash3()(key) % (_ratio * N);
if (!_bits->test(hash3))
return false; // 准确的
return true; // 可能存在误判
}
// 能否支持删除->
void Reset(const K& key);
private:
const static size_t _ratio = 5; // 4.3,这里给了一个整数5
// vs标准库里面bitset是一个静态数组,当要插入的元素太多,会把栈撑爆,所以结局办法是在堆上
//std::bitset<_ratio* N>* _bits;
std::bitset<_ratio* N>* _bits = new std::bitset<_ratio* N>;
};
テストコード
void TestBloomFilter2()
{
srand(time(0));
const size_t N = 100000;
BloomFilter<N> bf;
cout << sizeof(bf) << endl;
std::vector<std::string> v1;
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(1234 + i));
}
for (auto& str : v1)
{
bf.Set(str);
}
// 相似
std::vector<std::string> v2;
for (size_t i = 0; i < N; ++i)
{
std::string url = "http://www.cnblogs.com/-clq/archive/2021/05/31/2528153.html";
url += std::to_string(rand() + i);
v2.push_back(url);
}
size_t n2 = 0;
for (auto& str : v2)
{
if (bf.Test(str))
{
++n2;
}
}
cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;
std::vector<std::string> v3;
for (size_t i = 0; i < N; ++i)
{
string url = "zhihu.com";
url += std::to_string(rand()+i);
v3.push_back(url);
}
size_t n3 = 0;
for (auto& str : v3)
{
if (bf.Test(str))
{
++n3;
}
}
cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}
ブルーム フィルター ルックアップ
ブルーム フィルターの考え方は、複数のハッシュ関数を使用して要素をビットマップにマップすることなので、マップされた位置のビットは 1 でなければなりません。したがって、次の方法で検索できます: 各ハッシュ値に対応するビット位置がゼロとして格納されているかどうかを計算します. 1 がゼロである限り、要素がハッシュ テーブルに存在してはならないことを意味します。ハッシュ表。
注: ブルーム フィルターが要素が存在しないと言う場合、その要素は存在してはなりません. 要素が存在する場合、一部のハッシュ関数には特定の誤判定があるため、要素が存在する可能性があります.
例えば、ブルームフィルターで「アリババ」を探す場合、3つのハッシュ関数で計算したハッシュ値が1、3、7で、他の要素のビットと重なるだけだとします。ブルームフィルターは要素が存在することを伝えますが、要素は存在しません。
ブルームフィルターの除去
1 つの要素が削除されると、他の要素が影響を受ける可能性があるため、ブルーム フィルターは削除を直接サポートできません。
例: 上図の「tencent」要素を削除します。要素に対応するバイナリ ビット位置が直接 0 の場合、「baidu」要素も削除されます。これらの 2 つの要素は、複数のハッシュ関数によって計算されたビット上にあるためです。たまたま重なりがあります。
削除をサポートするメソッド: ブルーム フィルターの各ビットを小さなカウンターに展開し、要素を挿入するときに k カウンター (k ハッシュ関数によって計算されたハッシュ アドレス) に 1 を追加し、要素を削除するとき、k カウンターを 1 ずつ減らし、増加します。数倍のストレージ スペースを占有することによる削除操作。
欠陥:
- 要素が実際にブルーム フィルターにあるかどうかを確認できません
- プレゼンス カウント ラップ
ブルームフィルターの利点
- 要素の追加とクエリの時間の複雑さは、データのサイズに関係なく、O(K) (K はハッシュ関数の数で、一般に比較的小さい) です。
- ハッシュ関数は相互に関係がなく、ハードウェアの並列操作に便利です
- ブルーム フィルターは要素自体を保存する必要がないため、機密性の要件が厳しい場合に大きな利点があります。
- ある程度の判断ミスに耐えることができる場合、ブルーム フィルターは他のデータ構造よりも大きなスペースの利点があります。
- データ量が多い場合、ブルーム フィルターは完全なセットを表すことができ、他のデータ構造では表現できません。
- 同じ一連のハッシュ関数を使用するブルーム フィルターは、交差、和、および差分演算を実行できます。
ブルームフィルターの不具合
- 偽陽性率がある、つまり偽陽性(False Position)がある、つまり要素がセット内にあるかどうかを正確に判断できない(対策:ホワイトリストを作成して、誤判定される可能性のあるデータを保存する)
- 要素自体を取得できません
- 一般にブルームフィルターから要素を削除することはできません
- 削除にカウントを使用する場合、ラッピングのカウントに問題がある可能性があります