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.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
Mubin
  • 4,192
  • 3
  • 26
  • 45
  • possible duplicate of [How to get the index and max value of an array in one shot?](http://stackoverflow.com/questions/30730861/how-to-get-the-index-and-max-value-of-an-array-in-one-shot) – Chetan Kinger Jun 29 '15 at 17:13

4 Answers4

28
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
Community
  • 1
  • 1
Misha
  • 27,433
  • 6
  • 62
  • 78
  • 1
    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. – Alexis C. Jun 29 '15 at 13:05
2

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)

Community
  • 1
  • 1
Alexis C.
  • 91,686
  • 21
  • 171
  • 177
  • Thanks @Alexis, seems to be more complicated than the simple usage of `Collections` or 'Scala`s implementation. – Mubin Jun 29 '15 at 12:59
2

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.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
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.

Cephalopod
  • 14,632
  • 7
  • 51
  • 70
  • 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. – Tagir Valeev Jun 29 '15 at 16:04
  • 1
    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 Jun 29 '15 at 19:28
  • @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](https://github.com/amaembo/streamex/commit/86703a8b787e4769766c99112db5ba42d75ac3c4) a fixed version, probably will include it in my lib. – Tagir Valeev Jun 30 '15 at 05:39