Java 8: Find index of minimum value from a List

前端 未结 4 1682
北荒
北荒 2021-02-01 23:50

Say I have a list with elements (34, 11, 98, 56, 43).

Using Java 8 streams, how do I find the index of the minimum element of the list (e.g. 1 in this case)

相关标签:
4条回答
  • 2021-02-02 00:01

    You could do it like this:

    int indexMin = IntStream.range(0, list.size())
                    .mapToObj(i -> new SimpleEntry<>(i, list.get(i)))
                    .min(comparingInt(SimpleEntry::getValue))
                    .map(SimpleEntry::getKey)
                    .orElse(-1);
    

    If the list is a random access list, get is a constant time operation. The API lacks of a standard tuple class, so I used the SimpleEntry from the AbstractMap class as a substitute.

    So IntStream.range generates a stream of indexes from the list from which you map each index to its corresponding value. Then you get the minimum element by providing a comparator on the values (the ones in the list). From there you map the Optional<SimpleEntry<Integer, Integer>> to an Optional<Integer> from which you get the index (or -1 if the optional is empty).

    As an aside, I would probably use a simple for-loop to get the index of the minimum value, as your combination of min / indexOf does 2 passes over the list.

    You might also be interested to check Zipping streams using JDK8 with lambda (java.util.stream.Streams.zip)

    0 讨论(0)
  • 2021-02-02 00:13

    Since this is for learning purposes, let's try to find a solution that doesn't just somehow use a stream, but actually works on the stream of our list. We also don't want to assume random access.

    So, there are two ways to get a non-trivial result out of a stream: collect and reduce. Here is a solution that uses a collector:

    class Minimum {
        int index = -1; 
        int range = 0;
        int value;
    
        public void accept(int value) {
            if (range == 0 || value < this.value) {
                index = range;
                this.value = value;
            }
            range++;
        }
    
        public Minimum combine(Minimum other) {
            if (value > other.value) {
                index = range + other.index;
                value = other.value;
            }
            range += other.range;
            return this;
        }
    
        public int getIndex() {
            return index;
        }
    }
    
    static Collector<Integer, Minimum, Integer> MIN_INDEX = new Collector<Integer, Minimum, Integer>() {
            @Override
            public Supplier<Minimum> supplier() {
                return Minimum::new;
            }
            @Override
            public BiConsumer<Minimum, Integer> accumulator() {
                return Minimum::accept;
            }
            @Override
            public BinaryOperator<Minimum> combiner() {
               return Minimum::combine;
            }
            @Override
            public Function<Minimum, Integer> finisher() {
                return Minimum::getIndex;
            }
            @Override
            public Set<Collector.Characteristics> characteristics() {
                return Collections.emptySet();
            }
        };
    

    Writing a collectors creates an annoying amount of code, but it can be easily generalized to support any comparable value. Also, calling the collector looks very idiomatic:

    List<Integer> list = Arrays.asList(4,3,7,1,5,2,9);
    int minIndex = list.stream().collect(MIN_INDEX);
    

    If we change the accept and combine methods to always return a new Minimum instance (ie. if we make Minimum immutable), we can also use reduce:

    int minIndex = list.stream().reduce(new Minimum(), Minimum::accept, Minimum::combine).getIndex();
    

    I sense large potential for parallelization in this one.

    0 讨论(0)
  • 2021-02-02 00:20

    Here's two possible solutions using my StreamEx library:

    int idx = IntStreamEx.ofIndices(list).minBy(list::get).getAsInt();
    

    Or:

    int idx = EntryStream.of(list).minBy(Entry::getValue).get().getKey();
    

    The second solution internally is very close to one proposed by @AlexisC. The first one is probably the fastest as it does not use boxing (internally it's a reduce operation).

    Without using third-party code @Misha's answer looks the best for me.

    0 讨论(0)
  • 2021-02-02 00:21
    import static java.util.Comparator.comparingInt;
    
    int minIndex = IntStream.range(0,list.size()).boxed()
                .min(comparingInt(list::get))
                .get();  // or throw if empty list
    

    As @TagirValeev mentions in his answer, you can avoid boxing by using IntStream#reduce instead of Stream#min, but at the cost of obscuring the intent:

    int minIdx = IntStream.range(0,list.size())
                .reduce((i,j) -> list.get(i) > list.get(j) ? j : i)
                .getAsInt();  // or throw
    
    0 讨论(0)
提交回复
热议问题