10.4 ハッシュを使った最適化¶
アルゴリズムの問題では、線形探索をハッシュ探索に置き換えることで、時間計算量を減らす方法をよく使います。一つの問題を通して理解を深めましょう。
問題
整数配列 nums と目標要素 target が与えられます。配列内から和が target になる二つの要素を探し、それぞれのインデックスを返してください。解はいずれか一つでかまいません。
10.4.1 線形探索:時間を使って領域を節約する¶
すべての組み合わせを直接調べることを考えます。図 10-9 のように二重ループを使い、各反復で二つの整数の和が target かどうかを判定し、そうであればインデックスを返します。

図 10-9 線形探索で二数の和を求める
コードは次のとおりです。
two_sum.c
/* 方法 1:総当たり列挙 */
int *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {
for (int i = 0; i < numsSize; ++i) {
for (int j = i + 1; j < numsSize; ++j) {
if (nums[i] + nums[j] == target) {
int *res = malloc(sizeof(int) * 2);
res[0] = i, res[1] = j;
*returnSize = 2;
return res;
}
}
}
*returnSize = 0;
return NULL;
}
実行の可視化
この方法の時間計算量は \(O(n^2)\)、空間計算量は \(O(1)\) であり、データが多いと非常に時間がかかります。
10.4.2 ハッシュ探索:領域を使って時間を節約する¶
配列の要素をキー、そのインデックスを値とするハッシュテーブルを使うことを考えます。配列をループで走査し、各反復で図 10-10 の手順を実行します。
- 数値
target - nums[i]がハッシュテーブルにあるか調べ、あれば二つの要素のインデックスをそのまま返します。 nums[i]とインデックスiのペアをハッシュテーブルに追加します。



図 10-10 補助ハッシュテーブルで二数の和を求める
実装は次のとおりで、一重のループだけで済みます。
two_sum.cpp
/* 方法 2:補助ハッシュテーブル */
vector<int> twoSumHashTable(vector<int> &nums, int target) {
int size = nums.size();
// 補助ハッシュテーブルの空間計算量は O(n)
unordered_map<int, int> dic;
// 一重ループのため、時間計算量は O(n)
for (int i = 0; i < size; i++) {
if (dic.find(target - nums[i]) != dic.end()) {
return {dic[target - nums[i]], i};
}
dic.emplace(nums[i], i);
}
return {};
}
two_sum.java
/* 方法 2:補助ハッシュテーブル */
int[] twoSumHashTable(int[] nums, int target) {
int size = nums.length;
// 補助ハッシュテーブルの空間計算量は O(n)
Map<Integer, Integer> dic = new HashMap<>();
// 一重ループのため、時間計算量は O(n)
for (int i = 0; i < size; i++) {
if (dic.containsKey(target - nums[i])) {
return new int[] { dic.get(target - nums[i]), i };
}
dic.put(nums[i], i);
}
return new int[0];
}
two_sum.cs
/* 方法 2:補助ハッシュテーブル */
int[] TwoSumHashTable(int[] nums, int target) {
int size = nums.Length;
// 補助ハッシュテーブルの空間計算量は O(n)
Dictionary<int, int> dic = [];
// 一重ループのため、時間計算量は O(n)
for (int i = 0; i < size; i++) {
if (dic.ContainsKey(target - nums[i])) {
return [dic[target - nums[i]], i];
}
dic.Add(nums[i], i);
}
return [];
}
two_sum.ts
/* 方法 2:補助ハッシュテーブル */
function twoSumHashTable(nums: number[], target: number): number[] {
// 補助ハッシュテーブルの空間計算量は O(n)
let m: Map<number, number> = new Map();
// 一重ループのため、時間計算量は O(n)
for (let i = 0; i < nums.length; i++) {
let index = m.get(target - nums[i]);
if (index !== undefined) {
return [index, i];
} else {
m.set(nums[i], i);
}
}
return [];
}
two_sum.dart
/* 方法 2:補助ハッシュテーブル */
List<int> twoSumHashTable(List<int> nums, int target) {
int size = nums.length;
// 補助ハッシュテーブルの空間計算量は O(n)
Map<int, int> dic = HashMap();
// 一重ループのため、時間計算量は O(n)
for (var i = 0; i < size; i++) {
if (dic.containsKey(target - nums[i])) {
return [dic[target - nums[i]]!, i];
}
dic.putIfAbsent(nums[i], () => i);
}
return [0];
}
two_sum.rs
/* 方法 2:補助ハッシュテーブル */
pub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {
// 補助ハッシュテーブルの空間計算量は O(n)
let mut dic = HashMap::new();
// 一重ループのため、時間計算量は O(n)
for (i, num) in nums.iter().enumerate() {
match dic.get(&(target - num)) {
Some(v) => return Some(vec![*v as i32, i as i32]),
None => dic.insert(num, i as i32),
};
}
None
}
two_sum.c
/* ハッシュテーブル */
typedef struct {
int key;
int val;
UT_hash_handle hh; // uthash.h を用いて実装する
} HashTable;
/* ハッシュテーブルから取得する */
HashTable *find(HashTable *h, int key) {
HashTable *tmp;
HASH_FIND_INT(h, &key, tmp);
return tmp;
}
/* ハッシュテーブルに要素を挿入する */
void insert(HashTable **h, int key, int val) {
HashTable *t = find(*h, key);
if (t == NULL) {
HashTable *tmp = malloc(sizeof(HashTable));
tmp->key = key, tmp->val = val;
HASH_ADD_INT(*h, key, tmp);
} else {
t->val = val;
}
}
/* 方法 2:補助ハッシュテーブル */
int *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {
HashTable *hashtable = NULL;
for (int i = 0; i < numsSize; i++) {
HashTable *t = find(hashtable, target - nums[i]);
if (t != NULL) {
int *res = malloc(sizeof(int) * 2);
res[0] = t->val, res[1] = i;
*returnSize = 2;
return res;
}
insert(&hashtable, nums[i], i);
}
*returnSize = 0;
return NULL;
}
two_sum.kt
/* 方法 2:補助ハッシュテーブル */
fun twoSumHashTable(nums: IntArray, target: Int): IntArray {
val size = nums.size
// 補助ハッシュテーブルの空間計算量は O(n)
val dic = HashMap<Int, Int>()
// 一重ループのため、時間計算量は O(n)
for (i in 0..<size) {
if (dic.containsKey(target - nums[i])) {
return intArrayOf(dic[target - nums[i]]!!, i)
}
dic[nums[i]] = i
}
return IntArray(0)
}
実行の可視化
この方法では、ハッシュ探索によって時間計算量を \(O(n^2)\) から \(O(n)\) に減らし、実行効率を大きく向上させています。
追加のハッシュテーブルを維持する必要があるため、空間計算量は \(O(n)\) です。それでも、時間と空間の効率のバランスがよいため、この問題の最適な解法となります。