使用Java 8从对象列表中查找中位数值

11

我有两个类的结构如下:

public class Company {
     private List<Person> person;
     ...
     public List<Person> getPerson() {
          return person;
     }
     ...
}

public class Person {
     private Double age;
     ...
     public Double getAge() {
          return age;
     }
     ...
}

基本上 Company 类具有 Person 对象列表,每个 Person 对象都可以获得 Age 值。

如果我获取了 Person 对象的列表,是否有好的方法使用 Java 8 在所有 Person 对象中查找中位数年龄值(Stream 不支持中位数,但还有其他方法吗)?

Double medianAge;
if(!company.getPerson().isEmpty) {
     medianAge = company.getPerson() //How to do this in Java 8?
}

3
最简单的方法,但不是最高效的:对列表进行排序,找到中间元素。 - Andy Turner
2
你为什么认为需要使用流? - Joe C
@JoeC 在我的构建中,流仅被提及为一种不错的使用方式,因此我想知道在当前技术中是否有其他可用于中位数的东西。谢谢。 - 000000000000000000000
1
Stream 简化了需要迭代所有元素的任务。在中位数的情况下,您只需要确保元素已排序并选择中间一个或两个中间元素即可。 - Pshemo
1
如果使用Guava,就没有必要重新发明轮子:https://github.com/google/guava/wiki/StatsExplained#median-only-of-existing-values - dimo414
请查看此链接,其中描述了如何使用Java 8从对象列表中找到中位数值。 (https://dev59.com/olcP5IYBdhLWcg3w5d4l) - Tom Drake
6个回答

16

您可以使用

List<Person> list = company.getPerson();
DoubleStream sortedAges = list.stream().mapToDouble(Person::getAge).sorted();
double median = list.size()%2 == 0?
    sortedAges.skip(list.size()/2-1).limit(2).average().getAsDouble():        
    sortedAges.skip(list.size()/2).findFirst().getAsDouble();

这种方法的优点是它不修改列表,因此也不依赖于其可变性。然而,这并不一定是最简单的解决方案。

如果您有修改列表的选项,可以使用

List<Person> list = company.getPerson();
list.sort(Comparator.comparingDouble(Person::getAge));
double median = list.get(list.size()/2).getAge();
if(list.size()%2 == 0) median = (median + list.get(list.size()/2-1).getAge()) / 2;

相反。


9

必须使用Guava的方式。

import java.util.List;
import java.util.stream.Collectors;

import com.google.common.math.Quantiles;

...

List<Person> people = company.getPerson();
List<Double> ages = people.stream().map(Person::getAge).collect(Collectors.toList());
double median = Quantiles.median().compute(ages);

尽管在Guava 31.1中Quantiles仍被注释为@Beta


2
由于某种可怕的原因,所有东西都是@Beta - Mugen

7
这是 @Holger 的答案的简化版本,同样适用于 IntStreamLongStream,并避免在空流的情况下出现 NoSuchElementException

int size = someList.size();

//replace 'XXX' with 'Int', 'Long', or 'Double' as desired
return someList.stream().mapToXXX(...).sorted()
    .skip((size-1)/2).limit(2-size%2).average().orElse(Double.NaN);

如果列表为空,orElse(Double.NaN) 将返回 NaN,而非抛出 NoSuchElementException。如果你想抛出 NoSuchElementException,只需将其替换为 .getAsDouble()

1

这里是另一种使用Collectors.collectingAndThen的单流解决方案。


double median = people.stream()
        .map(Person::getAge)
        .sorted()
        .collect(Collectors.collectingAndThen(
                Collectors.toList(),
                ages -> {
                    int count = ages.size();
                    if (count % 2 == 0) { // even number
                        return (ages.get(count / 2 - 1) + ages.get(count / 2)) / 2;
                    } else { // odd number
                        return ages.get(count / 2);
                    }
                }));

它不会修改现有列表,也不需要对其进行排序。

0
如果列表未排序,则可以使用以下代码:
int middle = list.size()/2;
Integer value = list.stream().filter(i -> list.indexOf(i) == middle).collect(Collectors.toList()).get(0);
System.out.println(value);

这不是一个解决方案--你似乎正在获取列表的中间元素,并且自己说你假设列表未排序。在这种情况下,中间元素不是中位数。 这不是正确的代码--你假设列表中有一个或多个元素具有中间索引。如果列表的元素数量为奇数,则不会出现这种情况,并且get(0)将抛出异常。 这不是有效率的--你遍历整个列表,然后询问每个元素它在列表中的索引是什么。这是O(n^2)的。 - J S

-10
你可以使用lambda表达式来减少代码并获取列表的中位数:
Integer median = Person
   .stream()
   .map(Person::getAge)
   .filter(n -> n.length()/2);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接