我想知道是否有任何方式可以使用新的Stream API来" group"价值序列。
e.g。将一系列整数拆分为整数组,其中每个组都是一个递增的数字序列:
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 :(得分:7)
不幸的是,Stream API不太适合解决涉及Stream元素的依赖操作的问题,例如此问题。
但是,您可以使用StreamEx库:
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]]"
}
这将所有连续的整数分组为List
,其中第二个整数等于应用于第一个的next
函数。最后,此流被收集到List
。
答案 1 :(得分:6)
如果您愿意使用内存数据结构(例如数组或列表),只需几步即可在标准Java 8中执行此操作。这可以使用我的answer to this question中所示的数组编程技术来完成。使用类似于Flown's answer to this question中使用的一些聪明条件,可以整齐地处理边缘情况。
关键的洞察力是要意识到新的片段(或群组)从所需谓词不满足的每个点开始。也就是说,新的细分市场开始于seq[i-1] + 1 != seq[i]
。让我们在输入上运行IntStream
并过滤此属性的索引,并将结果存储在某个数组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();
导致
[3, 4, 5, 7]
这只为我们提供了细分的内部边界。要获得段的开始和结束,我们需要确定第一段的开始和最后一段的结束。我们调整索引范围并向过滤器添加一些条件:
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]
现在每个相邻的索引对都是原始数组的子范围。我们可以使用另一个流来提取这些子范围,从而得到所需的结果:
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]]
这可以被提取到一个函数中,该函数本身采用“下一个”函数来计算段中的下一个值。也就是说,对于任何元素,如果右边的元素与下一个函数的结果匹配,则元素在同一个段中;否则它是一个分段边界。这是代码:
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);
}
你会这样称呼它:
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]]
更改下一个功能允许以不同的方式拆分段。例如,要将数组拆分为相等值的段,您可以这样做:
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]]
使用这样的下一个函数的困难在于属于段的值的条件是有限的。提供一个与相邻值进行比较以测试它们是否在同一段中的谓词会更好。如果我们愿意支付拳击费用,我们可以使用BiPredicate<Integer, Integer>
来做到这一点:
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);
}
这允许使用不同的标准收集细分,例如,单调递增细分:
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]]
这可以专门用于在两个int
值上使用原始双谓词,或者可以推广到允许使用任何类型的BiPredicate
而不是任何类型的输入。
答案 2 :(得分:1)
不像@Tunaki解决方案那么优雅,但使用&#34; pure&#34; Java-8流:
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]]
这里只是为了优雅的代码,我使用Deque
类来使用getLast()
方法(对于List
,它将不那么紧凑)。