2016-02-05 7 views
9

Ich frage mich, ob es in irgendeiner geschickten Weise die neuen Stream-APIs gibt, um Sequenzen von Werten zu "gruppieren".Gruppenfolgen von Werten

z.B. Split eine Reihe von ganzen Zahlen, in Gruppen von ganzen Zahlen, wobei jede Gruppe eine aufsteigende Zahlenfolge ist:

IntStream seq = IntStream.of(1, 2, 3, -1, -1, 1, 2, 1, 2); 
IntFunction next = i -> i + 1; 

// DESIRED OUTPUT: [[1,2,3], [-1], [-1], [1,2], [1,2]] 
+0

Sollte die Ausgabe nicht so aussehen: '[[1,2,3], [-1], [-1,1,2], [1,2]]'? – Flown

+3

@Flown Nein, weil '1! = Next.apply (-1)' – Tunaki

+0

Ah ok 'next' ist ein Prädikat. – Flown

Antwort

7

Leider ist der Stream-API nicht sehr gut geeignet ist, Probleme zu lösen, die abhängigen Operationen auf dem Stream-Elemente beinhaltet, wie dies ein.

Sie können jedoch die StreamEx Bibliothek für diesen Einsatz:

public static void main(String[] args) { 
    IntStream seq = IntStream.of(1, 2, 3, -1, -1, 1, 2, 1, 2); 
    IntUnaryOperator next = i -> i + 1; 

    List<List<Integer>> result = 
     IntStreamEx.of(seq).boxed().groupRuns((i1, i2) -> next.applyAsInt(i1) == i2).toList(); 

    System.out.println(result); // prints "[[1, 2, 3], [-1], [-1], [1, 2], [1, 2]]" 
} 

Diese Gruppen in eine List alle aufeinanderfolgenden ganzen Zahlen, wo der zweite auf die auf die erste aufgebracht next Funktion gleich ist. Schließlich wird dieser Strom in eine List gesammelt.

+0

Nicht so elegant wie deine Idee, aber es kann wirklich mit reinen Java-8-Streams gemacht werden. – Andremoniy

+0

Danke! Es sieht so aus, als würde StreamEx mir viele Kopfschmerzen ersparen! – rednoah

1

nicht so elegant wie @Tunaki Lösung, aber unter Verwendung von "reinen" Java-8-Streams:

IntStream seq = IntStream.of(1, 2, 3, -1, -1, 1, 2, 1, 2); 

Deque<Deque<Integer>> r = new ArrayDeque<>(singleton(new ArrayDeque<>())); 

seq.filter(i -> !r.getLast().isEmpty() && r.getLast().getLast() + 1 != i || !r.getLast().add(i)) 
      .forEach(i -> r.add(new ArrayDeque<>(singleton(i)))); 

System.out.println(r); // prints: [[1, 2, 3], [-1], [-1], [1, 2], [1, 2]] 

hier nur für elegancy Code I Deque Klasse verwenden, um getLast() Methode zu verwenden (für List wird es sei nicht so kompakt).

+2

Es sollte angemerkt werden, dass eine solche Lösung die API missbraucht (insbesondere "Prädikat", das an ".filter" übergeben wurde, muss gemäß [Spezifikation] zustandslos sein. (Https://docs.oracle.com/javase/8/docs/api/ java/util/stream/Stream.html # filter-java.util.function.Predicate-)). Folglich kann diese Lösung nicht parallelisiert werden. –

+0

@TagirValeev Kann Tanuki's eins parallelisiert werden? – Andremoniy

+1

Ja, und Sie werden wahrscheinlich eine Beschleunigung bei großen Eingaben haben. Jede StreamEx-Funktion behandelt die Parallelisierung korrekt und die meisten profitieren sogar von der Parallelisierung. –

6

Wenn Sie eine In-Memory-Datenstruktur wie ein Array oder eine Liste verwenden möchten, können Sie dies in Java 8 in nur wenigen Schritten tun. Dies kann unter Verwendung von Array-Programmiertechniken durchgeführt werden, wie in meiner answer to this question veranschaulicht. Mit einigen cleveren Conditionals, ähnlich wie in Flown's answer to this question, werden die Edge-Cases sauber gepflegt.

Die wichtigste Erkenntnis besteht darin zu erkennen, dass ein neues Segment (oder eine neue Gruppe) an jedem Punkt beginnt, an dem das gewünschte Prädikat nicht erfüllt ist. Das heißt, ein neues Segment beginnt mit seq[i-1] + 1 != seq[i]. Lassen Sie uns ein IntStream über den Eingang laufen und die Indizes für diese Eigenschaft filtern und speichern das Ergebnis in einigen Array x:

int[] seq = { 1, 2, 3, -1, -1, 1, 2, 1, 2 }; 
    int[] x = IntStream.range(1, seq.length) 
         .filter(i -> seq[i-1] + 1 != seq[i]) 
         .toArray(); 

was

[3, 4, 5, 7] 

Das gibt uns nur die Innen Grenzen der Segmente. Um Anfang und Ende der Segmente zu erhalten, müssen wir den Anfang des ersten Segments und das Ende des letzten Segments anheften. Wir stellen Sie den Indexbereich und einige conditionals zum Filter hinzufügen:

int[] x = IntStream.rangeClosed(0, seq.length) 
         .filter(i -> i == 0 || i == seq.length || 
            seq[i-1] + 1 != seq[i]) 
         .toArray(); 

    [0, 3, 4, 5, 7, 9] 

Nun jedes benachbarte Paar von Indizes ein Teilbereich des ursprünglichen Arrays ist. Wir können einen anderen Strom verwenden, um diese Unterbereiche zu extrahieren, um das gewünschte Ergebnis zu geben:

int[][] result = 
     IntStream.range(0, x.length - 1) 
       .mapToObj(i -> Arrays.copyOfRange(seq, x[i], x[i+1])) 
       .toArray(int[][]::new); 

    [[1, 2, 3], [-1], [-1], [1, 2], [1, 2]] 

Dies kann in eine Funktion extrahiert werden, die selbst ein „next“ -Funktion erfolgt, dass der nächste Wert in dem Segment berechnet. Das heißt, für jedes Element, wenn das Element zu seiner Rechten mit dem Ergebnis der nächsten Funktion übereinstimmt, befinden sich die Elemente im selben Segment; Ansonsten ist es eine Segmentgrenze.Hier ist der Code:

int[][] segments(int[] seq, IntUnaryOperator next) { 
    int[] x = IntStream.rangeClosed(0, seq.length) 
         .filter(i -> i == 0 || i == seq.length || 
           next.applyAsInt(seq[i-1]) != seq[i]) 
         .toArray(); 

    return IntStream.range(0, x.length - 1) 
        .mapToObj(i -> Arrays.copyOfRange(seq, x[i], x[i+1])) 
        .toArray(int[][]::new); 
} 

Sie es so nennen würde:

int[] seq = { 1, 2, 3, -1, -1, 1, 2, 1, 2 }; 
    System.out.println(Arrays.deepToString(segments(seq, i -> i + 1))); 

    [[1, 2, 3], [-1], [-1], [1, 2], [1, 2]] 

die nächste Funktion ändern, können die Segmente in einer anderen Art und Weise zu splitten. Zum Beispiel kann ein Array in Segmente gleicher Werte zu teilen, würde man dies tun:

int[] seq = { 2, 2, 1, 3, 3, 1, 1, 1, 4, 4, 4 }; 
    System.out.println(Arrays.deepToString(segments(seq, i -> i))); 

    [[2, 2], [1], [3, 3], [1, 1, 1], [4, 4, 4]] 

Die Schwierigkeit bei einer nächst Funktion wie diese ist, dass die Bedingung für die Werte zu einem Segment gehört, begrenzt ist. Es wäre schöner, ein Prädikat bereitzustellen, das mit benachbarten Werten verglichen wird, um zu testen, ob sie sich in demselben Segment befinden. Wir können tun, dass ein BiPredicate<Integer, Integer> verwenden, wenn wir bereit sind, die Kosten für die Boxen zu zahlen:

int[][] segments(int[] input, BiPredicate<Integer, Integer> pred) { 
    int[] x = IntStream.rangeClosed(0, input.length) 
         .filter(i -> i == 0 || i == input.length || 
           !pred.test(input[i-1], input[i])) 
         .toArray(); 

    return IntStream.range(0, x.length - 1) 
        .mapToObj(i -> Arrays.copyOfRange(input, x[i], x[i+1])) 
        .toArray(int[][]::new); 
} 

Dies ermöglicht Segmente Versammlung ein anderes Kriterium, beispielsweise mit monoton Segmente zu erhöhen:

int[] seq = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3 }; 
    System.out.println(Arrays.deepToString(segments(seq, (a, b) -> b > a))); 

    [[3], [1, 4], [1, 5, 9], [2, 6], [5], [3]] 

Diese könnte spezialisiert sein, um ein primitives Bi-Prädikat über zwei int Werte zu verwenden, oder es könnte verallgemeinert werden, um einen BiPredicate irgendeines Typs über Eingabe irgendeines Typs zu verwenden.