算法
排序算法
Comparable接口的使用规则
定义一个Date类,如下:
/**
* @author 望轩
* @date 2023/4/15 10:54
*/
public class Date implements Comparable<Date> {
private int day;
private int month;
private int year;
public Date(int day, int month, int year) {
this.day = day;
this.month = month;
this.year = year;
}
@Override
public String toString() {
return "Date{" +
"day=" + day +
", month=" + month +
", year=" + year +
'}';
}
// 先按照天从小到大排序,如果天相同再按照月从小到大排序,如果月也相同则说明两个对象相同
public int compareTo(Date that) {
if(this.day < that.day) return -1;
if(this.day > that.day) return +1;
if(this.month < that.month) return -1;
if(this.month > that.month) return +1;
if(this.year < that.year) return -1;
if(this.year > that.year) return +1;
return 0;
//另外一种功能相同但是写法不同的方法,我之前都是用下面的这种写法,但是不易读,现在发现还是上面的方法易读
// if(this.day == that.day) {
// if(this.month == that.month) {
// return this.year -that.year;
// } else {
// return this.month - that.month;
// }
// } else {
// return this.day - that.day;
// }
}
}
使用TreeSet集合进行检验,如下:
/**
* @author 望轩
* @date 2023/4/15 10:59
*/
public class DateTest {
public static void main(String[] args) {
Date date1 = new Date(1, 3, 2023);
Date date2 = new Date(1, 2, 2024);
Date date3 = new Date(2, 1, 2022);
Date date4 = new Date(1, 3, 2023);
TreeSet<Date> set = new TreeSet<Date>();
set.add(date1);
set.add(date2);
set.add(date3);
set.add(date4);
System.out.println(set);
}
}
//输出结果
[Date{day=1, month=2, year=2024}, Date{day=1, month=3, year=2023}, Date{day=2, month=1, year=2022}]
//因为date1和date4的天,月,年都相同,所以它们两个被当成了两个相同的元素,而set集合中又不允许有相同的元素,所以最后输出的只有一个date1元素对象,date4被自动过滤掉了
Comparator使用规则
个人觉得Comparator接口是要比Comparable使用起来要更加方便,为什么这样说呢?因为如果要是使用Comparable接口的话,那么要求我们所比较的类必须要实现Comparable接口,也就是说要求我们即将比较的类要支持排序,所以你就必须要给类实现一个Comparable接口,比如说现在有一个List集合,它里面存储的是Student学生元素,你想要给这个list集合排序,这就要求你事先必须要给Student类实现一个Comparable接口,这样是不是显得有点冗余;那有没有简洁一点的方法呢?有没有一种方法,我们不用让Student实现Comparable接口,就可以给list集合排序呢?答案是有的,可以使用Comparator接口,事先创建一个排序对象,指定具体的排序规则即可。代码如下:
/**
* @author xuan
* @create 2023/12/6
*/
public class Test1 {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
Student student1 = new Student(18, 199);
Student student2 = new Student(18, 188);
Student student3 = new Student(17, 177);
Student student4 = new Student(16, 166);
List<Student> students = new ArrayList<>();
students.add(student1);
students.add(student2);
students.add(student3);
students.add(student4);
Collections.sort(students, StudentSortComparator.comparator);
for(Student student : students) {
System.out.println(student);
}
}
}
/**
创建排序器
*/
class StudentSortComparator implements Comparator<Student> {
public static StudentSortComparator comparator = new StudentSortComparator();
/**
* 先按照学生的年龄从小到大排序,如果学生年两相同的话就按照学生的身高从小到大排序
* */
@Override
public int compare(Student o1, Student o2) {
if(o1.age < o2.age) {
return -1;
} else if (o1.age == o2.age) {
return o1.height - o2.height;
} else {
return 1;
}
}
}
class Student {
int age;
int height;
public Student(int age, int height) {
this.age = age;
this.height = height;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", height=" + height +
'}';
}
}
运行上面的main方法的输出结果,如下图:
有个问题啊,不知道你想过没有,如果你想要给集合元素从小到大排序,为什么当第一个元素o1小于第二个元素的时候要返回-1,而当第一个元素o1大于第二个元素的时候要返回1呢?这其实是一个模板方法,我们可以点击查看Collections.sort(…)排序方法的源代码,一直往下你会发现下面这个地方,如下图:
请仔细分析jdk里面的这段源码,它是什么意思呢?它的意思是,假如你先在由一个集合,它里面有一些元素,按照下标从小到大排序分别是o1,o2,o3,o4;
当执行到上面的mergeSort方法的时候,它会把下标为j-1的元素去和下标为j的元素做比较,其实也就是把前一个元素和后一个元素做比较,如果它发现前一个元素和后一个元素比较值是大于0的,其实也就是pre.compareTo(next) > 0,那么就给这两个元素交换顺序,那其实就可以推出如果pre.compareTo(next) <= 0,那么前后两个元素就不交换位置了。
有了上面的分析,现在试想一下你有一个list集合[4,2,6,1],如果你想让他们按照从小到大的顺序排序,假设这个时候pre前一个元素比next后一个元素小,那么当执行完pre.compareTo(next)方法之后,你期待它返回一个<=0的数值,还是期待它返回一个>0的数值?注意这个时候你的目的是从小到大排序,而现在pre前一个元素确实是比next后一个元素小的,那么你就不希望他们交换位置了,因此这个时候调用完pre.compareTo(next)方法的时候你肯定是想要返回一个小于等于0的数值的;而如果此时你发现pre > next,因为你是从小到大排序,所以调用完pre.compareTo(next)你是希望它返回一个大于0的数值的。
那么有没有一个视角可以帮助我们快速理解这种思路呢?有的,就是我们的compare方法并不是给元素指定大小的,因为当两个元素调用compare方法的时候,他们的大小已经确定了,
如果你发现前面一个元素的值小于后面一个元素的值,即o1.age < o2.age,而你这个时候想要的效果刚好也是从小到大排序,那么这个时候需要返回一个小于等于0的数值,因为这个时候jdk源代码不会把o1和o2交换顺序;但假如你想要的是从大到小的排序,而现在你发现o1.age < o2.age,那么这个时候就不可以了,你得让他们交换顺序,那么怎么让他们交换顺序呢?就是这种情况下你直接返回一个大于0的数值就可以了,jdk内部代码识别到返回值之后会自动把o1和o2交换顺序,那么我们就能实现从大到小的排序了。
特别注意:compare方法的返回值要满足自反性和反对称性;什么是自反性什么是反对称性呢?
**自反性:**当两个相同的元素相比时,compare(o1,o2)必须返回0,也就是compare(o1,o2)=0
**反对称性:**如果compare(o1,o2) = -1,则必须要求compare(o2,o1)返回是-1;
否则就会出现下面的这个错误,如下图:
因此我们约定俗成的习惯是,返回三个值,如果不交换顺序就返回-1,如果想要交换顺序就返回1,如果两个数相等就返回0,如下图:
这样使用Comparator的时候就不会出现上面的异常了。
排序算法的模板工具
public class SortUtil {
//判断第一个元素是否比第二个元素小
public static boolean less(Comparable first, Comparable second) {
return first.compareTo(second) < 0;
}
//交换数组元素中的某两个下标的元素
public static void exchange(Comparable[] elems, int index1, int index2) {
Comparable middle = elems[index1];
elems[index1] = elems[index2];
elems[index2] = middle;
}
}
选择排序算法
选择排序算法逻辑:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直至将整个数组排序。这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最小者。
选择排序算法代码如下:
public class Selection {
private static final Date[] dates = new Date[4];
static {
Date date1 = new Date(1, 3, 2023);
Date date2 = new Date(1, 2, 2024);
Date date3 = new Date(2, 1, 2022);
Date date4 = new Date(1, 3, 2023);
dates[0] = date1;
dates[1] = date2;
dates[2] = date3;
dates[3] = date4;
}
//将elems中的元素升序排列
public static void sort(Comparable[] elems) {
int length = elems.length;
for(int i = 0; i < length; i++) {
int minIndex = i;
for(int j = i+1; j < length; j++) {
if(SortUtils.less(elems[j], elems[minIndex])) {
minIndex = j;
}
}
SortUtils.exchange(elems, i, minIndex);
}
}
//测试排序结果
public static void main(String[] args) {
sort(dates);
System.out.println(Arrays.toString(dates));
}
}
//运行main方法之后的输出结果,如下
[Date{day=1, month=2, year=2024}, Date{day=1, month=3, year=2023}, Date{day=1, month=3, year=2023}, Date{day=2, month=1, year=2022}]
//从上面的输出结果中可以发现,使用选择排序之后,成功的把数组元素按从小到大的顺序排列了
选择排序的应用场景:当我们的数组里面的元素基本没有初始顺序的时候,我们使用选择排序,如果数组里面的元素的初始顺序有很多的话,我们就不要用选择排序了,为什么呢?因为假如我们想要用选择排序给数组元素从小到大的排序,而我们数组里面的元素的初始顺序已经是从小到大排序了,当你使用选择排序的时候,我们的循环次数仍然不变,仍然会循环查找所有的元素,所以这个时候效率就比较低了。
插入排序算法
插入排序算法逻辑:通常人们整理桥牌的方法是一张一张的来,将每一张牌插入到其它已经有序的牌中的适当位置。在计算机的实现中,为了给插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。这种算法就叫做插入排序,与选择排序一样,当前索引左边的所有元素都是有序的,但他们的最终位置还不确定(比如 5 7 10 6 8,假如说我们现在的当前位置是在6这个地方,6前面的三个元素已经是从小到大排序了,但是它们的最终位置仍然不确定,因为我们把6插入进来之后,就变成了5 6 7 10 8),为了给更小的元素腾出空间,它们可能会移动。但是当索引到大数组的右端时,数组排序就完成了。
和算则排序不同的是,插入排序所需的时间取决于输入中元素的初始顺序。例如对一个很大且其中的元素已经有序(或者接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快的多。
插入排序算法代码如下:
public class Insertion {
private static final Date[] dates = new Date[4];
static {
Date date1 = new Date(1, 3, 2023);
Date date2 = new Date(1, 2, 2024);
Date date3 = new Date(2, 1, 2022);
Date date4 = new Date(1, 3, 2023);
dates[0] = date1;
dates[1] = date2;
dates[2] = date3;
dates[3] = date4;
}
//将elems中的元素升序排列
public static void sort(Comparable[] elems) {
int length = elems.length;
for(int i = 1; i < length; i++) {
//将elems[i] 插入到 elems[i-1],elems[i-2],elems[i-3]...中
for(int j = i; j > 0 && SortUtil.less(elems[j], elems[j-1]); j--) {
SortUtil.exchange(elems, j , j-1);
}
}
}
//测试排序结果
public static void main(String[] args) {
sort(dates);
System.out.println(Arrays.toString(dates));
}
}
//运行main方法之后的输出结果,如下
[Date{day=1, month=2, year=2024}, Date{day=1, month=3, year=2023}, Date{day=1, month=3, year=2023}, Date{day=2, month=1, year=2022}]
//从上面的输出结果中可以发现,使用插入排序之后,成功的把数组元素按从小到大的顺序排列了
插入排序算法的应用场景:当数组中的元素存在很多原始顺序的时候,比如说前面的很多元素都小于后面的元素,这个时候可以使用插入排序算法,这会让我们的程序省略运行很多for循环,可以提升我们的程序效率。