如何获取 Arrays.stream(array).min() 的索引?

5

我想获取最小值的索引,尝试了一些类似于getIndexOf的方式,但都不起作用。我该如何做呢?

import java.util.Arrays;
class getIndexOfMin {
    public static void main(String[] args) {

        double arr[] = {263.5, 393.75, 5.0, 289.75};

        double min = Arrays.stream(arr).min().getAsDouble();
        
        System.out.println(min);
    }
}

2
顺便提一下,流式处理比简单遍历数组稍微慢一些。我们使用流式处理可以使代码更易读,但在您的情况下,这并不会使代码更易读(因为您想要执行的操作不是标准操作),同时您需要两次循环遍历数组,而实际上您可以通过使用经典 for 循环仅循环一次即可完成任务。 - Matteo NNZ
5个回答

4
你可以创建一个代表数组索引的流,从0到arr.length(不包括):
Optional<Integer> minIndex = IntStream.range(0, arr.length).boxed()
    .min(Comparator.comparingDouble(i -> arr[i]));

请注意,这是可选的,因为数组可能为空。而且,当存在多个最小值时,它没有指定将返回哪一个的索引。在我使用的实现(OpenJDK 17)中,它返回第一个的索引。
如果你想控制获取哪个最小值的索引,可以这样做:
Optional<Integer> minIndex = IntStream.range(0, arr.length).boxed().reduce(
    // gets the first one's:
    (a, b) -> arr[a] <= arr[b] ? a : b
    // gets the last one's:
    // (a, b) -> arr[a] < arr[b] ? a : b
);

你可以使用 comparingDouble() 使其更加高效。 - shmosel
当存在多个最小值时,它将返回第一个的索引。这在哪里有说明? - shmosel
@shmosel 不是...我在得出那个结论时跳过了几个步骤。 - Sweeper

2
int index;
for (int i = 0; i < arr.length; i++){
    double d = arr[i];
    if (d == min){
       index = i; 
       break;
    }
}

1

以下是如何在不迭代数组索引并尝试使用流模仿 for-loop 的方法。

我们还可以一路上获取实际的最小值。

为此,我们可以使用 DoubleStream.collect(),它需要三个参数:

  • Supplier<R> supplier - 提供一个可变对象,该对象将用作数据的容器;
  • ObjDoubleConsumer<R> accumulator - 确定如何在由supplier提供的可变收集器中累积流元素;
  • BiConsumer<R,R> combiner - 在并行执行流时组合部分结果。
作为一个可变容器,供应商可以提供 double[] 数组 (如果您查看旨在累加原始值的收集器的源代码,例如 summingInt()summingDouble()等,您可能会发现一些相似之处)。
double[] arr = {263.5, 393.75, 5.0, 289.75};
        
double[] min = Arrays.stream(arr)
    .collect(
        () -> new double[]{0, -1, 0},    // supplier
        (double[] res, double next) -> { // accumulator
            res[2]++; // element count
            if (res[1] == -1 || res[0] > next) {
                res[0] = next;          // min value
                res[1] = res[2] - 1;    // corresponding index (element count - 1)
            }
        },
        (left, right) -> {              // combiner
            if (left[0] > right[0]) {
                left[0] = right[0];
                left[1] = left[2] + right[1];
            }
            left[2] += right[2];
        }
    );
        
System.out.printf("Min value is %s at index: %d", min[0], (int) min[1]);

输出:

Min value is 5.0 at index: 2

上面显示的确定最小值和跟踪已消耗元素数量的逻辑可以封装到一个中(如@Holger所建议的),该类将用作累加类型而不是数组。

为了方便起见,我实现了{{link1:DoubleConsumer}}接口,它的方法accept()将用于实现累加器。方法merge()将用于组合器

public static class MinValueAndIndex implements DoubleConsumer {
    private int totalCount;
    private int index;
    private double min;

    @Override
    public void accept(double value) {
        if (totalCount == 0 || value < min) {
            min = value;
            index = totalCount;
        }
        totalCount++;
    }
    
    public void merge(MinValueAndIndex other) {
        if (min > other.min) {
            min = other.min;
            index = totalCount + other.index;
        }
        totalCount += other.totalCount;
    }
    
    // getters
}

流应该是这样的:

double[] arr = {263.5, 393.75, 5.0, 289.75};
        
MinValueAndIndex valueIndex = Arrays.stream(arr)
    .collect(
        MinValueAndIndex::new,
        MinValueAndIndex::accept,
        MinValueAndIndex::merge
    );
    
System.out.printf("Min value is %s at index: %d", valueIndex.getMin(), valueIndex.getIndex());

输出:

Min value is 5.0 at index: 2

此外(这只是我的个人意见,其他人可能不同意),这种特定的简化几乎是无法阅读的。使用中间对象和 IntStream 模仿 for 循环的解决方案将更加清晰、易读且更少出错。 - Chaosfire
@Chaosfire 这个使用流查找索引的任务没有实际价值,只有作为一种练习来熟悉API才有用。我认为这段代码很好地展示了API的功能,而且它并不容易理解(我没有说相反的话)。顺便问一下,这个让你想起了什么吗?我认为在学习时模仿for循环是有害的,除非学习者不想养成坏习惯。 - Alexander Ivanchenko
你说得没错,但是对于一个新手来说,理解这段代码会非常困难。如果目的是熟悉Stream API的功能,那么越简单越好。此外,在Java 17中,这段代码无法使用并行流(而并行流正是Stream API的亮点之一),在并行执行时会得到错误的索引。 - Chaosfire
@Chaosfire 已修复。源数组太小,分裂器只向每个线程提供了一个元素,因此累积的索引仍然为零。 - Alexander Ivanchenko
@Chaosfire 是的,简单的事情很棒,因为它们可以加快学习过程,但是如果想要深入学习某些东西,挑战是不可避免的(甚至是必要的),没有人能只吃糖果。我不同意你关于代码清洁的初始说法,使用流迭代数组索引并不算是“清洁”,在这种情况下,清洁编码的秘诀: 不要模仿for循环,而是使用for循环。太多初学者误解了流只是写循环的一种花哨方式,强调它们属于不同的世界,不能一一对应是很重要的。 - Alexander Ivanchenko

0

您可以使用自定义类来保存索引和该索引处的值。然后,查找最小值的任务变得非常简单,您可以一次性找到索引和值。

如果您使用Java 14之前的版本,则记录非常适合此目的,否则您可以使用普通类。

public record IndexValue(int index, double value) {
}

使用示例:

public class Test {

  public static void main(String[] args) {
    double[] arr = {263.5, 393.75, 5.0, 289.75};
    IndexValue min = IntStream.range(0, arr.length) //stream indexes
            .mapToObj(i -> new IndexValue(i, arr[i])) //map index and value together using the class
            .min(Comparator.comparing(IndexValue::value)) //find minimum by comparing values
            .orElseGet(() -> new IndexValue(-1, Double.MAX_VALUE)); //supply default value suiting your needs, if array was empty
    System.out.println("Min index value - " + min);
  }
}

打印 - 最小索引值 - IndexValue[index=2, value=5.0]。此流中的所有操作均为非干扰性和无状态,并且可以安全地进行并行执行。


0

我认为应该优先选择以下解决方案,因为在流的1次迭代中,您可以同时收集所需的索引和所需的值。

double arr[] = {263.5, 393.75, 5.0, 289.75};

Optional<Map.Entry<Integer, Double>> keyValueOpt = IntStream.range(0, arr.length)
              .mapToObj(i -> Map.entry(i,arr[i]))
              .min(Map.Entry.comparingByValue());

keyValueOpt.ifPresent(keyValue -> System.out.println(String.format("Index %s with min value %s", keyValue.getKey(), keyValue.getValue())));

enter image description here

您也不需要引入任何新的类到您的代码中,因为Map.Entry(K k, V v)可以扮演将单个键值对保留到结果中的角色。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接