上午一个朋友在群里推荐每日打卡,题目《找到K个最接近的元素》,这道题推荐使用二分查找来做。这里写这边文章的主要目的是为了记录Java的比较器是怎样进行比较的。因为官方推荐了一种做法,使用Java8的新特性—Lambda表达式来做,觉得很有意思,就花了些时间琢磨了一下,并记录下来。
通常情况下,若要对数组元素进行排序,Java中的规定是元素对应的类要么实现Comparable< T >接口,即实现该接口中的唯一一个方法—public int compareTo(T o),要不就要实现Comparator< T >接口,并至少需要实现该接口中的int compare(T o1, T o2)实例方法。这里推荐一篇文章,这篇文章的作者通过举例说明了针对自定义数据类型,采用两种策略说明了如何进行比较的过程。这里不再赘述。
在这里,就单独谈谈在Java8中的函数式表达式中中如何使用比较器的。首先,自定义数据类型如下:
/**
* Class {@code Person} is a class to validate the {@link java.util.Comparator}.
*
* @author Vimist
*/
class Person {
private String name;
private int age;
private double grade;
public Person(String name, int age, double grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public double getGrade() {
return grade;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", grade=" + grade +
'}';
}
}
这这个类中,并没有实现Comparable< T >或者Comparator< T >接口。接下来,在主函数中,先初始化一些对象,然后将这个对象排序输出。排序原则:首先根据成绩(升序或者降序)排序;若成绩相同,则根据姓名;若姓名相同(实际情况当然是不可能的啦,这里主要是为了做演示),则根据年龄(升序或者降序)来排序。初始化对象如下:
Person person1 = new Person("zzz", 20, 30);
Person person2 = new Person("bbb", 11, 20);
Person person3 = new Person("bba", 10, 20);
Person person4 = new Person("bba", 11, 20);
Person[] people = {person1, person2, person3, person4};
首先,仅仅输入按照成绩升序排序的结果:
System.out.println(Arrays.stream(people).sorted(Comparator.comparing(Person::getGrade)).collect(Collectors.toList()));
输出结果如下:
[Person{name='bbb', age=11, grade=20.0}, Person{name='bba', age=10, grade=20.0}, Person{name='bba', age=11, grade=20.0}, Person{name='zzz', age=20, grade=30.0}]
通常情况下,成绩都是按照降序排序,因此
System.out.println(Arrays.stream(people)
.sorted((p1, p2) -> p2.getGrade() > p1.getGrade() ? 1 : p2.getGrade() == p1.getGrade() ? 0 : -1)
.collect(Collectors.toList()));
这里需要注意的地方是,这里的sorted()函数的参数是一个Comparator接口,而这个接口是不能直接使用的。因此,这里将代码直接传给Comparator接口。更多细节可以参考Java的匿名类相关知识点。另外,由于是逆序,因此需要保证p2.getGrade() > p1.getGrade()
才能满足逆序条件。
输出结果如下:
[Person{name='zzz', age=20, grade=30.0}, Person{name='bbb', age=11, grade=20.0}, Person{name='bba', age=10, grade=20.0}, Person{name='bba', age=11, grade=20.0}]
紧接着,需要根据姓名排序按照升序排序。如果说将collect(Collectors.toList())
得到的链表再按照姓名sort一下,那个原先的排序顺序就会被打乱。因此,需要再第一步比较的基础上,修改部分代码,将按照成绩逆序排序的过程以匿名函数的形式提取出来如下:
Comparator<Person> comparatorByGrade = (o1, o2) -> o2.getGrade() > o1.getGrade() ? 1 : o2.getGrade() == o1.getGrade() ? 0 : -1;
这里的comparatorByGrade
是一个Comparator类的匿名子类,其包含父类的所有静态函数。通过再该对象后使用thenComparing( Function<? super T, ? extends U> keyExtractor)
即可实现再第一步按照成绩逆序排序的基础上,再按照姓名升序排序,代码如下:
System.out.println(Arrays.stream(people)
.sorted(comparatorByGrade.thenComparing(Person::getName))
.collect(Collectors.toList()));
输出结果如下:
[Person{name='zzz', age=20, grade=30.0}, Person{name='bba', age=10, grade=20.0}, Person{name='bba', age=11, grade=20.0}, Person{name='bbb', age=11, grade=20.0}]
同样第三步,再之前两步的基础上,再按照年龄逆序排序,这里就直接给出代码如下:
Comparator<Person> comparatorByAge = (p1, p2) -> p2.getAge() > p1.getAge() ? 1 : p2.getAge() == p1.getAge() ? 0 : -1;
System.out.println(Arrays.stream(people)
.sorted(comparatorByGrade.thenComparing(Person::getName).thenComparing(comparatorByAge))
.collect(Collectors.toList()));
输出结果如下:
[Person{name='zzz', age=20, grade=30.0}, Person{name='bba', age=11, grade=20.0}, Person{name='bba', age=10, grade=20.0}, Person{name='bbb', age=11, grade=20.0}]
结果符合基本预期。完整的主函数代码如下:
public static void main(String[] args) {
Person person1 = new Person("zzz", 20, 30);
Person person2 = new Person("bbb", 11, 20);
Person person3 = new Person("bba", 10, 20);
Person person4 = new Person("bba", 11, 20);
Person[] people = {person1, person2, person3, person4};
Comparator<Person> comparatorByGrade = (o1, o2) -> o2.getGrade() > o1.getGrade() ? 1 : o2.getGrade() == o1.getGrade() ? 0 : -1;
Comparator<Person> comparatorByAge = (p1, p2) -> p2.getAge() > p1.getAge() ? 1 : p2.getAge() == p1.getAge() ? 0 : -1;
System.out.println(Arrays.stream(people)
.sorted(comparatorByGrade.thenComparing(Person::getName).thenComparing(comparatorByAge))
.collect(Collectors.toList()));
}
最后,回到题目《找到K个最接近的元素》。根据题目的要求,需要从一个数组中找到最接近x的k个元素。这里先给出代码如下:
return Arrays.stream(arr).boxed()
.sorted((n1, n2) -> n1 == n2 ? n1 - n2 : Math.abs(n1 - x) - Math.abs(n2 - x))
.limit(k).sorted().collect(Collectors.toList());
首先,通过Arrays.stream()将数组转换为一个数据流(IntStream)。由于数组的元素是基本类型,因此,通过boxed()将流中的每个元素装箱,封装成对象形式,然后将流中的数据进行排序。sorted默认按照升序排序,而排序条件是按照元素距离给定值x的距离的升序排序。可以将排序后的结果收集起来输出如下:
int[] arr = {0, 0, 1, 2, 3, 3, 4, 7, 7, 8};
int k = 6, x = 5;
[4, 3, 3, 7, 7, 2, 8, 1, 0, 0]
可以发现,sorted结果符合预期,排在最前面的值距离给定值5最接近。然后通过limit(long maxSize)
取出前k个元素。但是这些元素是乱序的,因此,再次调用sorted(),使用默认排序规则升序排序,最后收集成一个链表返回。这里给出最后运行结果:
[2, 3, 3, 4, 7, 7]
上述的Lamda表达式代码在LeetCode上是可以直接运行的,虽然不是最快的(大概100ms左右,而使用二分查找法大概在4ms左右),但是通过Lamda表达式去解决问题可以看作是对Java的一种探索吧。