16

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)?

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.

1

4 Answers 4

29
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
Sign up to request clarification or add additional context in comments.

1 Comment

Yes this is a nice one +1. I was focused on the zipWithIndex case, but this might be the idiom way to solve this. I would just use orElse(-1) instead.
3

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)

1 Comment

Thanks @Alexis, seems to be more complicated than the simple usage of Collections or 'Scala`s implementation.
3

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.

Comments

2

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.

3 Comments

Quite interesting solution. First I thought it will not work for parallel streams, but seems it will. Note that you can use Collector.of(Minimum::new, Minimum::accept, Minimum::combine, Minimum::getIndex) instead of defining an anonymous class.
This approach has the advantage of not requiring the list to be random access friendly. Note that combine method should correctly handle the case of other being empty. Collector javadoc specifically requires that for parallel-friendliness as the "identity constraint". It doesn't say anything about handling the case of this being empty and other not empty, but it seems prudent to handle that as well.
@Misha, yes, it has some minor problems, but I like the idea. Btw it works quite fast, on some tests it even outperforms my IntStreamEx.minBy. I committed a fixed version, probably will include it in my lib.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.