最经典的、最常用的:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序
排序算法的执行效率
- 最好情况、最坏情况、平均情况时间复杂度
- 时间复杂度的系数、常数 、低阶
- 比较次数和交换(或移动)次数
排序算法的内存消耗
原地排序(Sorted in place)。原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。
排序算法的稳定性
稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
举例子
比如我们有一组数据 2,9,3,4,8,3,按照大小排序之后就是 2,3,3,4,8,9。
这组数据里有两个 3。经过某种排序算法排序之后,如果两个 3 的前后顺序没有改变,那我们就把这种排序算法叫作稳定的排序算法;如果前后顺序发生变化,那对应的排序算法就叫作不稳定的排序算法。
比如说,我们现在要给电商交易系统中的“订单”排序。订单有两个属性,一个是下单时间,另一个是订单金额。如果我们现在有 10 万条订单数据,我们希望按照金额从小到大对订单数据排序。对于金额相同的订单,我们希望按照下单时间从早到晚有序。对于这样一个排序需求,我们怎么来做呢?
最先想到的方法是:我们先按照金额对订单数据进行排序,然后,再遍历排序之后的订单数据,对于每个金额相同的小区间再按照下单时间排序。这种排序思路理解起来不难,但是实现起来会很复杂。
借助稳定排序算法,这个问题可以非常简洁地解决。解决思路是这样的:我们先按照下单时间给订单排序,注意是按照下单时间,不是金额。排序完成之后,我们用稳定排序算法,按照订单金额重新排序。两遍排序之后,我们得到的订单数据就是按照金额从小到大排序,金额相同的订单按照下单时间从早到晚排序的。为什么呢?稳定排序算法可以保持金额相同的两个对象,在排序之后的前后顺序不变。第一次排序之后,所有的订单按照下单时间从早到晚有序了。在第二次排序中,我们用的是稳定的排序算法,所以经过第二次排序之后,相同金额的订单仍然保持下单时间从早到晚有序。
拓展
稳定排序算法指的是排序算法在排序相同键值的元素时,能保持其原有的相对次序。也就是说,在排序后的序列中,相等元素的先后顺序和在原始序列中的顺序相同。稳定排序对于有多个排序键(排序条件)的情况特别有用,比如上面提到的订单排序问题。
稳定排序算法的包括:
- 冒泡排序(Bubble Sort)
- 插入排序(Insertion Sort)
- 归并排序(Merge Sort)
- 计数排序(Counting Sort)
- 基数排序(Radix Sort)
使用稳定排序算法解决问题可以分为两步(还是以订单排序为例):
-
首先按照下单时间排序:首先使用一种稳定排序算法(如归并排序)对订单数据按照下单时间进行排序。此时,所有订单都是按照时间顺序排列的。
-
然后按照订单金额稳定排序:然后再次使用稳定排序算法(同样可以是归并排序)按照订单金额进行排序。由于算法是稳定的,因此在第二次排序中,金额相同的订单会保持第一次排序后的顺序,即下单时间的顺序。
使用稳定排序算法的好处是简化了排序逻辑。比如订单排序中,只需进行两次排序操作,无需额外的逻辑来处理金额相同订单的时间排序。稳定排序算法确保了第一次排序的顺序在第二次排序中得以保留,这对于处理复杂数据集的多重排序条件非常重要。
在Java中,Collections.sort()
和 Arrays.sort()
针对对象数组和集合默认使用的是 TimSort 算法,这是一种稳定的排序算法。TimSort 是一种优化的归并排序,它结合了归并排序和插入排序的优势,对于小数组会使用插入排序,对于大数组则使用归并排序的优化版本。
如果要在Java中进行稳定排序,可以直接使用这些工具类。例如,有一个订单类 Order
,它包含一个时间戳属性 timestamp
和一个金额属性 amount
,可以如下使用 Collections.sort()
或 Arrays.sort()
:
import java.util.*;
class Order {
private Date timestamp;
private double amount;
// 构造器、getters 和 setters 省略
public Date getTimestamp() {
return timestamp;
}
public double getAmount() {
return amount;
}
}
// 假设有一个订单列表
List<Order> orders = ...;
// 先按照下单时间排序
Collections.sort(orders, new Comparator<Order>() {
@Override
public int compare(Order o1, Order o2) {
return o1.getTimestamp().compareTo(o2.getTimestamp());
}
});
// 再按照金额稳定排序
Collections.sort(orders, new Comparator<Order>() {
@Override
public int compare(Order o1, Order o2) {
return Double.compare(o1.getAmount(), o2.getAmount());
}
});
在这个例子中,Collections.sort()
第一次调用会根据下单时间进行排序,然后第二次调用根据订单金额进行排序,由于 Collections.sort()
是稳定的,所以金额相同的订单会保持第一次排序后的时间顺序。
Lambda表达式来简化代码:
// 按下单时间排序
orders.sort(Comparator.comparing(Order::getTimestamp));
// 再按金额稳定排序
orders.sort(Comparator.comparingDouble(Order::getAmount));
这样,通过两次排序,可以得到先按金额排序,然后在金额相同的情况下按时间先后排序的订单列表。
学习:极客时间《数据结构与算法之美》学习笔记