カスタムハッシュ関数
独自の型をハッシュします
「カスタムハッシュ関数」はCoddyKit上の無料C++ Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC++ Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C++ Academyコースには全4レッスンが含まれています。
カスタムハッシュが必要な理由
順序付けされないコンテナでは、キーをハッシュする方法が必要です。組み込み型と std::string にはすでにハッシュ関数がありますが、独自の型にはありません。自分で用意する必要があります。
#include <iostream>
#include <unordered_set>
#include <string>
int main() {
std::unordered_set<std::string> s{"hi"};
std::cout << s.count("hi") << '\n';
return 0;
}std::hash テンプレート
std::hash は値を size_t に対応付けるファンクタです。関数と同じように呼び出せます。
#include <iostream>
#include <functional>
#include <string>
int main() {
std::hash<std::string> h;
std::cout << "hash exists and returns a size_t\n";
std::size_t v = h("hello");
std::cout << (v != 0 ? "non-zero hash" : "zero") << '\n';
return 0;
}ハッシュする構造体
2つの int を持つ Point があるとします。これを unordered_set に格納するには、等価性とハッシュの両方が必要です。
#include <iostream>
struct Point {
int x, y;
bool operator==(const Point& o) const { return x == o.x && y == o.y; }
};
int main() {
Point a{1, 2}, b{1, 2};
std::cout << std::boolalpha << (a == b) << '\n';
return 0;
}ハッシュファンクタを記述する
ハッシュファンクタは、size_t を返す operator() を持つ構造体です。通常は XOR とシフトを使って、各フィールドのハッシュを組み合わせます。
#include <iostream>
#include <functional>
struct Point { int x, y; };
struct PointHash {
std::size_t operator()(const Point& p) const {
return std::hash<int>()(p.x) ^ (std::hash<int>()(p.y) << 1);
}
};
int main() {
PointHash h;
std::cout << "hashed: " << (h({3, 4}) != 0 ? "ok" : "zero") << '\n';
return 0;
}ハッシュファンクタを使う
ハッシュファンクタを、順序付けされないコンテナの2番目のテンプレート引数として渡します。
#include <iostream>
#include <unordered_set>
#include <functional>
struct Point {
int x, y;
bool operator==(const Point& o) const { return x == o.x && y == o.y; }
};
struct PointHash {
std::size_t operator()(const Point& p) const {
return std::hash<int>()(p.x) ^ (std::hash<int>()(p.y) << 1);
}
};
int main() {
std::unordered_set<Point, PointHash> pts;
pts.insert({1, 2});
pts.insert({1, 2});
std::cout << pts.size() << '\n';
return 0;
}等価性も必要
2つのキーのハッシュが衝突すると、それらは同じバケットに入ります。するとコンテナは operator== を使って両者を区別するため、等価性の定義が必須です。
#include <iostream>
#include <unordered_set>
struct Point {
int x, y;
bool operator==(const Point& o) const { return x == o.x && y == o.y; }
};
struct PointHash {
std::size_t operator()(const Point& p) const {
return std::hash<int>()(p.x * 31 + p.y);
}
};
int main() {
std::unordered_set<Point, PointHash> s{{1, 1}, {2, 2}};
std::cout << s.count({1, 1}) << '\n';
return 0;
}ハッシュを map のキーに使う
同じカスタムハッシュを使えば、構造体を unordered_map のキーにできます。
#include <iostream>
#include <unordered_map>
#include <functional>
struct Point {
int x, y;
bool operator==(const Point& o) const { return x == o.x && y == o.y; }
};
struct PointHash {
std::size_t operator()(const Point& p) const {
return std::hash<int>()(p.x) ^ (std::hash<int>()(p.y) << 1);
}
};
int main() {
std::unordered_map<Point, std::string, PointHash> m;
m[{0, 0}] = "origin";
std::cout << m[{0, 0}] << '\n';
return 0;
}複数のフィールドを組み合わせる
よく使われるヘルパーでは、boost::hash_combine に似た乗算と加算のパターンを使い、フィールドを1つずつ処理してハッシュを組み合わせます。
#include <iostream>
#include <functional>
std::size_t combine(std::size_t seed, std::size_t v) {
return seed ^ (v + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}
int main() {
std::size_t h = 0;
h = combine(h, std::hash<int>()(10));
h = combine(h, std::hash<int>()(20));
std::cout << (h != 0 ? "combined ok" : "zero") << '\n';
return 0;
}良好なハッシュ分布
常に同じ値を返す粗悪なハッシュでは、すべての要素が1つのバケットに入り、計算量が O(n) に低下します。すべてのフィールドのビットを適切に混ぜてください。
#include <iostream>
#include <functional>
struct Bad { std::size_t operator()(int) const { return 0; } };
struct Good { std::size_t operator()(int x) const { return std::hash<int>()(x); } };
int main() {
std::cout << Bad()(5) << ' ' << (Good()(5) != 0 ? "varies" : "0") << '\n';
return 0;
}std::hash の特殊化
別の方法として、独自の型に対して std::hash を特殊化すれば、ファンクタを明示的に渡さずに使えるようになります。
#include <iostream>
#include <unordered_set>
struct Point {
int x, y;
bool operator==(const Point& o) const { return x == o.x && y == o.y; }
};
namespace std {
template <> struct hash<Point> {
std::size_t operator()(const Point& p) const {
return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1);
}
};
}
int main() {
std::unordered_set<Point> s{{1, 2}};
std::cout << s.count({1, 2}) << '\n';
return 0;
}ハッシュとしてのラムダ
C++20 では、型を渡すことでステートレスなラムダをハッシュとして使うこともできます。
#include <iostream>
#include <unordered_set>
int main() {
auto h = [](int x) { return std::hash<int>()(x * 2654435761u); };
std::unordered_set<int, decltype(h)> s(8, h);
s.insert(42);
std::cout << s.count(42) << '\n';
return 0;
}クイックチェック
カスタムハッシュについて、理解度を確認しましょう。
まとめ
カスタム型をハッシュする方法として、次のことを学びました。
size_tを返すハッシュファンクタを用意する(またはstd::hashを特殊化する)- 衝突したキーを区別できるよう、operator== も用意する
- 適切な分布になるよう、各フィールドのハッシュを適切に組み合わせる
次は、バケットと負荷率がパフォーマンスに与える影響を調べます。
よくある質問
「カスタムハッシュ関数」レッスンは無料ですか?
はい。「カスタムハッシュ関数」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C++ Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C++ Academyコースには全4レッスンが含まれています。
「カスタムハッシュ関数」で何を学びますか?
独自の型をハッシュします ブラウザで直接実行するハンズオンコードでC++ Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
C++ Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのC++ Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「カスタムハッシュ関数」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このC++ Academyレッスンでコードを書いて実行できますか?
はい。すべてのC++ Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。