《算法》—排序算法总结
文章结构
公用代码
/* 后面的排序算法会实现这个接口 */
public interface Sort<T> {
// 排序具体实现
void sort(Comparable<T>[] data);
// 比较算法,v < w 时返回true
default boolean less(Comparable<T> v, Comparable<T> w) {
return v.compareTo((T)w) < 0;
}
// 交换元素
default void exch(Comparable<T>[] data, int i, int j) {
Comparable<T> t = data[i];
data[i] = data[j];
data[j] = t;
}
// 打印数组
default void show(Comparable<T>[] data) {
for (Comparable<T> i : data) {
System.out.println(i);
}
System.out.println();
}
// 是否完成排序,用于检验结果是否完成排序
default boolean isSort(Comparable<T>[] data) {
for (int i = 1; i < data.length; i++) {
if (less(data[i], data[i-1])) {
return false;
}
}
return true;
}
/**
* 归并数组方法
* [1,3,5,7,2,4,6,8] => [1,2,3,4,5,6,7,8]
* @param aux 归并辅助数组
* @param data 原始数组
* @param low 需要归并的开始索引
* @param mid 需要归并的中间索引
* @param high 需要归并的结束索引
*/
default void merge(Comparable<T>[] aux, Comparable<T>[] data, int low, int mid, int high) {
int i = low;
int j = mid + 1;
for (int k=low; k<=high; k++) {
aux[k] = data[k];
}
for (int k=low; k<=high; k++) {
if (i > mid) {
data[k] = aux[j++];
}
else if (j > high) {
data[k] = aux[i++];
}
else if (less(aux[j], aux[i])) {
data[k] = aux[j++];
}
else {
data[k] = aux[i++];
}
}
}
}
/* 运行时间统计工具类 */
public class Watch {
private long startPoint;
private long endPoint;
private WatchState watchState;
private enum WatchState {
// 初始化完成
init,
// 运行中
running,
// 停止
stop;
}
public Watch(long startPoint, long endPoint, WatchState watchState) {
this.startPoint = startPoint;
this.endPoint = endPoint;
this.watchState = watchState;
}
// 新建实例
public static Watch newWatch() {
return new Watch(0, 0, WatchState.init);
}
// 开始计时
public void start() {
if (watchState != WatchState.init) {
throw new RuntimeException("非重置状态,请先设置为重置状态");
}
this.startPoint = System.currentTimeMillis();
watchState = WatchState.running;
}
// 停止计时
public void stop() {
if (watchState != WatchState.running) {
throw new RuntimeException("非运行状态,无法停止");
}
this.endPoint = System.currentTimeMillis();
watchState = WatchState.stop;
}
// 读取耗时
public long read(TimeUnit timeUnit) {
if (watchState != WatchState.stop) {
throw new RuntimeException("非停止状态,无法读取");
}
long timeCost = endPoint - startPoint;
return timeUnit.convert(Duration.ofMillis(timeCost));
}
// 打印耗时
public void printRead(TimeUnit timeUnit) {
long time = read(timeUnit);
System.out.println(String.format("Cost: %d %s", time, timeUnit.toString().toLowerCase()));
}
// 重置计时器,方便复用
public void reset() {
startPoint = 0;
endPoint = 0;
watchState = WatchState.init;
}
}
简单排序算法
1.选择排序
最简单的一种排序算法,符合正常思维,实现起来也是最简单的。时间复杂度为平方级复杂度:O(N^2)
算法描述
首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最小者。
下面是示例代码
public class Selection implements Sort<Double>{
@Override
public void sort(Comparable<Double>[] data) {
int N = data.length;
for (int i = 0; i < N; i++) {
int min = i;
// 此循环是找出从i个元素之后最小的元素的索引
for (int j = i+1; j < N; j++) {
if (less(data[j], data[min])) {
min = j;
}
}
// 与最小的元素的值进行交换
exch(data, i, min);
}
}
// 运行算法
public static void main(String[] args) {
Double[] data = new Double[10000];
for (int i = 0; i < 10000; i++) {
data[i] = Math.random();
}
final Selection selection = new Selection();
final Watch watch = Watch.newWatch();
watch.start();
selection.sort(data);
watch.stop();
selection.show(data);
System.out.println("isSort: " + selection.isSort(data));
watch.printRead(TimeUnit.MILLISECONDS);
}
}
/*
运行结果:
isSort: true
Cost: 147 milliseconds,花费了147毫秒
*/
算法特点
运行时间与输入无关:无论输入是有序数组还是无序数组,运行的任务量是固定的。
上一次的遍历并不会给下一次的遍历提供时间收敛信息。
数据移动量最少:每次遍历最多只需要交换两个元素。
循环次数多,交换次数少(固定)
不需要额外的内存空间,在原序列上交换排序
适用场景
数据量小,方便调用的数据序列排序时使用
2.插入排序
算法描述
在计算机的实现中,为了给要插入的元素腾出空间,们需要将其余所有元素在插入之前都向右移动一位。这种算法叫做插入排序。具体实现是:从左到右遍历数组,每次遍历都将左边剩余数组往右移动一位,插入相对较小元素,实现排序。
算法示例代码
public class Insertion implements Sort<Double> {
@Override
public void sort(Comparable<Double>[] data) {
int N = data.length;
for (int i = 1; i < N; i++) {
// 遍历索引i之前的所有元素,当后一个元素小于前一个元素的时候,交换两个元素的位置
for (int j = i; j > 0 && less(data[j], data[j-1]); j--) {
exch(data, j, j-1);
}
}
}
public static void main(String[] args) {
Double[] data = new Double[10000];
for (int i = 0; i < 10000; i++) {
data[i] = Math.random();
}
final Insertion insertion = new Insertion();
final Watch watch = Watch.newWatch();
watch.start();
insertion.sort(data);
watch.stop();
insertion.show(data);
System.out.println("isSort: " + insertion.isSort(data));
watch.printRead(TimeUnit.MILLISECONDS);
}
}
/*
运行结果:
isSort: true
Cost: 274 milliseconds
*/
理论上插入排序的性能要好于选择排序的,但是对于随机混乱的数组来说,插入排序的性能不如选择排序。所以在选择排序算法的时候,需要考虑输入的序列是不是随机混乱的数组。插入排序不适合大规模的均匀混乱的无序序列的排序。
算法特点
插入排序算法适用于大部分为有序元素的序列,效率相较于选择排序算法,元素的交换次数明显减少。
循环次数相对固定,但交换次数明显增多(交换次数于初始序列的混乱程度有关)
插入排序所需的时间取决于输入中元素的初始顺序。如果初始序列中存在部分有序,那么通常比完全混乱的序列速度更快
适用场景
插入排序所需的时间取决于输入中元素的初始顺序,实际应用中对某些非随机数组很有效
小数量快速排序场景
3.希尔排序
希尔排序算法是对插入排序算法的改进算法,插入排序算法存在一个问题:如果两个相邻元素在元素输入序列中相隔很远。在循环中需要花费很多次的交换操作才能排列成两个元素相邻,消耗了大量时间。而希尔排序可以跨远距离的迁移交换元素,明显减少排序的交换次数。还有一个原因是,插入排序的上一次循环的结果并不会影响下一次循环的交换量。而希尔排序的由于是跨相邻元素的排序,在上一次循环结束后,会直接减少下次循环的交换量。
算法描述
希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。使数组中任意间隔为h的元素都是有序的。这样的数组被称为h有序数组。换句话说,一个h有序数组就是h个互相独立的有序数组编织在一起组成的一个数组。在进行排序时,如果h很大,们就能将元素移动到很远的地方,为实现更小的h有序创造方便
代码示例
public class Shell implements Sort<Double> {
@Override
public void sort(Comparable<Double>[] data) {
int N = data.length;
int h = 1;
// 选择切分步长h
while( h < N/3) {
h = 3*h + 1;
}
while (h >= 1) {
for (int i = h; i < N; i++) {
for (int j = i; j >= h && less(data[j], data[j-h]); j = j - h) {
exch(data, j, j-h);
}
}
h = (h / 3);
}
}
public static void main(String[] args) {
Double[] data = new Double[10000];
for (int i = 0; i < 10000; i++) {
data[i] = Math.random();
}
final Shell shell = new Shell();
final Watch watch = Watch.newWatch();
watch.start();
shell.sort(data);
watch.stop();
shell.show(data);
System.out.println("isSort: " + shell.isSort(data));
watch.printRead(TimeUnit.MILLISECONDS);
}
}
算法特点
速度很快,前一次多循环会帮助后一次多循环减少交换多次数,具有收敛效应。虽然算法结构是两个嵌套的for循环,但是时间复杂度是小于O(n^2)。
适用场景
适用性强,对于中等规模的数组也有很好的适用性,比插入排序具有更好的性能。通常中等大小的排序是可以接收的,属于实际开发中用的最多的排序算法。其它更加高效的算法,具有更加复杂的代码结构,但性能提升上只会比希尔排序快两倍(可能还达不到 )。最佳实践是,应先考虑使用希尔排序,然后才考虑更换更加复杂的高效排序算法,因为其它高效的排序算法,往往结构都非常复杂,给程序加入不必要的复杂性往往在实践中意味项目会逐渐失去控制性。
归并排序算法
归并排序的算法与简单排序算法不同的地方是,归并排序由于需要归并操作,所以需要一块额外的内存空间用于暂存归并操作时的原始数据
自顶向下归并排序
算法描述
递归二分数组至不能二分,然后排序后再进行归并。核心代码是归并代码
代码示例
/**
* 自顶向下归并排序算法
*/
public class MergeFromTopSort implements Sort<Double> {
private static Comparable<Double>[] aux;
@Override
public void sort(Comparable<Double>[] data) {
aux = new Double[data.length];
sort(data, 0, data.length - 1);
}
private void sort(Comparable<Double>[] data, int low, int high) {
if (low >= high) {
return;
}
int mid = low + (high - low)/2;
// 左半函数
sort(data, low, mid);
// 右半函数
sort(data, mid+1, high);
// 归并左右函数
merge(aux, data, low, mid, high);
}
public static void main(String[] args) {
Double[] data = new Double[10000];
for (int i = 0; i < 10000; i++) {
data[i] = Math.random();
}
final MergeFromTopSort mergeFromTopSort = new MergeFromTopSort();
final Watch watch = Watch.newWatch();
watch.start();
mergeFromTopSort.sort(data);
watch.stop();
mergeFromTopSort.show(data);
System.out.println("isSort: " + mergeFromTopSort.isSort(data));
watch.printRead(TimeUnit.MILLISECONDS);
}
}
///运行结果
// isSort: true
// Cost: 9 milliseconds
可以看到速度是非常快的,随机1w数组的排序时间花费了9毫秒
自底向上的归并排序
算法描述
示例代码
/**
* 自底向上的归并算法
*/
public class MergeFromBottomSort implements Sort<Double> {
private static Comparable<Double>[] aux;
@Override
public void sort(Comparable<Double>[] data) {
int N = data.length;
aux = new Double[N];
// i是间隔步长,i=1,2,4,8,16,32....
for (int i=1; i<N; i *= 2) {
for (int low=0; low < N-i; low += 2*i) {
int mid = low + i -1;
int high = mid + i;
merge(aux, data, low, mid, Math.min(high, N-1));
}
}
}
public static void main(String[] args) {
Double[] data = new Double[10000];
for (int i = 0; i < 10000; i++) {
data[i] = Math.random();
}
final MergeFromBottomSort mergeFromBottomSort = new MergeFromBottomSort();
final Watch watch = Watch.newWatch();
watch.start();
mergeFromBottomSort.sort(data);
watch.stop();
mergeFromBottomSort.show(data);
System.out.println("isSort: " + mergeFromBottomSort.isSort(data));
watch.printRead(TimeUnit.MILLISECONDS);
}
}
//运行结果
// isSort: true
// Cost: 8 milliseconds
随机1w数组的排序话费8毫秒
快速排序算法
算法描述
/**
* 快速排序
*/
public class QuickSort implements Sort<Double> {
@Override
public void sort(Comparable<Double>[] data) {
sort(data, 0, data.length - 1);
}
// 排序
private void sort(Comparable<Double>[] data, int low, int high) {
if (low >= high) {
return;
}
// 切分点索引
int j = partition(data, low, high);
// 递归切分左侧
sort(data, low, j-1);
// 递归切分右侧
sort(data, j+1, high);
}
// 切分左右有序序列,左侧都小于j位置元素,右侧元素都大于j位置元素
private int partition(Comparable<Double>[] data, int low, int high) {
int i = low;
int j = high + 1;
Comparable<Double> v = data[low];
while (true) {
while (less(data[++i], v)) {
if (i == high) {
break;
}
}
while (less(v, data[--j])) {
if (j == low) {
break;
}
}
if (i >= j) {
break;
}
exch(data, i, j);
}
exch(data, low, j);
return j;
}
public static void main(String[] args) {
Double[] data = new Double[10000];
for (int i = 0; i < 10000; i++) {
data[i] = Math.random();
}
final QuickSort quickSort = new QuickSort();
final Watch watch = Watch.newWatch();
watch.start();
quickSort.sort(data);
watch.stop();
quickSort.show(data);
System.out.println("isSort: " + quickSort.isSort(data));
watch.printRead(TimeUnit.MILLISECONDS);
}
}
特点
- 比较次数很少
- 快速排序切分方法的内循环会用一个递增的索引将数组元素和一个定值比较。这种简洁性也是快速排序的一个优点。
总结
快速排序是最快的通用排序算法。自从数十年前快速排序发明以来,它在无数计算机系统中的无数实现已经证明了这一点。
大多数实际情况中,快速排序是最佳选择
参考资料
《算法:第4版》
原文创作:沐风之境
原文链接:https://www.cnblogs.com/mufeng3421/p/12942315.html