1. Java中的排序算法
讲Java中的集合排序前,我们来一起思考一下: 什么样的排序算法适合做为标准类库的算法?
- 对比各种常见排序算法的优劣性
算法 | 稳定性 | 时间复杂度 | 空间复杂度 |
---|---|---|---|
选择排序 | 不稳定 | N² | 1 |
希尔排序 | 不稳定 | 说法很多,N^(1.25-2) | 1 |
堆排序 | 不稳定 | NlogN | 1 |
快速排序 | 不稳定 | NlogN | lgN |
插入排序 | 稳定 | N-N² | 1 |
归并排序 | 稳定 | NlogN | N |
其中,快速排序算法对于多数的情况都是效率较高的,其内循环中的指令很少并且总是顺序地访问数据,可以利用缓存(预加载),但是稳定性无法保证(如果待排序的元素序列本身即为有序的状态).
如果对稳定性有要求,那么归并排序可能是更为适合的,它的弊端是需要更多的空间(通常编写归并排序,我们需要创建临时数组).
关于局部性原理知识,可以通往下面链接看知乎大神如何讨论:
如何理解计算机操作系统中的局部性原理?
关于快速排序算法的讨论:
快速排序到底有多快
排序算法(三): Quick Sort 快速排序(包含三向切分)
- 原始类型数据与引用类型数据
这里说一下笔者的一些简单理解
原始类型的数据,大小比较都是可以确定的,比如:
比较int大小,可以直接用 a > b这种方式进行比较.
但是Java中还提供了Comparable
接口,它通过实现该接口的方法-compareTo
来决定排列顺序.
这时,无法通过直接比较数值的大小,而是通过引用来执行方法得到结果,需要额外的空间.
为此,Java中对 原始类型数组(类型为 int、long、short、char、byte、boolean、float 或 double 的数组) 采用优化的快速排序算法-DualPivotQuicksort
.
对于引用类型数据的集合采用基于归并排序的TimSort算法
.
2. 使用集合排序需要注意的问题
禁止对不可变列表(unmodifiableList)进行排序,传入的列表可以为必须是可修改的, 但不必是可以改变大小的
.
- 错误示范
Student xiaoMing = Student.builder().age(20).name("20岁的小明").build();
Student xiaoHong = Student.builder().age(22).name("22岁的小红").build();
Student xiaoHua = Student.builder().age(25).name("25岁的小华").build();
List studentList = Collections.unmodifiableList(Lists.newArrayList(xiaoHong, xiaoMing, xiaoHua));
// 使用不可变集合,会抛出UnsupportedOperationException异常
Collections.sort(studentList);
studentList.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
3. 实战Java排序
3.1 数据源准备
为了更好地复用数据,我们先声明两个数据列表:
一个为数值型的数据列表
一个为业务对象的数据列表
- 业务对象-Student
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* @author jaymin
*/
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Student implements Serializable, Comparable {
/**
* 年龄
*/
private Integer age;
/**
* 名字
*/
private String name;
/**
* the value 0 if x == y; a value less than 0 if x < y; and a value greater than 0 if x > y
* @param o 与之比较的其他学生
* @return
*/
@Override
public int compareTo(Object o) {
Student otherStudent = (Student) o;
return Integer.compare(this.age, otherStudent.age);
}
}
- 声明成员变量
import com.google.common.collect.Lists;
import com.tea.modules.model.Student;
import com.tea.modules.util.JacksonUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author jaymin.
* 在一次使用lambda的时候对Java排序产生了兴趣。因此本demo用来研究集合排序.
* 2021/2/18 20:16
*/
@SuppressWarnings("unchecked")
public class CollectionSort {
/**
* 数字列表
*/
protected final static List numberList = Lists.newArrayList(2, 3, 4, 1, 5, 2, 5, 6, 7, 99);
/**
* 存储不同年龄段的学生列表
*/
protected static List studentList;
/**
* 构建出学生列表参数
*/
private static void createStudentList() {
Student xiaoMing = Student.builder().age(20).name("20岁的小明").build();
Student xiaoHong = Student.builder().age(22).name("22岁的小红").build();
Student xiaoHua = Student.builder().age(25).name("25岁的小华").build();
studentList = Lists.newArrayList(xiaoHong, xiaoMing, xiaoHua);
}
}
3.2 对原始类型列表进行排序
使用Collections.sort可以直接对Integer列表进行排序
/**
* 使用Collections.sort对集合进行升序排序.
* 要求集合中的对象实现Comparable接口.
*/
public static void sortByCollections() {
Collections.sort(numberList);
numberList.forEach(e -> System.out.print(e + "-"));
}
- Result
1-2-2-3-4-5-5-6-7-99-
3.3 对实现了Comparable接口的对象进行排序
Collections.sort中的对象需要重写compareTo方法
/**
* 这里我们使用了集合存储学生的信息.
* 如果此时希望按照年龄来进行排序,可以在Student类中实现Comparable接口。
* 然后再使用Collections.sort进行排序.
*/
public static void sortByCollectionsComparable() {
createStudentList();
Collections.sort(studentList);
studentList.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
}
- Result
{
"age" : 20,
"name" : "20岁的小明"
}
{
"age" : 22,
"name" : "22岁的小红"
}
{
"age" : 25,
"name" : "25岁的小华"
}
3.4 使用比较器-Comparator来进行排序
通过匿名内部类来创建Comparator
/**
* 不实现接口,使用比较器-Comparator来进行排序.
* 比较器接收两个参数,可以理解成,给定两个学生对象,你实现compare方法来规定按什么顺序进行排序.
* 比较规则:the value 0 if x == y; a value less than 0 if x < y; and a value greater than 0 if x > y.
* 首先我们通过匿名内部类来完成这项工作.
*
* @see Comparator
*/
public static void sortByCollectionsComparator() {
createStudentList();
Collections.sort(studentList, new Comparator() {
@Override
public int compare(Student studentA, Student studentB) {
return Integer.compare(studentA.getAge(), studentB.getAge());
}
});
studentList.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
}
输出结果同上,这里就不贴了
3.5 Java8中使用lambda来简化匿名内部类.
/**
* 在Java8中,你可以使用lambda来简化匿名内部类.
*/
public static void sortByCollectionsComparatorInJava8() {
createStudentList();
// 常规的lambda
Collections.sort(studentList, (studentA, studentB) -> Integer.compare(studentA.getAge(), studentB.getAge()));
// 同时,你可以将这种方式改写成方法引用
Collections.sort(studentList, Comparator.comparingInt(Student::getAge));
studentList.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
}
输出结果同上,这里就不贴了
3.6 使用Java8的stream进行排序
/**
* 使用Java8的stream流,让你的集合操作更多元化.
*/
public static void sortByStream() {
createStudentList();
// 升序
List result = studentList.stream()
.sorted(Comparator.comparing(Student::getAge))
.collect(Collectors.toList());
result.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
System.out.println("------------reversedOrder---------------");
// 倒序
List reversedOrderResult = studentList.stream()
.sorted(Comparator.comparingInt(Student::getAge).reversed())
.collect(Collectors.toList());
reversedOrderResult.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
}
- Result
{
"age" : 20,
"name" : "20岁的小明"
}
{
"age" : 22,
"name" : "22岁的小红"
}
{
"age" : 25,
"name" : "25岁的小华"
}
------------reversedOrder---------------
{
"age" : 25,
"name" : "25岁的小华"
}
{
"age" : 22,
"name" : "22岁的小红"
}
{
"age" : 20,
"name" : "20岁的小明"
}
- 空指针保护的Comparator.comparing
在上面的实验中,我们使用了
Comparator.comparing()
来进行比较,但是,如果student对象中的age为空时,我们会遇到空指针异常.
- 触发空指针异常
/**
* 使用null值保护比较器来避免空指针异常
*/
public static void sortByStreamWithNullFirst() {
createStudentList();
studentList.get(0).setAge(null);
// 升序
List result = studentList.stream()
.sorted(Comparator.comparing(Student::getAge))
.collect(Collectors.toList());
result.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
}
- 使用nullFirst和nullLast来避免空指针异常
nullFirst表示遇到null时,将包含null属性的对象排列在第一位.
nullsLast表示遇到null时,将包含null属性的对象排列在最后一位.
两者都需要声明排序顺序:
升序为:Comparator.naturalOrder()
降序为:Comparator.reverseOrder()
/**
* 使用null值保护比较器来避免空指针异常
*/
public static void sortByStreamWithNullFirst() {
createStudentList();
studentList.get(0).setAge(null);
// 升序,空值排在第一位
List result = studentList.stream()
.sorted(Comparator.comparing(Student::getAge,Comparator.nullsFirst(Comparator.naturalOrder())))
.collect(Collectors.toList());
result.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
System.out.println("******************** Null First reverseOrderResult*****************************");
// 降序,空值排在第一位
List reverseOrderResult = studentList.stream()
.sorted(Comparator.comparing(Student::getAge,Comparator.nullsFirst(Comparator.reverseOrder())))
.collect(Collectors.toList());
reverseOrderResult.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
System.out.println("******************** Null Last reverseOrderResult*****************************");
// 降序,空值排在第一位
List nullLastResult = studentList.stream()
.sorted(Comparator.comparing(Student::getAge,Comparator.nullsLast(Comparator.reverseOrder())))
.collect(Collectors.toList());
nullLastResult.forEach(e -> System.out.print(JacksonUtils.toJsonString(e) + "\n"));
}
- Result
{
"name" : "22岁的小红"
}
{
"age" : 20,
"name" : "20岁的小明"
}
{
"age" : 25,
"name" : "25岁的小华"
}
******************** Null First reverseOrderResult*****************************
{
"name" : "22岁的小红"
}
{
"age" : 25,
"name" : "25岁的小华"
}
{
"age" : 20,
"name" : "20岁的小明"
}
******************** Null Last reverseOrderResult*****************************
{
"age" : 25,
"name" : "25岁的小华"
}
{
"age" : 20,
"name" : "20岁的小明"
}
{
"name" : "22岁的小红"
}
源码地址
所有源码已经上传到我的个人仓库,有兴趣的可以pull下来进行demo.
仓库地址
代码引用地址:com.tea.modules.java8.collection.sort.CollectionSort