HashMapでリストの比較を早くする
HashMapの使い方を覚えたので、関連項目と一緒にまとめてみる。まずは、HashMapとは何ぞやということを抑えてから、具体例を通して、どういう場合に使えるかを考えてみる。
HashMapとは?
Map
インターフェイスを実装しているデータストラクチャー。他にもdictionary
やunordered map
などと呼ばれることもあるが、Javaの標準ライブラリではHashMap
というクラスが使われている。
強み | 弱み |
---|---|
基本的には早い検索機能[1] | 場合によっては時間がかかる[2] |
key がフレキシブル |
要素の格納順が順不同 |
- |
value をキーとしてkey を検索する場合は、全要素をループする必要があり時間がかかる |
- | linked listを実装しているため、それぞれのデータがメモリ上で隣接していない[3] |
Map
Map
は、Collection
の一つ。key
とvalue
のペアとなる要素を格納しており、key
を与えると、対応するvalue
を検索できる。
ここで注意すべきは、key
はindex
ではないことだ。index
は各要素に対して、格納された順番に自動で番号が振られる。規則性が担保されているため、例えば配列の場合、index
を軸にしsort()
で要素の並び替えができるが、Map
の場合はそういう手段はない。自由とは時に不自由なものなのだ!
HashMapとTreeMap
Javaの中で一般的なimplementation of Map
がHashMap
とTreeMap
だ。両方ともMap
インターフェイスを実装している。
HashMap | TreeMap | |
---|---|---|
格納方法の違い | 順不同 |
put された順に格納されていく |
得意分野 |
TreeMap より少し早い。一般的に順番にこだわらない場合に使われる |
格納順にこだわる場合はTreeMap 一択 |
HashMapの出番
2つのリストの比較
次のような文字列を格納する2つのリストがある。(長さは同じでなくてもよい)
List<String> strings = List.of("aba", "baba", "aba", "xzxb");
List<String> queries = List.of("aba", "xzxb", "ab");
strings
を順に調べ、queries
内の各要素といくつ合致しているか分かる配列が欲しい。
上記の例で言えば、
-
aba
は、strings
内に2コ -
xzxb
は、strings
内に1コ -
ab
は、strings
0内に0コ
となるから、求める配列は、
[2,1,0]
になる。
よくある例:for-loopを配列分回す
public static List<Integer> matchingStrings(List<String> strings, List<String> queries) {
// (1) queries内の全要素を調べ、その要素の数だけ(2)countStr()を呼び出し、
// その結果をsolに書き出す
List<Integer> sol = new ArrayList<>();
for (String s : queries)
sol.add(countStr(s, strings));
return sol;
}
// (2) strings内の全要素を調べ、sと合致した回数を返す
public static Integer countStr(String s, List<String> strings) {
int count = 0;
for(String x: strings)
if(s.equals(x))
count++;
return count;
}
public static void main(String[] args) {
List<String> strings = List.of("aba", "baba", "aba", "xzxb");
List<String> queries = List.of("aba", "xzxb", "ab");
System.out.println(matchingStrings(strings, queries));
}
HashMapを使った例
public static List<Integer> matchingStrings2(List<String> strings, List<String> queries) {
// (1) dicの作成
// dic = {key: queries内の各要素, value: strings内にそのkeyがいくつあるか}
Map<String, Integer> dic = new HashMap<>();
for(String str: strings){ // O(n)
int temp = dic.getOrDefault(str, 0);
dic.put(str, ++temp);
System.out.println("dic "+ dic);
}
// dic = {baba=1, aba=2, xzxb=1}
// (2) queriesの各要素をkeyとして、dicのvalueを取り出し、solに書き出す
List<Integer> sol = new ArrayList<>();
for (String s : queries) {
sol.add(dic.getOrDefault(s,0)); // O(m)
}
return sol;
}
public static void main(String[] args) {
List<String> strings = List.of("aba", "baba", "aba", "xzxb");
List<String> queries = List.of("aba", "xzxb", "ab");
System.out.println(matchingStrings2(strings, queries));
}
比較
queties
の要素数をm、strings
の要素数をnとおき、この二つのアプローチの違いを調べてみるとHashMapを使ったものの方が、早い。Big O-notationで記してみると:
- よくある例は、O(m*n)
- HashMapを使った例は、O(m+n)
よくある例:for-loopを配列分回すの基本方針は、queries
内の全要素を調べ、その要素の数だけcountStr()
を呼び出し、その結果をsol
に書き出すというものである。この中で、countStr()
は呼び出される度に、strings
の全要素を調べることになる。つまり、O(m*n)。
一方HashMapを使った例では、
-
strings
を一周して、それぞれの文字列が何回出てきたかが分かるHashMapdic
を作成する。 - その次に、その
dic
とqueries
を比較して結果の配列に書き出す。
という二段階に分かれている。つまり、それぞれのループは独立しているので、O(m+n)
ということで、要素数が多くなれば多くなるほど、この二つのアプローチの処理効率の差は指数関数的に大きくなっていく。
Discussion