69

私はこのリストを持っています ( List<String>):

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

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

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

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

4

13 に答える 13

76

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

まず、議論と分析のために、従来の解決策を次に示します。

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 つのポイントは、保留中のサブリスト from prevtocurが常に開いていることです。遭遇したらnull、それを閉じて結果リストに追加し、先に進みprevます。ループの後、無条件にサブリストを閉じます。

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

これに気付くと、入力内の の各位置がサブリストの区切り文字であることがわかりますnull。これはサブリストの右端から左にあり、(プラス 1) はサブリストの左端から右。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]ますindexes.get(i)。第 2 に、配列を使用するとボックス化が回避されます。

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

討論

ストリーム アプローチは for ループ バージョンよりもわずかに短くなりますが、密度が高くなります。for ループ バージョンはよく知られています。これは常に Java で行われているためです。ただし、このループが何を行うべきかをまだ認識していない場合は、明らかではありません。何prevが行われているか、ループの終了後に開いているサブリストを閉じる必要がある理由を理解する前に、いくつかのループ実行をシミュレートする必要がある場合があります。(最初は忘れていましたが、テストでこれを見つけました。)

サブリスト間の境界を示すリスト (または配列) を取得します。これは簡単なストリーム 2 ライナーです。上で述べたように、問題は端にエッジ値を追加する方法を見つけることです。これを行うためのより良い構文があれば、たとえば、

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

物事がぐちゃぐちゃになりません。(実際に必要なのは、配列またはリストの理解です。) インデックスを取得したら、それらを実際のサブリストにマップし、それらを結果リストに収集するのは簡単なことです。

もちろん、これは並行して実行しても安全です。

更新 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" (ビデオ) という講演を行いました。そこに提示された問題は、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);
    }
}
于 2015-03-17T22:36:57.420 に答える
23

解決策は、を使用することStream.collectです。ビルダー パターンを使用して Collector を作成する方法は、既に解決策として示されています。代替手段は、オーバーロード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つ目は、前述のコンバイナーです。
于 2015-03-17T11:55:23.833 に答える
8

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

これは a と a を使用したソリューションですが、Streamこれforeachは 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 でより洗練されたソリューションを見つけたいと考えていることは理解していますが、このケース用に設計されていないと本当に思います。そして、スプーン氏が言ったように、この場合は単純な方法を強く好みます.

于 2015-03-17T11:16:06.857 に答える
5

Marks Stuartの答えは簡潔で、直感的で、並列に安全(そして最高)ですが、開始/終了境界のトリックを必要としない別の興味深い解決策を共有したいと思います。

問題のドメインを見て並列処理について考えると、分割統治戦略で簡単に解決できます。問題をトラバースしなければならない一連のリストとして考える代わりに、同じ基本的な問題の構成として問題を見ることができます: リストを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場合、このブランチは終了し、リストを返すだけです。すべての結果を連結すると、検索しているリストが返されます。

百聞は一見に如かず。

ここに画像の説明を入力

アルゴリズムはシンプルで完全です。リストの開始/終了の特殊なケースを処理するために特別なトリックは必要ありません。null空のリストや値のみのリストなどの特殊なケースを処理するために、特別なトリックは必要ありません。または で終わる、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() にすることができ、アルゴリズムは問題の機能分解のために引き続き機能することに注意してください。

コードはすでにかなり簡潔になっていますが、常にさまざまな方法で適応させることができます。例として、基本ケースでオプションの値をチェックする代わりに、 のorElseメソッドを利用OptionalIntしてリストの終了インデックスを返し、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)しかなく、リストのマッピング/分割にはかなりのコストがかかります)。副作用。

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

于 2016-11-13T11:47:01.080 に答える
4

これは、グループ化にリスト インデックスを利用するグループ化関数を使用する別のアプローチです。

ここでは、その要素に続く最初のインデックスで要素をグループ化し、 value を使用していますnull。したがって、あなたの例では、"a"and"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;
        }
    };
于 2015-03-17T11:07:33.193 に答える
3

これは非常に興味深い問題です。私は一行の解決策を思いつきました。パフォーマンスはあまり高くないかもしれませんが、機能します。

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());
于 2015-03-17T13:07:00.260 に答える
3

さて、ちょっとした作業の後、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();

ただし1行です!

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

System.out.println(result);

プロデュース:

[[a, b], [c], [d, e]]
于 2015-03-18T14:23:55.280 に答える
1

ここに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 の開発者です。

于 2016-11-24T17:06:30.303 に答える
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)
于 2016-12-08T19:02:28.463 に答える
0

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

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

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);
于 2017-01-19T13:57:38.173 に答える