【问题标题】:Drop values to keep only N occurrences删除值以仅保留 N 次出现
【发布时间】:2018-03-27 20:13:18
【问题描述】:

我今天正在做一些来自 Codewars 的 katas。我必须编写一个函数,它只保留数组中的 N 个相同元素,例如:

{1,2,3,4,1}, N=1 -> {1,2,3,4}
{2,2,2,2}, N=2 -> {2,2}

我提出了使用流的解决方案:

 public static int[] deleteNth(int[] elements, int maxOcurrences) {
    List<Integer> ints = Arrays.stream(elements)
                               .boxed()
                               .collect(Collectors.toList());
    return ints.stream().filter(x -> Collections.frequency(ints, x) <= maxOcurrences)
        .mapToInt(Integer::intValue)
        .toArray();
}

因此,首先将 int 更改为 Integers,然后在 freq 大于 N 时进行过滤。 但这不起作用,因为重复元素具有相同的频率,无论它们的位置如何。看起来值在过滤器调用之后被过滤了。如何解决此问题以获得正确的值?

PS:我知道那是 O(n^2),但这对我来说不是问题。

【问题讨论】:

  • 是的,超出限制的出现应该被丢弃并且应该保持顺序

标签: java-8 java-stream


【解决方案1】:

我发现完成手头任务的解决方案如下:

public static int[] deleteNth(int[] elements, int maxOccurrences) {
    return Arrays.stream(elements)
                 .boxed()
                 .collect(Collectors.groupingBy(Function.identity(), 
                        LinkedHashMap::new,
                        Collectors.counting()))
                 .entrySet()
                 .stream()
                 .flatMapToInt(entry ->
                    IntStream.generate(entry::getKey)
                             .limit(Math.min(maxOccurrences, entry.getValue())))
                 .toArray();
}

我们首先对元素进行分组,然后应用Collectors.counting() 作为下游收集器来获取给定元素的计数。完成后,我们只需映射给定数字 n 次数,然后使用 toArray 急切操作收集到一个数组。

【讨论】:

  • 这不保证订单,可能是必需的。
  • @JoseDaSilva 你有它。
  • 不,我的意思是元素的顺序,例如n=2{2,3,2,2,2},这里的输出将是 {2, 2, 3}。但应该是{2, 3, 2}?,我们还不知道。
  • 我要保存那个巧妙的收集技巧,感谢您的解决方案
【解决方案2】:

实际上你排除了优于maxOcurrences值的元素:

.filter(x -> Collections.frequency(ints, x) <= maxOcurrences)

我不确定完整的Stream 解决方案是否是此用例的最佳选择,因为您想根据这些值“当前收集”的数量添加一些值。

这里是我将如何实现它:

public class DeleteN {

    public static void main(String[] args) {
        System.out.println(Arrays.toString(deleteNth(new int[] { 1, 2, 3, 4, 1 }, 1)));
        System.out.println(Arrays.toString(deleteNth(new int[] { 2, 2, 2, 2 }, 2)));
    }

    public static int[] deleteNth(int[] elements, int maxOcurrences) {

        Map<Integer, Long> actualOccurencesByNumber = new HashMap<>();
        List<Integer> result = new ArrayList<>();
        Arrays.stream(elements)
              .forEach(i -> {
                  Long actualValue = actualOccurencesByNumber.computeIfAbsent(i, k -> Long.valueOf(0L));
                  if (actualValue < maxOcurrences) {
                      result.add(i);
                      actualOccurencesByNumber.computeIfPresent(i, (k, v) -> v + 1L);
                  }
              });

        return result.stream().mapToInt(i -> i).toArray();
    }
}

输出:

[1, 2, 3, 4]

[2, 2]

【讨论】:

    【解决方案3】:

    我认为这是不使用流的好例子。当涉及到有状态的操作时,流并不总是最好的方法。

    但绝对可以做到,而且问题是专门针对流的,所以你可以使用以下。


    使用 forEachOrdered

    您可以使用forEachOrdered 来确保顺序(这里显然流必须是顺序):

    public static int[] deleteNth(int[] elements, int maxOcurrs) {
        List<Integer> list = new ArrayList<>();
        Arrays.stream(elements).forEachOrdered(elem -> {
            if (Collections.frequency(list, elem) < maxOcurrs) list.add(elem);
        });
        return list.stream().mapToInt(Integer::intValue).toArray();
    }
    

    使用收集

    考虑到一些情况,您可以使用collect 方法来完成此操作。

    当流是orderedsequential时,Arrays.stream(elements).boxed()就是这种情况,collect()方法不使用combiner operator(this是 java8 和 java9 当前版本的事实,但不能保证在下一个版本中完全一样,因为可能会发生许多优化)。

    此实现保持流的顺序,并且如前所述,在当前版本中可以正常工作。就像下面链接中的答案所说的那样,而且在我个人看来,我发现在顺序流中实现collect 将永远需要使用组合器。

    collect方法的代码如下:

    public static int[] deleteNth(int[] elements, int maxOcurrs) {
        return Arrays.stream(elements).boxed()
                .collect(() -> new ArrayList<Integer>(),
                        (list, elem) -> {
                            if (Collections.frequency(list, elem) < maxOcurrs) list.add(elem);
                        },
                        (list1, list2) -> { 
                            throw new UnsupportedOperationException("Undefined combiner"); 
                        })
                .stream()
                .mapToInt(Integer::intValue)
                .toArray();
    }
    

    这个collector 创建了一个ArrayList,当要添加新元素时检查是否满足maxOcurrences,如果不满足,则添加该元素。如前所述,在下面的答案中,根本不调用组合器。这个性能比n^2好一点。

    关于为什么在顺序流中不调用组合器方法的更多信息可以找到here

    【讨论】:

    • 所以基本上你是在检查 list 是否有少于 N 个元素并在 true 时添加它们,对吗?你能解释一下为什么我的方法不起作用吗?
    • 您的方法不起作用,因为您正在检查所有元素,而不仅仅是在那之前您拥有的元素。例如。使用{2,2,2,2},如果您检查孔列表,您会得到 4 次重复,然后在过滤器中将它们全部丢弃。如果您改为以{} 开头,然后添加2,首先检查reps 是0&lt;n -> {2},然后添加另一个2,检查reps 是1&lt;n -> {2,2},最后想要添加另一个2,首先检查reps 2&lt;n这里n=2所以你不要添加更多元素,并以当前{2,2}列表结束。
    猜你喜欢
    • 2018-06-24
    • 1970-01-01
    • 2021-10-29
    • 1970-01-01
    • 1970-01-01
    • 2010-11-20
    • 1970-01-01
    • 2016-09-10
    • 2016-02-17
    相关资源
    最近更新 更多