简体   繁体   中英

Elegant min/max absolute values in java

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?

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.

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.

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);
                 }
               })));
// ...

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;

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

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