简体   繁体   中英

Java 8: Find index of minimum value from a List

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 (eg 1 in this case)?

I know this can be done easily in Java using list.indexOf(Collections.min(list)) . However, I am looking at a Scala like solution where we can simply say List(34, 11, 98, 56, 43).zipWithIndex.min._2 to get the index of minimum value.

Is there anything that can be done using streams or lambda expressions (say Java 8 specific features) to achieve the same result.

Note: This is just for learning purpose. I don't have any problem in using Collections utility methods.

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

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)

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM