避免在Java 8 stream reduce方法

时间:2016-07-24 00:22:41

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

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

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表达式中的全局变量的最佳方法是什么。

3 个答案:

答案 0 :(得分:6)

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();
}

答案 1 :(得分: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;
}

答案 2 :(得分:1)

这里的问题是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. 它需要累积功能关联
  3. 这个文档很难解释,但我读它的方式就是这个。即使它可能无序地处理元素:

    • 如果您的累积功能是关联的然后,则可以保证产生与顺序处理流相同的结果;
    • 如果您的累积功能不是关联的,那么它可能会产生与简单顺序过程不同的结果。

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

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

    1. 将输入流拆分为连续的块;
    2. 并行处理多个块;
    3. 在完成时按顺序组合相邻块的结果。
    4. 因此,关联性和身份要求可以保证这种并行处理产生与顺序处理相同的结果。但这意味着虽然算法有一个简单的功能实现,但没有简单的方法用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);
                      }
                  }
              );
          });
      }
      

      ......这可能是非常慢的。