ハッシュテーブルとは何ですか?なぜ必要ですか?
ハッシュテーブル(ハッシュマップ)は、キーと値のペアを格納し、データへの非常に高速なアクセスを提供するデータ構造です。平均して、検索、挿入、削除の操作は O(1) の定数時間で実行されます。つまり、テーブルに10個の要素が格納されているか1000万個の要素が格納されているかに関係なく、それらのいずれかにアクセスするのにかかる時間はほぼ同じです。
人生の簡単な例:
const userAges = {
"Alexey": 28,
"Maria": 25,
"Dmitry": 32
};
console.log(userAges["Maria"]); // 25 - 即時アクセス!ほとんどの現代のプログラミング言語では、ハッシュテーブルが組み込まれています。 dict Pythonで、 Map JavaScriptで、 HashMap Javaで、 map行く。
ハッシュテーブルの仕組み
ボンネットの下のハッシュテーブルは通常の配列です。魔法は起こります ハッシュ関数キー(文字列、数値、またはオブジェクト)を配列のインデックスに変換します。
作業プロセス:
キーのハッシュ: キー(例:「アレクセイ」)は、数値(例:42)を返すハッシュ関数を介してスキップされます
インデックスの計算:ハッシュ結果は、除算演算の残りを使用して配列インデックスに変換されます:
index = hash % array.length値を保存する:値はこのインデックスを持つ配列のセルに保存されます
"アレクセイ" → hash ("アレクセイ") → 1892374 → 1892374%10 →インデックス4配列:[_, _, _, _, {key: "アレクセイ", value: 28}, _, _, _, _, _]
ハッシュ関数:システムの心臓部
良いハッシュ関数には、いくつかの特性が必要です。
決定論 - 同じキーは常に同じハッシュを与えます
均一性 — ハッシュは値の範囲全体に均等に分散する必要があります
スピード — ハッシュの計算は迅速であるべきです
衝突の最小化 - 異なるキーは異なるハッシュを与える必要があります
文字列の単純なハッシュ関数の例:
function simpleHash(str, tableSize) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash + str.charCodeAt(i) * (i + 1)) % tableSize;
}
return hash;
}
console.log(simpleHash("Alexey", 100)); // たとえば、67
console.log(simpleHash("Maria", 100)); // 例:23実際のシステムでは、MurmurHash、CityHash、またはSHA-256(特別な場合)のような暗号関数など、より複雑な関数が使用されます。
競合:2つのキーが1つの場所を要求する場合
衝突 2つの異なるキーがハッシュ後に同じインデックスを受け取ったときに発生します。これは避けられない問題です。なぜなら、可能なキーの数は通常、配列のサイズよりも大きいからです。
「アレクセイ」→インデックス4「ドミトリー」→インデックス4←衝突!
チェーンメソッド(チェーン)
衝突を解決する最も一般的な方法。配列の各セルには、1 つの値ではなく、同じインデックスを持つすべての値のリスト (チェーン) が格納されます。
class HashTable {
constructor(size = 50) {
this.table = new Array(size);
this.size = size;
}
_hash(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash + key.charCodeAt(i) * (i + 1)) % this.size;
}
return hash;
}
set(key, value) {
const index = this._hash(key);
if (!this.table[index]) {
this.table[index] = [];
}
// そのようなキーがすでに存在するかどうかを確認します
for (let item of this.table[index]) {
if (item[0] === key) {
item[1] = value; // 値を更新しています
return;
}
}
// 新しいキーと値のペアを追加します
this.table[index].push([key, value]);
}
get(key) {
const index = this._hash(key);
const bucket = this.table[index];
if (!bucket) return undefined;
for (let item of bucket) {
if (item[0] === key) {
return item[1];
}
}
return undefined;
}
remove(key) {
const index = this._hash(key);
const bucket = this.table[index];
if (!bucket) return false;
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === key) {
bucket.splice(i, 1);
return true;
}
}
return false;
}
}
// 使用
const users = new HashTable();
users.set("Alexey", { age: 28, city: "Moscow" });
users.set("Maria", { age: 25, city: "St. Petersburg" });
users.set("Dmitry", { age: 32, city: "Novosibirsk" });
console.log(users.get("Maria")); // { age: 25, city: "サンクトペテルブルク" }users.remove("Alexey");
console.log(users.get("Alexey")); // undefinedオープンアドレス指定(Open Addressing)
衝突が発生した場合、チェーンを作成する代わりに、特定のアルゴリズムに従って配列内の別の空きセルを探します。
線形穿孔 — 次のセルを連続してチェックします。index、index+1、index+2...
class HashTableOpenAddressing {
constructor(size = 50) {
this.table = new Array(size);
this.size = size;
this.count = 0;
}
_hash(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash + key.charCodeAt(i) * (i + 1)) % this.size;
}
return hash;
}
set(key, value) {
if (this.count / this.size > 0.7) {
this._resize(); // 70%以上の充填時にサイズを拡大します
}
let index = this._hash(key);
let i = 0;
while (this.table[index] !== undefined && this.table[index].key !== key) {
i++;
index = (this._hash(key) + i) % this.size; // 線形穿孔
}
if (this.table[index] === undefined) {
this.count++;
}
this.table[index] = { key, value };
}
get(key) {
let index = this._hash(key);
let i = 0;
while (this.table[index] !== undefined) {
if (this.table[index].key === key) {
return this.table[index].value;
}
i++;
index = (this._hash(key) + i) % this.size;
}
return undefined;
}
_resize() {
const oldTable = this.table;
this.size *= 2;
this.table = new Array(this.size);
this.count = 0;
for (let item of oldTable) {
if (item !== undefined) {
this.set(item.key, item.value);
}
}
}
}二次サンプリング — 確認します: index, index+1², index+2², index+3²...
二重ハッシュ — 2 番目のハッシュ関数を使用してステップを計算します。
生産性と複雑さ
操作 | 平均的なケース | 最悪のケース |
|---|---|---|
挿入 | O(1) | O(n) |
検索 | O(1) | O(n) |
削除 | O(1) | O(n) |
負荷係数 = 要素数/配列サイズ
負荷係数が 0.7 より大きい場合、通常は rehashing — 新しいより大きな配列を作成し、すべての要素を移動します。

実際のユースケース
1. 結果のキャッシュ
class Cache {
constructor() {
this.cache = new Map();
}
async fetchUser(userId) {
// キャッシュをチェックしています
if (this.cache.has(userId)) {
console.log('Received from cache');
return this.cache.get(userId);
}
// データを読み込んでいます
const user = await api.getUser(userId);
// キャッシュに保存
this.cache.set(userId, user);
return user;
}
}2. 要素の頻度の計算
function countWords(text) {
const wordCount = new Map();
const words = text.toLowerCase().split(/\s+/);
for (let word of words) {
wordCount.set(word, (wordCount.get(word) || 0) + 1);
}
return wordCount;
}
const text = "JavaScript is a programming language JavaScript is popular";
console.log(countWords(text));
// Map { 'javascript' => 2, 'this' => 1, 'language' => 1, ... }... }3. 重複の検索
function hasDuplicates(arr) {
const seen = new Set();
for (let item of arr) {
if (seen.has(item)) {
return true;
}
seen.add(item);
}
return false;
}
console.log(hasDuplicates([1, 2, 3, 4, 5])); // false
console.log(hasDuplicates([1, 2, 3, 2, 5])); // true4. データベースのインデックス作成
データベースは、キーによるレコードの高速検索にハッシュインデックスを使用します。
-- В PostgreSQL
CREATE INDEX users_email_hash ON users USING HASH (email);
-- Теперь поиск по email молниеносный
SELECT * FROM users WHERE email = 'alex@example.com';5. ユーザーの一意の識別子
class Session {
constructor() {
this.sessions = new Map();
}
createSession(userId) {
const sessionId = this.generateSessionId();
this.sessions.set(sessionId, {
userId,
createdAt: Date.now(),
data: {}
});
return sessionId;
}
getSession(sessionId) {
return this.sessions.get(sessionId);
}
destroySession(sessionId) {
this.sessions.delete(sessionId);
}
generateSessionId() {
return Math.random().toString(36).substring(2);
}
}6. Webフレームワークでのルーティング
class Router {
constructor() {
this.routes = new Map();
}
addRoute(path, handler) {
this.routes.set(path, handler);
}
handleRequest(path) {
const handler = this.routes.get(path);
if (handler) {
return handler();
}
return 'Not Found';
}
}
const router = new Router();
router.addRoute('/home', () => 'Home Page');
router.addRoute('/about', () => 'About Page');
console.log(router.handleRequest('/home')); // Home Page - O(1)!7.アナグラムと行のグループ化
function groupAnagrams(words) {
const groups = new Map();
for (let word of words) {
// 文字をキーとして並べ替える
const key = word.split('').sort().join('');
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(word);
}
return Array.from(groups.values());
}
console.log(groupAnagrams(['eat', 'tea', 'tan', 'ate', 'nat', 'bat']));
// [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]JavaScriptのMap vs Object:どちらを選択しますか?
JavaScript には、ハッシュ テーブルを操作する方法が 2 つあります。
Object:
const obj = {};
obj.name = "Alexey";
obj['age'] = 28;Map:
const map = new Map();
map.set('name', 'Alexey');
map.set('age', 28);マップを使用するタイミング:
キーは文字列ではありません(オブジェクト、数値、関数)
追加順に反復する必要があります
要素の頻繁な追加/削除
要素の正確な数を知る必要があります(
map.size)
Objectを使用するタイミング:
キーは常に文字列です
JSON.stringify/parseが必要です
シンプルな構成
実用的なアドバイス
1. 正しい初期サイズを選択する
// 約1000個の要素があることがわかっている場合
const map = new Map(); // デフォルトは小さいサイズです
// vs
const users = new HashTable(2000); // リハッシュを避ける2. キーのようなオブジェクトに注意する
const map = new Map();
const key1 = { id: 1 };
const key2 = { id: 1 };
map.set(key1, 'value1');
console.log(map.get(key2)); // 未定義!異なるオブジェクト3. 使用していないデータをクリーンアップする
// 自動クリアにWeakMapを使用する
const cache = new WeakMap();
let user = { name: 'Alexey' };
cache.set(user, 'some data');
user = null; // データはWeakMapから自動的に削除されます結論
ハッシュテーブルは、データベースからWebフレームワーク、コンパイラから検索エンジンまで、あらゆる場所で使用される基本的なデータ構造です。そのデザイン、競合解決、パフォーマンスを理解することで、より効率的な開発者になれます。
B コディケ ハッシュテーブルだけでなく、開発者にとって重要な他のテーマも多数解説しています。基本から高度なテクニックまで網羅しています。当社のコースには、実践的な課題と業界の実際のケースが含まれています。
Telegramチャンネルにご参加ください — ここには、フレンドリーな開発者コミュニティ、毎週の役立つ資料、興味深いタスクの分析、質問への回答があります。
一緒に成長しましょう! 🚀
