データ構造選択とは何か
データ構造選択は「どの形(配列、オブジェクト、Map、Set、派生構造)でデータを持つか」を決めることです。ここが重要です:用途に合った構造を選ぶと、計算量が下がり、コードが短く、安全性が上がります。例えば“順序つきで並べたい”なら配列、“IDで即アクセスしたい”なら辞書(オブジェクト/Map)、“存在チェックだけ高速化したい”なら Set を選びます。
配列(Array)を選ぶ場面と注意点
配列は「順序」「連続アクセス」「全走査」に強い汎用構造です。リスト表示、並び替え、ページングなどに向いています。末尾操作(push/pop)は速く、先頭操作(shift/unshift)は遅いため、先頭中心の設計は避けます。大量データでは filter→map の中間配列を無闇に増やさず、必要なら1パス融合で結果だけ作ると安定します。
// 順序重視の一覧(配列で保持)
const rows = [{ id: 1 }, { id: 2 }, { id: 3 }];
const sorted = rows.toSorted((a, b) => a.id - b.id);
const page = sorted.slice(0, 20);
JavaScriptここが重要です:配列は“順序+まとまった処理”に向くが、頻繁な検索・更新(ID指定)には不向きです。そのときは辞書と併用します。
オブジェクト({})と Map の違いと使い分け
辞書化は「キー→レコード」を即アクセスしたい場面で最強です。オブジェクトとMapは似ていますが、特性が異なります。ここが重要です:キーの型、列挙順、意図しない衝突、JSON化の容易さを基準に選びます。
// オブジェクト(文字列/シンボルキーのみ、JSON化が簡単)
const dict = Object.fromEntries(rows.map(x => [String(x.id), x]));
dict["2"].active = true;
// Map(任意型キー、サイズ・順序管理が明快)
const map = new Map(rows.map(x => [x.id, x]));
map.get(2).active = true;
JavaScriptオブジェクトは「JSONで送る/保存する」「文字列IDが主」で便利。Mapは「数値IDやオブジェクトをキーにしたい」「サイズや反復順序を明確に扱いたい」場面に適します。実務では、外部I/Oやシリアライズが絡む層はオブジェクト、アプリ内部のアルゴリズムやキャッシュ層はMapが噛み合います。
Set と配列の存在チェック(メンバー判定の高速化)
存在チェックが多いなら Set を選びます。配列の includes は最悪で全走査ですが、Set は平均で一定時間です。ここが重要です:重複排除、一意性の担保、属しているかの判定で Set に置き換えるとコードも性能もシンプルになります。
// 配列での存在チェック(遅いことがある)
const existsSlow = ids.includes(targetId);
// Set での存在チェック(速い)
const idSet = new Set(ids);
const existsFast = idSet.has(targetId);
// 重複排除
const unique = [...new Set(ids)];
JavaScript配列で並びが必要なら配列+Setの併用(順序は配列、メンバー判定はSet)が定番です。
正規化(entities)で構造を分ける設計
一覧表示と部分更新を両立したいなら、正規化(entities)構造にします。ここが重要です:配列は“順序と表示”、辞書は“検索と更新”、関係は“IDで紐付け”。これで大規模でも破綻しません。
// 正規化(辞書+ID配列)
function normalizeUsers(users) {
return {
entities: Object.fromEntries(users.map(u => [String(u.id), u])),
result: users.map(u => String(u.id))
};
}
// 更新は辞書だけ触る
const nextEntities = {
...entities,
["123"]: { ...entities["123"], active: true }
};
// 表示時に配列へ戻す
const viewRows = result.map(id => nextEntities[id]);
JavaScriptこの分離により、O(1)更新と順序制御を同時に満たせます。重複する同一レコードの複数コピーを避けられるため、参照の不整合が減ります。
優先度やトップNに“ヒープ”を使う考え方
全体ソートが重いときに、必要な上位だけを維持するならヒープ(優先度付きキュー)が有効です。JavaScriptに標準ヒープはありませんが、配列で簡易実装できます。ここが重要です:トップN抽出は“部分選抜”で O(n log n) の全体ソートを避けると速くなります。
// 最小ヒープの最小限実装(N個の最大値を残す)
class MinHeap {
constructor() { this.a = []; }
push(x, cmp = (x, y) => x - y) {
const a = this.a; a.push(x);
for (let i = a.length - 1; i > 0; ) {
const p = ((i - 1) / 2) | 0;
if (cmp(a[i], a[p]) < 0) { [a[i], a[p]] = [a[p], a[i]]; i = p; } else break;
}
}
pop(cmp = (x, y) => x - y) {
const a = this.a; const top = a[0]; const x = a.pop();
if (a.length) {
a[0] = x;
for (let i = 0; ; ) {
const l = 2 * i + 1, r = l + 1;
let m = i;
if (l < a.length && cmp(a[l], a[m]) < 0) m = l;
if (r < a.length && cmp(a[r], a[m]) < 0) m = r;
if (m === i) break;
[a[i], a[m]] = [a[m], a[i]]; i = m;
}
}
return top;
}
}
function topN(items, n, score) {
const heap = new MinHeap();
for (const it of items) {
heap.push(it, (x, y) => score(x) - score(y));
if (heap.a.length > n) heap.pop((x, y) => score(x) - score(y));
}
return heap.a.toSorted((a, b) => score(b) - score(a));
}
JavaScript必要部分だけを維持する構造を選べば、全体に高価な処理をかけずに済みます。
イミュータブルな更新と“部分コピー”の構造
UI状態や共有データでは非破壊(新インスタンス)で更新します。ここが重要です:必要な階層だけをコピーして新しくし、他は参照を保つと安全と効率のバランスが取れます。データ構造の選択は“更新の単位”にも影響します。
// ネストの部分更新(必要階層のみ)
const state = { user: { flags: { vip: false } }, list: [{ id: 1, active: false }] };
const next = {
...state,
user: { ...state.user, flags: { ...state.user.flags, vip: true } },
list: state.list.map(x => x.id === 1 ? { ...x, active: true } : x)
};
JavaScript配列中心の構造は“一括操作”が得意、辞書中心の構造は“個別更新”が得意。要件に応じて併用すると無駄が減ります。
文字列キー・自然順・ロケール比較の構造選択
文字列を比較・キー化する場面は“正規化キー”を別フィールドで持つと構造的に速くなります。ここが重要です:比較時に毎回重い処理を行わず、前計算したキーを使うことで配列でも辞書でも効率が上がります。
// 前計算キーを持つ配列(並びや検索で軽量化)
const prepared = rows.map(x => ({ x, key: x.name.trim().toLowerCase() }));
const filtered = prepared.filter(p => p.key.includes("alice")).map(p => p.x);
// 自然順ソート(ファイル名など)
const sorted = rows.toSorted((a, b) => a.name.localeCompare(b.name, "ja", { numeric: true }));
JavaScript構造を選ぶだけでなく、“キーをどう持つか”という設計も性能に直結します。
キャッシュ・索引構造(派生データを構造で持つ)
検索や集計を高速化したいなら“派生構造”を追加します。ここが重要です:元データから派生した索引(インデックス)や集計器を別に持って、読み取りを速くします。更新時は派生も同期し、表示時は派生を優先利用します。
// 索引(category → ID配列)
const indexByCat = rows.reduce((acc, x) => {
const k = x.category ?? "(unknown)";
(acc[k] ??= []).push(x.id);
return acc;
}, {});
// 参照は辞書+索引で即時
const dict = Object.fromEntries(rows.map(x => [String(x.id), x]));
const books = indexByCat["book"].map(id => dict[id]);
JavaScript配列だけに頼らず、必要な読み取りパターンに合わせて“構造を足す”のがプロの設計です。
まとめ
データ構造選択の核心は「アクセス特性に合わせて持ち方を変える」ことです。順序・並びが主なら配列、即アクセス・個別更新が主なら辞書(オブジェクト/Map)、存在判定は Set。一覧は正規化(entities)で“辞書+ID配列”に分離し、派生構造(索引・前計算キー)で読み取りを速くする。トップNなどはヒープで“必要部分だけ”を維持し、UIや共有データは非破壊で安全に更新する。構造を意図的に選べば、初心者でも読みやすくて速い現場レベルのコードが書けます。
