【问题标题】:Avoid using global variable in Java 8 stream reduce method避免在 Java 8 流减少方法中使用全局变量
【发布时间】:2016-11-27 14:31:06
【问题描述】:

我正在尝试使用 Java 8 重写 Moore’s Voting Algorithm 的实现以在数组中查找多数元素。

Java 7 的实现将是这样的:

public int findCandidate(int[] nums) {

    int maj_index = 0, count = 1;
    for(int i=1; i<nums.length;i++){
        if(count==0){
            count++;
            maj_index=i;
        }else if(nums[maj_index]==nums[i]){
            count++;
        } else {
            count--;
        }
    }
    return nums[maj_index];
}

我能想到的方法是使用stream reduce得到最终结果

public int findCandidate(int[] nums) {
    int count = 1;
    Arrays
            .asList(nums)
            .stream()
            .reduce(0, (result, cur) -> {
                if (count == 0) {
                    result = cur;
                    count++;
                } else if (result == cur){
                    count++;
                } else {
                    count --;
                }
            });
    return result;
}

但是这种方法有编译错误,另外,它也破坏了函数纯粹主义,我多次遇到这种情况,那么处理lambda表达式中的全局变量的最佳方法是什么。

【问题讨论】:

  • 您可能希望使用可变对象而不是原始对象,但这并不是使用 lambda 表达式时的真正目标

标签: java algorithm functional-programming java-8 reduce


【解决方案1】:

@Stuart Marks 编写了简洁的代码。但是它仍然可以通过AbacusUtil进行简化

Stream.from(nums).groupBy(Function.identity(), Collectors.counting())
     .findAny(e -> e.getValue() > nums.length / 2)

披露:我是AbacusUtil的开发者。

【讨论】:

    【解决方案2】:

    您的问题是Java 流没有真正的list fold operation。使用真正的折叠操作,将函数编写为左折叠并不难。例如,在 Haskell 中:

    import Data.List (foldl')
    
    -- A custom struct to represent the state of the fold.
    data MooreState a = MooreState { candidate :: a, count :: !Int }
    
    findCandidate :: Eq a => [a] -> Maybe a
    findCandidate (first:rest) = Just result
        where 
          Moore result _ = foldl' combiner (MooreState first 1) rest                       
    
          combiner :: Eq a => MooreState a -> a -> MooreState a
          combiner (Moore candidate count) current
              | count == 0           = MooreState current 1
              | candidate == current = MooreState candidate (count + 1)
              | otherwise            = MooreState candidate (count - 1)
    
    -- The empty list has no candidates.
    findCandidate [] = Nothing
    

    Java 的 reduce() 方法最接近真正的左折叠,但如果您查看 the Javadoc for the reduce() method that you're using,您会注意到它说:

    1. 它“不限于顺序执行”;
    2. 要求累加函数关联

    这个文档真的很难解释,但我阅读它的方式是这样的。即使它可能会乱序处理元素:

    • 如果您的累积函数是关联的那么它保证产生与您按顺序处理流一样的结果;
    • 如果您的累积函数不是关联函数,那么它可能会产生不同于简单顺序过程的结果。

    为什么这很重要?嗯,首先,你正在改变一个外部变量的事实意味着你使用count 的方式被打破了。据你所知,流的元素 #7 可能在元素 #5 之前处理。

    更隐蔽的是,上述 Haskell 版本中的 combine 操作结合了不同类型的输入(Moore aa),但您使用的 Java reduce 方法基于 BinaryOperator&lt;T&gt;,它组合两个相同类型的对象。有another overload of reduce that uses a BiFunction&lt;U, T, U&gt;,但这需要您提供BinaryOperator&lt;U&gt; combiner 及其U identity。这是因为 Java 的 reduce 方法被设计成可以:

    1. 将输入流分成连续的块;
    2. 并行处理多个块;
    3. 在完成时按顺序组合相邻块的结果。

    因此,关联性和身份要求是为了保证这种并行处理产生的结果与您按顺序执行的结果相同。但这意味着虽然该算法有一个简单的功能实现,但没有直接的方法可以使用 Java 的 Stream 类型来编写它。 (有一种不直截了当的方法,但这会涉及到一些魔法,这将(a)非常复杂,(b)在 Java 中非常慢。)

    所以我个人会接受 Java 不是一种出色的函数式编程语言,不要管它足够好,并按原样使用命令式版本。但是如果出于某种奇怪的原因我真的坚持要在功能上这样做,我会选择像jOOλ 这样提供true left folds in Java 的库。然后你可以像 Haskell 解决方案一样做(未经测试的伪代码):

    import org.jooq.lambda.Seq;
    import org.jooq.lambda.tuple.Tuple2;
    
    class MooreState<A> {
        private final A candidate;
        private final int count;
        // ...constructors and getters...
    }
    
    public static Optional<A> findCandidate(Seq<A> elements) {
        Tuple2<Optional<A>, Seq<A>> split = elements.splitAtHead();
        return split.v1().map(first -> {
            Seq<A> rest = split.v2();
            return rest.foldLeft(
                new MooreState<>(first, 1),
                (state, current) -> {
                    if (state.getCount() == 0) {
                        return new MooreState<>(current, 1);
                    } else if (state.getCandidate().equals(current) {
                        return new MooreState<>(state.getCandidate(),
                                                state.getCount() + 1);
                    } else {
                        return new MooreState<>(state.getCandidate(),
                                                state.getCount() - 1);
                    }
                }
            );
        });
    }
    

    ...这可能非常慢。

    【讨论】:

      【解决方案3】:

      Yassin Hajaj's answer 展示了一些非常好的流技术。 (+1)从根本上说,我认为它使用了正确的方法。不过,可以对其进行一些小的改进。

      第一个变化是使用counting() 收集器来计算每个组中的项目,而不是将它们累积到列表中。由于我们正在寻找多数,我们只需要计数,而不是实际元素,并且我们避免了比较列表的长度。

      第二个更改是过滤列表以查找计数为多数的组。根据定义,最多可以有一个,因此我们只需使用此谓词过滤映射条目,并使用findAny 而不是max 终止流。

      第三个变化是让函数返回OptionalInt,这更符合它的意图。 OptionalInt 要么包含多数值,要么在没有多数值时为空。这避免了必须使用可能实际出现在数据中的标记值,例如 -1。由于findAny 返回OptionalInt,我们就完成了。

      最后,我在几个地方都依赖于静态导入。这主要是风格问题,但我认为它清理了一些代码。

      这是我的变体:

      static OptionalInt majority(int... nums) {
          Map<Integer, Long> map =
              Arrays.stream(nums)
                    .boxed()
                    .collect(groupingBy(x -> x, counting()));
      
          return map.entrySet().stream()
                    .filter(e -> e.getValue() > nums.length / 2)
                    .mapToInt(Entry::getKey)
                    .findAny();
      }
      

      【讨论】:

      • @LalitRao 谢谢!
      【解决方案4】:

      就像我在评论中告诉你的那样,在你的 lambda 表达式中使用可变对象是不行的。但是在你的情况下,如果你真的想应用相同的算法,那就很难了。

      这是一个和你想要的一样的,如果没有找到多数,它返回-1

      public static int findCandidate(int ... nums) {
          Map<Integer, List<Integer>> map =
          Arrays.stream(nums)
                .boxed()
                .collect(Collectors.groupingBy(x -> x));
          int value = 
                map
                .entrySet().stream()
                .max((e1, e2) -> Integer.compare(e1.getValue().size(), e2.getValue().size()))
                .map(e -> e.getKey())
                .get();
          int result = map.get(value).size();
          return result > nums.length / 2 ? value : -1;
      }
      

      【讨论】:

        猜你喜欢
        • 2012-12-19
        • 1970-01-01
        • 1970-01-01
        • 2019-04-12
        • 1970-01-01
        • 2014-05-03
        • 1970-01-01
        • 2020-09-02
        • 1970-01-01
        相关资源
        最近更新 更多