如何使用Java Stream API解决滑动窗口问题?

时间:2019-11-17 10:33:01

标签: java algorithm java-stream sliding-window

自Java 8开始,使用Stream API的情况就很普遍。但是,使用基于批处理的算法可以轻松解决一些问题,而使用基于流的解决方案则可能无法轻松解决这些问题。

例如,给定一张信用卡按时间顺序进行的交易流,我希望在每24小时内次找到该卡的总交易金额,以便将其与阈值进行比较猜测卡是否被盗。 流数据可以很简单

transaction time            amount
2019-01-23T10:12:31.484Z    100
2019-01-24T00:12:30.004Z    50
2019-01-24T09:00:00.000Z    23
2019-01-27T05:10:00.300Z    65

这可以看作是滑动窗口问题,需要检查元素之间的关系。基于批处理的解决方案不是很复杂。我可以使用队列将所有交易保持在24小时之内。 该算法可以通过以下步骤大致描述:

  1. 创建一个队列并将第一个事务放入队列;

  2. 将下一个事务与队列头事务进行比较。

  3. 检查2笔交易的交易时间差

    如果时间差小于24小时,

  4. 将事务添加到队列中并返回到步骤2。

    否则,如果超过24小时,则

    5.1。计算那些在24小时内发生的交易中队列中交易的总交易量

    5.2将结果放入结果列表。

    5.3轮询队列以删除最早的事务,直到新事务少于24小时且队列中有事务。

    5.4循环至步骤2。

但是,我发现使用Java Stream API来实现上述算法很困难,所以我想知道使用Java流实现滑动窗口问题是一个好主意吗?如果是,那么有人可以使用Java Stream来实现一些提示或伪代码吗?不必使用上述算法。任何基于流的算法都可以。

2 个答案:

答案 0 :(得分:0)

这是一个示例,如何使用reduce完成。 用于收集数据的类已创建:

class Row {

    private LocalDateTime date;
    private Integer value;

    public Row(LocalDateTime date, Integer value) {
        this.date = date;
        this.value = value;
    }

    // getters and setters

首先将示例数据读入流。

Stream<Row> readData = Stream.of(
        "2019-01-23T10:12:31    100",
        "2019-01-24T00:12:30    50",
        "2019-01-24T09:00:00    23",
        "2019-01-25T03:00:00    23",
        "2019-01-27T05:10:00    65")

        .map(s -> s.split("\\s+"))
        .map(a -> new Row(LocalDateTime.parse(a[0], DateTimeFormatter.ISO_LOCAL_DATE_TIME), Integer.valueOf(a[1])));

比起reduce方法,每隔24小时都要收集一行以分隔的列表并将其存储在主列表中。

List<List<Row>> all = new ArrayList<>();
all.add(readData
        .map(Arrays::asList)
        .reduce(new ArrayList<>(), (a, v) -> {
            if (a.isEmpty()) {
                a.addAll(v);
            } else {
                LocalDateTime first = a.get(0).getDate().plusHours(24);
                if (first.isAfter(v.get(0).getDate())) {
                    a.addAll(v);
                } else {
                    all.add(a);

                    LocalDateTime last = v.get(0).getDate().minusHours(24);
                    a = new ArrayList<>(a.stream()
                            .filter(r -> last.isBefore(r.getDate()))
                            .collect(Collectors.toList()));
                    a.addAll(v);

                }
            }
            return a;
        }));

最后,您可以打印出所有列表或计算每个期间的交易额。

all.forEach(System.out::println);

all.stream().map(l -> l.stream()
        .map(Row::getValue)
        .reduce(Integer::sum)
        .get()
)
.forEach(System.out::println);

更新

reduce也可以替换为简单的forEach。 然后,主要部分将是这样,并且将不再那么复杂:

LinkedList<List<Row>> all = new LinkedList<>(Arrays.asList(new ArrayList<>()));
readData
        .forEach(v -> {
            if (!all.getLast().isEmpty()) {
                // check if in 24h boundary
                LocalDateTime upperValue = all.getLast().get(0).getDate().plusHours(24);
                if (!upperValue.isAfter(v.getDate())) {
                    // create copy with row older earlier than 24h
                    LocalDateTime lowerValue = v.getDate().minusHours(24);
                    all.add(new ArrayList<>(all.getLast().stream()
                            .filter(r -> lowerValue.isBefore(r.getDate()))
                            .collect(Collectors.toList())));
                }
            }
            all.getLast().add(v);
        });

答案 1 :(得分:0)

通过流执行此操作的“问题”是,它们被设计为易于并行化。根据设计,它们不是仅顺序的,因此,纯粹的顺序算法(如基于队列的算法一样)不适用于Streams。这是从命令式编程到函数式编程的飞跃,有时您需要一种新的算法,新的方法。将命令性代码转换为功能性并不总是那么容易。

如果您的数据源可以轻松地split多次(我认为,这等效于能够高效且同时提供所需的窗口),则可以使用Streams做到这一点。例如,如果您的数据源是List,则可以(重新)使用StreamExofSublists()方法。之所以可行,是因为任何子列表的创建都是有效的,并且与其他子列表无关,因此可以安全地并发调用它。

或者,您可以假设您的Stream永远不会并行运行,并使用专注于纯序列的内容,例如jOOLSeq.sliding()。这对任何Seq都有效,因为实现可以使用迭代状态而不必担心并行性。