web-dev-qa-db-ja.com

リストを要素に沿ってサブリストに分割する

このリストがあります(List<String>):

["a", "b", null, "c", null, "d", "e"]

そして、私はこのようなものが欲しいです:

[["a", "b"], ["c"], ["d", "e"]]

つまり、リストのリストを取得するために、null値をセパレータとして使用して、リストをサブリストに分割します(List<List<String>>)。 Java 8のソリューションを探しています。Collectors.partitioningByしかし、それが私が探しているものかどうかはわかりません。ありがとう!

65
Oneiros

すでにいくつかの答えがあり、受け入れられた答えがありますが、このトピックにはまだいくつかのポイントがありません。まず、コンセンサスは、ストリームを使用してこの問題を解決することは単なる練習であり、従来のforループアプローチが望ましいということです。第二に、これまでに与えられた答えは、配列またはベクトルスタイルの手法を使用するアプローチを見落としていました。これは、ストリームソリューションを大幅に改善すると思います。

まず、議論と分析を目的とした従来のソリューションを次に示します。

_static List<List<String>> splitConventional(List<String> input) {
    List<List<String>> result = new ArrayList<>();
    int prev = 0;

    for (int cur = 0; cur < input.size(); cur++) {
        if (input.get(cur) == null) {
            result.add(input.subList(prev, cur));
            prev = cur + 1;
        }
    }
    result.add(input.subList(prev, input.size()));

    return result;
}
_

これはほとんど簡単ですが、少し微妙です。 1つのポイントは、prevからcurへの保留中のサブリストが常に開いていることです。 nullが見つかったら、それを閉じて結果リストに追加し、prevを進めます。ループの後、サブリストを無条件に閉じます。

別の観察では、これは値自体ではなくインデックスのループであるため、拡張された「for-each」ループの代わりに算術forループを使用します。しかし、値をストリーミングしてロジックをコレクターに入れる代わりに、インデックスを使用してストリーミングしてサブレンジを生成できることを示唆しています( Joop Eggenの提案したソリューション によって行われたように)。

それに気付くと、入力内のnullの各位置がサブリストの区切り文字であることがわかります。サブリストの左端は右端であり、(プラス1)は左です。右側のサブリストの終わり。 Edgeのケースを処理できる場合、null要素が発生するインデックスを見つけてサブリストにマッピングし、サブリストを収集するアプローチにつながります。

結果のコードは次のとおりです。

_static List<List<String>> splitStream(List<String> input) {
    int[] indexes = Stream.of(IntStream.of(-1),
                              IntStream.range(0, input.size())
                                       .filter(i -> input.get(i) == null),
                              IntStream.of(input.size()))
                          .flatMapToInt(s -> s)
                          .toArray();

    return IntStream.range(0, indexes.length-1)
                    .mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1]))
                    .collect(toList());
}
_

nullが発生するインデックスを取得するのは非常に簡単です。つまずきは、左端に_-1_を追加し、右端にsizeを追加しています。 _Stream.of_を使用して追加を実行し、flatMapToIntを使用してそれらを平坦化することを選択しました。 (私はいくつかの他のアプローチを試しましたが、これは最もクリーンなように見えました。)

ここでは、インデックスに配列を使用する方が少し便利です。まず、配列にアクセスするための表記法は、リストの場合よりも優れています:_indexes[i]_ vs. indexes.get(i)。第二に、配列を使用すると、ボクシングが回避されます。

この時点で、配列内の各インデックス値(最後を除く)は、サブリストの開始位置より1つ小さくなります。すぐ右のインデックスはサブリストの終わりです。単純に配列をストリーミングして、インデックスの各ペアをサブリストにマッピングし、出力を収集します。

ディスカッション

ストリームアプローチはforループバージョンよりわずかに短いですが、より高密度です。 for-loopバージョンはおなじみです。なぜなら、この処理はJavaで常に行われますが、このループが何をしているのかわからない場合は、明らかではありません。 prevが何をしているか、そしてループの終了後になぜ開いているサブリストを閉じなければならないかを理解する前に、いくつかのループ実行をシミュレートする必要があるかもしれません。テスト中。)

ストリームアプローチは、起こっていることを概念化するのが簡単だと思います。サブリスト間の境界を示すリスト(または配列)を取得します。それは簡単なストリームの2ライナーです。先に述べたように、困難なのは、エッジの値を端に固定する方法を見つけることです。これを行うためのより良い構文があった場合、例えば、

_    // Java plus Pidgin Scala
    int[] indexes =
        [-1] ++ IntStream.range(0, input.size())
                         .filter(i -> input.get(i) == null) ++ [input.size()];
_

物事がすっきりします。 (本当に必要なのは配列またはリストの内包です。)インデックスを取得したら、それらを実際のサブリストにマップして結果リストに収集するのは簡単です。

そしてもちろん、これは並行して実行する場合に安全です。

UPDATE 2016-02-06

サブリストインデックスの配列を作成するより良い方法を次に示します。同じ原理に基づいていますが、インデックス範囲を調整し、フィルターに条件を追加して、インデックスを連結およびフラットマップする必要を回避します。

_static List<List<String>> splitStream(List<String> input) {
    int sz = input.size();
    int[] indexes =
        IntStream.rangeClosed(-1, sz)
                 .filter(i -> i == -1 || i == sz || input.get(i) == null)
                 .toArray();

    return IntStream.range(0, indexes.length-1)
                    .mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1]))
                    .collect(toList());
}
_

2016-11-23更新

Devoxx Antwerp 2016でBrian Goetzとの共同発表「Thinking In Parallel」( video )で、この問題と私のソリューションを取り上げました。そこに提示されている問題には、nullではなく「#」で分割されるわずかなバリエーションがありますが、それ以外は同じです。講演の中で、私はこの問題に対する多数の単体テストがあると述べました。ループとストリームの実装とともに、スタンドアロンプ​​ログラムとしてそれらを以下に追加しました。読者にとって興味深い演習は、ここで提供したテストケースに対して他の回答で提案されたソリューションを実行し、どのテストが失敗し、なぜなのかを確認することです。 (他のソリューションは、nullで分割するのではなく、述語に基づいて分割するように適合させる必要があります。)

_import Java.util.*;
import Java.util.function.*;
import Java.util.stream.*;

import static Java.util.Arrays.asList;

public class ListSplitting {
    static final Map<List<String>, List<List<String>>> TESTCASES = new LinkedHashMap<>();
    static {
        TESTCASES.put(asList(),
                  asList(asList()));
        TESTCASES.put(asList("a", "b", "c"),
                  asList(asList("a", "b", "c")));
        TESTCASES.put(asList("a", "b", "#", "c", "#", "d", "e"),
                  asList(asList("a", "b"), asList("c"), asList("d", "e")));
        TESTCASES.put(asList("#"),
                  asList(asList(), asList()));
        TESTCASES.put(asList("#", "a", "b"),
                  asList(asList(), asList("a", "b")));
        TESTCASES.put(asList("a", "b", "#"),
                  asList(asList("a", "b"), asList()));
        TESTCASES.put(asList("#"),
                  asList(asList(), asList()));
        TESTCASES.put(asList("a", "#", "b"),
                  asList(asList("a"), asList("b")));
        TESTCASES.put(asList("a", "#", "#", "b"),
                  asList(asList("a"), asList(), asList("b")));
        TESTCASES.put(asList("a", "#", "#", "#", "b"),
                  asList(asList("a"), asList(), asList(), asList("b")));
    }

    static final Predicate<String> TESTPRED = "#"::equals;

    static void testAll(BiFunction<List<String>, Predicate<String>, List<List<String>>> f) {
        TESTCASES.forEach((input, expected) -> {
            List<List<String>> actual = f.apply(input, TESTPRED);
            System.out.println(input + " => " + expected);
            if (!expected.equals(actual)) {
                System.out.println("  ERROR: actual was " + actual);
            }
        });
    }

    static <T> List<List<T>> splitStream(List<T> input, Predicate<? super T> pred) {
        int[] edges = IntStream.range(-1, input.size()+1)
                               .filter(i -> i == -1 || i == input.size() ||
                                       pred.test(input.get(i)))
                               .toArray();

        return IntStream.range(0, edges.length-1)
                        .mapToObj(k -> input.subList(edges[k]+1, edges[k+1]))
                        .collect(Collectors.toList());
    }

    static <T> List<List<T>> splitLoop(List<T> input, Predicate<? super T> pred) {
        List<List<T>> result = new ArrayList<>();
        int start = 0;

        for (int cur = 0; cur < input.size(); cur++) {
            if (pred.test(input.get(cur))) {
                result.add(input.subList(start, cur));
                start = cur + 1;
            }
        }
        result.add(input.subList(start, input.size()));

        return result;
    }

    public static void main(String[] args) {
        System.out.println("===== Loop =====");
        testAll(ListSplitting::splitLoop);
        System.out.println("===== Stream =====");
        testAll(ListSplitting::splitStream);
    }
}
_
65
Stuart Marks

解決策は、Stream.collectを使用することです。そのビルダーパターンを使用してコレクターを作成することは、既にソリューションとして提供されています。代替案は、もう少しオーバーロードされたcollectがもう少しプリミティブです。

    List<String> strings = Arrays.asList("a", "b", null, "c", null, "d", "e");
    List<List<String>> groups = strings.stream()
            .collect(() -> {
                List<List<String>> list = new ArrayList<>();
                list.add(new ArrayList<>());
                return list;
            },
            (list, s) -> {
                if (s == null) {
                    list.add(new ArrayList<>());
                } else {
                    list.get(list.size() - 1).add(s);
                }
            },
            (list1, list2) -> {
                // Simple merging of partial sublists would
                // introduce a false level-break at the beginning.
                list1.get(list1.size() - 1).addAll(list2.remove(0));
                list1.addAll(list2);
            });

ご覧のとおり、文字列リストのリストを作成します。このリストには、常に少なくとも1つの最後の(空の)文字列リストがあります。

  • 最初の関数は、文字列リストの開始リストを作成します。 結果(型付き)オブジェクトを指定します。
  • 2番目の関数は、各要素を処理するために呼び出されます。 部分的な結果と要素に対するアクションです
  • 3番目は実際には使用されません。部分的な結果を結合する必要がある場合、処理の並列化に役立ちます。

アキュムレータを使用したソリューション:

@ StuartMarksが指摘しているように、コンバイナは並列処理のコントラクトをフルフィルしません。

@ArnaudDenoyelleのコメントのため、reduceを使用したバージョン。

    List<List<String>> groups = strings.stream()
            .reduce(new ArrayList<List<String>>(),
                    (list, s) -> {
                        if (list.isEmpty()) {
                            list.add(new ArrayList<>());
                        }
                        if (s == null) {
                            list.add(new ArrayList<>());
                        } else {
                            list.get(list.size() - 1).add(s);
                        }
                        return list;
                    },
                    (list1, list2) -> {
                            list1.addAll(list2);
                            return list1;
                    });
  • 最初のパラメーターは累積オブジェクトです。
  • 2番目の関数は累積します。
  • 3番目は前述のコンバイナです。
23
Joop Eggen

投票しないでください。これをコメントで説明するのに十分な場所がありません

これはStreamforeachを使用したソリューションですが、これはAlexisのソリューションまたはforeachループと厳密に同等です(あまり明確ではないため、削除できませんでしたコピーコンストラクター):

List<List<String>> result = new ArrayList<>();
final List<String> current = new ArrayList<>();
list.stream().forEach(s -> {
      if (s == null) {
        result.add(new ArrayList<>(current));
        current.clear();
      } else {
        current.add(s);
      }
    }
);
result.add(current);

System.out.println(result);

Java 8を使用してよりエレガントなソリューションを見つけたいと思いますが、このケース用に設計されたものではないと本当に思います。スプーン氏が言ったように、この場合。

8

Marks Stuartの answer は簡潔で直感的で並列安全です(そして最高の)ですが、そうではない別の興味深いソリューションを共有したいと思います。開始/終了境界のトリックが必要です。

問題の領域を見て、並列性について考えると、分割統治戦略でこれを簡単に解決できます。問題を横断する必要があるシリアルリストとして考える代わりに、同じ基本的な問題の構成として問題を見ることができます:null値でリストを分割します。次の再帰的戦略を使用して、再帰的に問題を解決できることが直観的にわかります。

split(L) :
  - if (no null value found) -> return just the simple list
  - else -> cut L around 'null' naming the resulting sublists L1 and L2
            return split(L1) + split(L2)

この場合、最初にnull値を検索し、その値を見つけるとすぐにリストを切り取り、サブリストで再帰呼び出しを呼び出します。 null(基本ケース)が見つからない場合、このブランチは終了し、リストを返します。すべての結果を連結すると、検索中のリストが返されます。

写真は千の言葉に値する:

enter image description here

アルゴリズムはシンプルで完全です。リストの開始/終了のエッジのケースを処理するための特別なトリックは必要ありません。空のリストやnull値のみのリストなど、Edgeのケースを処理するための特別なトリックは必要ありません。または、nullで終わるリストまたはnullで始まるリスト。

この戦略の単純な単純な実装は次のようになります。

public List<List<String>> split(List<String> input) {

    OptionalInt index = IntStream.range(0, input.size())
                                 .filter(i -> input.get(i) == null)
                                 .findAny();

    if (!index.isPresent())
        return asList(input);

    List<String> firstHalf  = input.subList(0, index.getAsInt());
    List<String> secondHalf = input.subList(index.getAsInt()+1, input.size());

    return asList(firstHalf, secondHalf).stream()
                 .map(this::split)
                 .flatMap(List::stream)
                 .collect(toList());

}

リスト内のnull値のインデックスを最初に検索します。見つからない場合は、リストを返します。見つかった場合は、リストを2つのサブリストに分割し、それらの上にストリーミングして、splitメソッドを再帰的に呼び出します。結果の副問題のリストが抽出され、戻り値のために結合されます。

2つのストリームを簡単にparallel()にすることができ、問題の機能的な分解のためにアルゴリズムが引き続き機能することに注意してください。

コードはすでにかなり簡潔ですが、さまざまな方法でいつでも適合させることができます。例のために、基本ケースのオプション値をチェックする代わりに、orElseOptionalIntメソッドを利用してリストの終了インデックスを返し、 2番目のストリームを再利用し、さらに空のリストを除外します。

public List<List<String>> split(List<String> input) {

    int index =  IntStream.range(0, input.size())
                          .filter(i -> input.get(i) == null)
                          .findAny().orElse(input.size());

    return asList(input.subList(0, index), input.subList(index+1, input.size())).stream()
                 .map(this::split)
                 .flatMap(List::stream)
                 .filter(list -> !list.isEmpty())
                 .collect(toList());
}

この例は、再帰アプローチの単なる単純さ、適応性、および優雅さを示すためにのみ与えられています。実際、このバージョンでは、パフォーマンスがわずかに低下し、入力が空の場合は失敗します(および追加の空のチェックが必要な場合があります).

この場合、再帰はおそらく最良の解決策ではないかもしれません( Stuart Marks インデックスを見つけるアルゴリズムはO(N)とマッピング/分割のみですリストにはかなりのコストがかかります)が、副作用のないシンプルで直感的な並列化可能なアルゴリズムでソリューションを表現します。

複雑さや利点/欠点、または停止条件や結果の一部が利用可能なユースケースについては、これ以上深く掘り下げません。他のアプローチは単に反復的であるか、並列化できない非常に複雑なソリューションアルゴリズムを使用しているため、このソリューション戦略を共有する必要性を感じました。

4

次に、グループ化機能を使用する別のアプローチを示します。これは、グループ化にリストインデックスを使用します。

ここでは、値をnullにして、その要素に続く最初のインデックスで要素をグループ化しています。したがって、あなたの例では、"a""b"2にマッピングされます。また、null値を-1インデックスにマッピングしていますが、これは後で削除する必要があります。

List<String> list = Arrays.asList("a", "b", null, "c", null, "d", "e");

Function<String, Integer> indexGroupingFunc = (str) -> {
             if (str == null) {
                 return -1;
             }
             int index = list.indexOf(str) + 1;
             while (index < list.size() && list.get(index) != null) {
                 index++;
             }
             return index;
         };

Map<Integer, List<String>> grouped = list.stream()
               .collect(Collectors.groupingBy(indexGroupingFunc));

grouped.remove(-1);  // Remove null elements grouped under -1
System.out.println(grouped.values()); // [[a, b], [c], [d, e]]

また、現在の最小インデックスをnullにキャッシュすることで、毎回AtomicInteger要素の最初のインデックスを取得することを回避できます。更新されたFunctionは次のようになります。

AtomicInteger currentMinIndex = new AtomicInteger(-1);

Function<String, Integer> indexGroupingFunc = (str) -> {
        if (str == null) {
            return -1;
        }
        int index = names.indexOf(str) + 1;

        if (currentMinIndex.get() > index) {
            return currentMinIndex.get();
        } else {
            while (index < names.size() && names.get(index) != null) {
              index++;
            }
            currentMinIndex.set(index);
            return index;
        }
    };
4
Rohit Jain

これは非常に興味深い問題です。 1行のソリューションを思い付きました。パフォーマンスはあまり良くないかもしれませんが、動作します。

List<String> list = Arrays.asList("a", "b", null, "c", null, "d", "e");
Collection<List<String>> cl = IntStream.range(0, list.size())
    .filter(i -> list.get(i) != null).boxed()
    .collect(Collectors.groupingBy(
        i -> IntStream.range(0, i).filter(j -> list.get(j) == null).count(),
        Collectors.mapping(i -> list.get(i), Collectors.toList()))
    ).values();

@Rohit Jainが思いついたのも同様の考えです。 null値の間のスペースをグループ化しています。本当に必要な場合はList<List<String>>追加できます:

List<List<String>> ll = cl.stream().collect(Collectors.toList());
3
Flown

さて、少しの作業の後、Uは1行のストリームベースのソリューションを思い付きました。最終的にはreduce()を使用してグループ化を行いますが、これは自然な選択のように見えましたが、文字列をList<List<String>>reduceで必要:

List<List<String>> result = list.stream()
  .map(Arrays::asList)
  .map(x -> new LinkedList<String>(x))
  .map(Arrays::asList)
  .map(x -> new LinkedList<List<String>>(x))
  .reduce( (a, b) -> {
    if (b.getFirst().get(0) == null) 
      a.add(new LinkedList<String>());
    else
      a.getLast().addAll(b.getFirst());
    return a;}).get();

それisしかし1行!

質問からの入力で実行すると、

System.out.println(result);

生産物:

[[a, b], [c], [d, e]]
2
Bohemian

AbacusUtil によるコードです

List<String> list = N.asList(null, null, "a", "b", null, "c", null, null, "d", "e");
Stream.of(list).splitIntoList(null, (e, any) -> e == null, null).filter(e -> e.get(0) != null).forEach(N::println);

宣言:私はAbacusUtilの開発者です。

1
user_3380739

私の StreamEx ライブラリには、これを解決するのに役立つ groupRuns メソッドがあります。

List<String> input = Arrays.asList("a", "b", null, "c", null, "d", "e");
List<List<String>> result = StreamEx.of(input)
        .groupRuns((a, b) -> a != null && b != null)
        .remove(list -> list.get(0) == null).toList();

groupRunsメソッドはBiPredicateを取り、隣接する要素のペアに対して、グループ化する必要がある場合にtrueを返します。その後、nullを含むグループを削除し、残りをリストに収集します。

このソリューションは並列処理に適しています。並列ストリームにも使用できます。また、ストリームソース(他のソリューションのようなランダムアクセスリストだけでなく)でもうまく機能し、コレクターベースのソリューションよりも優れています。

0
Tagir Valeev

StuartのThinking in Parallelに関するビデオを見ていました。それで、ビデオで彼の反応を見る前にそれを解決することに決めました。時間とともにソリューションを更新します。今のところ

Arrays.asList(IntStream.range(0, abc.size()-1).
filter(index -> abc.get(index).equals("#") ).
map(index -> (index)).toArray()).
stream().forEach( index -> {for (int i = 0; i < index.length; i++) {
                    if(sublist.size()==0){
                        sublist.add(new ArrayList<String>(abc.subList(0, index[i])));
                    }else{

                    sublist.add(new ArrayList<String>(abc.subList(index[i]-1, index[i])));
                    }
                }
    sublist.add(new ArrayList<String>(abc.subList(index[index.length-1]+1, abc.size())));
});
0

Stringを使用すると、次のことができます。

String s = ....;
String[] parts = s.split("sth");

すべての順次コレクション(文字列は文字のシーケンスであるため)にこの抽象化がある場合、これもそれらに対して実行可能です。

List<T> l = ...
List<List<T>> parts = l.split(condition) (possibly with several overloaded variants)

元の問題を文字列のリストに制限する(およびその要素のコンテンツにいくつかの制限を課す)場合、次のようにハックできます。

String als = Arrays.toString(new String[]{"a", "b", null, "c", null, "d", "e"});
String[] sa = als.substring(1, als.length() - 1).split("null, ");
List<List<String>> res = Stream.of(sa).map(s -> Arrays.asList(s.split(", "))).collect(Collectors.toList());

(しかし、真剣に受け取らないでください:))

それ以外の場合、単純な古い再帰も機能します。

List<List<String>> part(List<String> input, List<List<String>> acc, List<String> cur, int i) {
    if (i == input.size()) return acc;
    if (input.get(i) != null) {
        cur.add(input.get(i));
    } else if (!cur.isEmpty()) {
        acc.add(cur);
        cur = new ArrayList<>();
    }
    return part(input, acc, cur, i + 1);
}

(この場合、入力リストにnullを追加する必要があることに注意してください)

part(input, new ArrayList<>(), new ArrayList<>(), 0)
0
janek

ヌル(またはセパレーター)を見つけるたびに、異なるトークンでグループ化します。ここで別の整数を使用しました(アトミックをホルダーとして使用しました)

次に、生成されたマップを再マップして、リストのリストに変換します。

AtomicInteger i = new AtomicInteger();
List<List<String>> x = Stream.of("A", "B", null, "C", "D", "E", null, "H", "K")
      .collect(Collectors.groupingBy(s -> s == null ? i.incrementAndGet() : i.get()))
      .entrySet().stream().map(e -> e.getValue().stream().filter(v -> v != null).collect(Collectors.toList()))
      .collect(Collectors.toList());

System.out.println(x);
0
Shadi Moadad