特にハミング距離のフィルタリングをどのように行うかが苦労しています。
streamに対するフィルタリングで「得たい要素だけを残す」というイメージをお持ちだと思います。しかし、その先のどこで躓いているかがはっきりと質問に書かれていません。プログラミングというのは
(A) 解を得るためにとるべき手順を考える(複雑な問題をより単純な手順の組み合わせとして考える)
(B) 充分単純化した部分的な手順のそれぞれについて頭の中にある処理パターンに照らし具体的コードに落とす
というような段取りで書いていくものです。ご質問の内容では(A)(B)どちらが問題なのか(あるいは両方なのか)がわからないわけです。わからない点が曖昧であればあるほど「ゴールにたどり着くに足る充分な回答が得られる確率は下がる」ことに注意ください。
ただ(B)を考えるための「頭の中の引き出しにある処理パターン」が乏しいと(A)をすること自体も難しいであろうことから、多くの場合(A)(B)どちらについても知識が不足しているといえるかも知れません。
本件は最適化を前提とせずに素朴に実装するなら割と単純な問題なので(A)を整理する例を挙げてみました。論理の組み立て方は人それぞれなところもあるのであくまで一例と考えてください。以下は「streamを使って論理を組み立てる練習をする」「最適化は考えず素朴な論理とする」方針で考えてみたものです。
手順を考える例
- (A1) 全ての単語について「ハミング距離が1であるような他の単語があるかどうか」を条件にフィルタリングする
質問者さんはここまではイメージしておられます。
- (A2) ハミング距離は等しい長さの列に対して定義されているので、まず全ての単語を同じ長さのもの同士でグルーピングする
これはswordoneさんが述べておられることです。
- (A3) 長さがLであるような単語の集合(以下WS(L)と呼ぶ)の各要素について「WS(L)内にハミング距離が1であるような単語が存在する」ものだけ残す
- (A4) WS(L)内の特定の単語についてWS(L)内の単語と総当たりで「ハミング距離が1か」を調べる。
そういう単語が一つでも見つかれば(A3)の判定結果はtrueさもなくばfalseです。
- (A5) 同じ長さの2つの単語の同じ位置の文字が違っている数を数える(ハミング距離の計算)
- (A6) 2つの単語の同じ位置の要素同士を表すペアの列に変換し各ペアを文字同士が異なれば1、さもなくば0という整数に変換しそれぞれの結果を合計する
(A5)をstream風にとらえた手順です。
- (A7) ありえる全てのLに対してWS(L)について(A3)でフィルタリングした集合を連結する
大雑把ですが上のような感じで考えました。上記ぐらいにすると個々の問題はstreamの処理パターンを知っていれば具体的コードに落とせますが、上から下へ順番に書くというより「わかる部分からコードに落としそれをうまく組み合わせる」というイメージで行うと作り易いかも知れません。
コードを組み立てる
(A6)は本当はzip的な機能がほしいところですがJavaの標準ライブラリーにはないので別のアプローチで次のようにしてみます。
java
1int hammingDistance(String w1, String w2) {
2 assert s1.length() == s2.length(); // 前提
3 return IntStream.range(0, w1.length())
4 .map(i -> w1.charAt(i) != w2.charAt(i) ? 1 : 0)
5 .sum();
6}
(A4)はanyMatchを使えばこう書けます。
java
1boolean pass(List<String> wsl, String word) {
2 return wsl.stream().anyMatch(w -> hamminguDistance(word, w) == 1);
3}
(A3)はfilterでよいですね。
java
1static Stream<String> select1(List<String> wsl) {
2 return wsl.stream().filter(word -> pass(word));
3}
(A2)はswordoneさん回答にあるとおりです。
(A2)で生成できたMap<Integer, List<String>>の各値(List<String>)に基づき(A3)で生成したストリームを(A7)の目的のため全てくっつける計算をしたいのですがそんな場合はflatMapの出番です。
java
1static Stream<String> makeFilteredWords(Map<Integer, List<String>> wslGroup) {
2 return wslGroup.values().stream()
3 .flatMap(wsl -> select1(wsl));
4}
このあたりまで個々の論理がイメージ出来れば最後に全体を合成します。
コードが長ったらしくなるのでJava11以降前提とし、varとか新たなFileReaderを用いてみました。また前述したメソッドのうちひょっとしたら再利用するかも知れないhammngDistanceを除きメソッドとはせずにmainメソッドの中で閉じた処理にしています(select1をメソッドとする代わりにFunctionインスタンスにした)。
java
1package hammingdistance;
2
3import java.io.*;
4import java.nio.charset.StandardCharsets;
5import java.util.*;
6import java.util.function.Function;
7import java.util.stream.Collectors;
8import java.util.stream.IntStream;
9import java.util.stream.Stream;
10
11public class HammingDistance {
12 public static void main(String[] args) throws IOException {
13 var path = "words.csv";
14 var cs = StandardCharsets.UTF_8;
15 try (var br = new BufferedReader(new FileReader(path, cs))) {
16 Function<List<String>, Stream<String>> select1 = wsl -> wsl.stream()
17 .filter(word -> wsl.stream().anyMatch(w -> hammingDistance(word, w) == 1));
18 String[] words = br
19 .lines()
20 .collect(Collectors.groupingBy(String::length))
21 .values().stream()
22 .flatMap(select1)
23 .toArray(String[]::new);
24 for (var word : words) {
25 System.out.println(word);
26 }
27 }
28 }
29
30 private static int hammingDistance(String w1, String w2) {
31 assert w1.length() == w2.length(); // 前提
32 return IntStream.range(0, w1.length())
33 .map(i -> w1.charAt(i) != w2.charAt(i) ? 1 : 0)
34 .sum();
35 }
36}
テキトーに作成した1万語のランダムな単語に対し上記を実行すると1秒あまりかかりました。このスピードはあまり満足すべきものではありませんが、あくまで「素朴な論理で性能はあまり考えずに」という前提なのでよしとしました。
本回答は今後役に立つような何かのtipsを示せているとはいえず、「問題点が明確でない問いに対して答えだけ教えたよくない回答」のそしりをうけるかも知れません。書いていて自分でそう思いました。本当は「streamにはいくつかのお定まりのパターンがあること、それを頭の引き出しにためておいた状態でないと論理は書けないこと」を言おうとしたのですが・・・そんなコメントならしないほうがましに思えたので、どうせ書くなら「streamの論理を組み立てる雰囲気を伝えてみよう」と考えこのような回答をしてみた次第です。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/03/04 05:07
2019/03/05 03:51 編集
2019/03/04 16:19
2019/03/05 01:29 編集
2019/03/05 02:31 編集
2019/03/05 02:30
2019/03/05 02:31