2

I need to get the minimum and maximum values in a collection of floating point values, after applying an absolute value conversion. In python I would do this

min(map(abs, [-5, -7, 10, 2]))

how can I perform the same operation in java in the most elegant way?

Stefano Borini
  • 138,652
  • 96
  • 297
  • 431

6 Answers6

4

You might be able to do something crazy, but the most straightforward solution is this:

List<Integer> x = new ArrayList<Integer>(Arrays.asList( {-5,-7,10,2} } );
for( int i = 0; i < x.size(); i++ ){
   x.set( i, Math.abs(x.get(i)) );
}

return Collections.max( x );

Collections and Arrays are infinitely useful.

Stefan Kendall
  • 66,414
  • 68
  • 253
  • 406
  • ah ok. I'm forced to create a temporary list. – Stefano Borini Mar 15 '10 at 05:18
  • 1
    Apologies if this is pedantic, but doesn't "return Collections.max( x );" mandate returning only the max value in the list? – Everyone Mar 15 '10 at 06:44
  • Right right. I didn't see the "min" and max part. I think it's pretty easy to guess that Collections.min exists from Collections.max, though, and I even linked to the Java API for confirmation of such :P. – Stefan Kendall Mar 15 '10 at 14:08
3

Here is another way to do it. This approach does not require creating a second copy of the array, since Arrays.asList merely produces an array-backed view of the given array. Another potential advantage is the fact that the signs of the minimum and maximum are preserved. In the example below, the minimum is displayed as 1.333 and the maximum as -9.43.

It is more complex than Stefan's solution, but depending on the situation, it might be right for you.

    Float numbers[] = {-9.43f, 2.3f, -8.2f, 1.333f};

    // Calculate the minimum
    Float min = Collections.min(Arrays.asList(numbers), 
            new Comparator<Float>() {
                public int compare(Float o1, Float o2) {
                    Float result = Math.abs(o1) - Math.abs(o2);
                    return result > 0 ? 1 : result < 0 ? -1 : 0;
                }
            }
    );

    // Calculate the maximum
    Float max = Collections.min(Arrays.asList(numbers), 
            new Comparator<Float>() {
                public int compare(Float o1, Float o2) {
                    Float result = Math.abs(o2) - Math.abs(o1);
                    return result > 0 ? 1 : result < 0 ? -1 : 0;
                }
            }
    );

    System.out.println("Min = " + min);
    System.out.println("Max = " + max);

Output:

Min = 1.333
Max = -9.43

Edit: In response to "hatchetman82" in the comments, I decided to run a simple benchmark to see how my solution performs compared to Stefan Kendall's. For small arrays of less than ten thousand elements, the two solutions perform almost identically. However, for larger arrays, my solution will usually perform better.

Stefan's approach is perfectly valid, but it uses about twice as much memory as mine, because it must create a copy of the original array in order to store each element's absolute value. As for time complexity, I found that my approach performed anywhere between 4X and 7X faster, mostly due to the time Stefan's approach requires to copy the array. Keep in mind that these benchmarks were performed on a single machine (a MacBook Pro, 4GB, Core2 Duo @ 2.53) and results will vary depending on your computer's and JVM's configurations.

Stefan's approach is certainly more straight forward, and mine can perform better under certain situations. So basically each solution is valid, though one or the other might be preferable depending on the situation.

William Brendel
  • 31,712
  • 14
  • 72
  • 77
  • Those calls to `Math.abs()` turn out to be cheap though, and having to copy the entire array turns out to be very expensive. I performed some simple benchmarks to determine the difference. The method I described is between 4X and 7X faster for a list of one million floating point numbers. It also uses half as much memory as Stefan's approach since the original array is never copied. I will add the benchmark results to my answer for those who are interested. – William Brendel Mar 15 '10 at 06:18
2

In the category of "something crazy":

import java.util.*;
import org.apache.commons.collections.iterators.IteratorEnumeration;
import org.apache.commons.functor.core.collection.TransformedIterator;
import org.apache.commons.functor.UnaryFunction;
//...
int commonsMin = Collections.min(
                 Collections.list((Enumeration<Integer>)
                 new IteratorEnumeration(
                 new TransformedIterator<Integer, Integer>(
                 Arrays.asList(3, -5, -7, 10, -2, 4).iterator(), new UnaryFunction<Integer, Integer>(){
                   public Integer evaluate(Integer a)
                   {
                     return Math.abs(a);
                   }
                 }))));
//...

This really wouldn't be that crazy if one of the Collections.min overloads took a Iterator, or there was an easy way to convert an Iterator to a Collection. It may be even more sane with Java 7 closures.

EDIT: This is cleaner with Guava/Google Collections. I use HashSet because we really don't care about order:

import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;

// ...
int guavaMin = Collections.min(
               Sets.newHashSet(
               Iterables.transform(
               Sets.newHashSet(3, -5, -7, 10, -2, 4), new Function<Integer, Integer>(){
                 public Integer apply(Integer a)
                 {
                   return Math.abs(a);
                 }
               })));
// ...
Matthew Flaschen
  • 278,309
  • 50
  • 514
  • 539
  • @Matthew: I'd recommend removing the first chunk of code from this answer - what you write at the bottom is really the best answer proposed so far, and I almost skipped over it b/c I saw the object oriented approach at the top. OO isn't always the best solution! – Kevin Day Mar 15 '10 at 13:58
  • Kevin, I've separated the two approaches. – Matthew Flaschen Mar 15 '10 at 15:46
1

Creating another (more pragmatic) answer, to separate two approaches (see Kevin Day's comment):

This can be done in one array pass (sort is O(log n)), without creating any new list. Just don't use Collections.max. Instead, just calculate the absolute value as you go and have a running minimum/maximum:

List<Integer> l = Arrays.asList(3, -5, -7, 10, -2, 4);
int iterMin = Integer.MAX_VALUE;
for(Integer i : l)
  if(Math.abs(i) < iterMin)
    iterMin = Math.abs(i);

If you want to preserve the original value, you can easily do that:

int iterMinOrig = Integer.MAX_VALUE;
for(Integer i : l)
  if(Math.abs(i) < Math.abs(iterMinOrig))
    iterMinOrig = i;
Matthew Flaschen
  • 278,309
  • 50
  • 514
  • 539
0

I would suggest using a Comparator. For instance, if your list is doubles, then you have the following options:

Get the maximum absolute value:

double maxAbs = Collections.max( myList, new Comparator<Double>() {
        @Override
        public int compare(Double x, Double y) {
            return Math.abs(x) < Math.abs(y) ? -1 : 1;
        }

    });

Get the minimum absolute value:

double minAbs = Collections.max( myList, new Comparator<Double>() {
        @Override
        public int compare(Double x, Double y) {
            return Math.abs(x) > Math.abs(y) ? -1 : 1;
        }

    });

Sort your list in ascending order (with respect to the absolute value):

Collections.sort( myList, new Comparator<Double>() {
            @Override
            public int compare(Double x, Double y) {
                return Math.abs(x) < Math.abs(y) ? -1 : 1;
            }

        });

Sort your list in descending order (with respect to the absolute value):

Collections.sort( myList, new Comparator<Double>() {
            @Override
            public int compare(Double x, Double y) {
                return Math.abs(x) > Math.abs(y) ? -1 : 1;
            }

        });

If you have some other type like integers, just replace Double with Integer

bremen_matt
  • 6,902
  • 7
  • 42
  • 90
0

Short (elegant?):

int min = Arrays.stream(array).map(Math::abs).min().getAsInt();
int max = Arrays.stream(array).map(Math::abs).max().getAsInt();

Or, to avoid traversing twice (more effective but maybe slightly less elegant):

IntSummaryStatistics d = Arrays.stream(array).map(Math::abs).summaryStatistics();
int min = d.getMin();
int max = d.getMax();

(Both adapted from https://stackoverflow.com/a/30692454)

Svullo
  • 421
  • 5
  • 5