排序
父类设计
为了测试方便,设计了Sort抽象父类,父类中有需要排序的数组、交换次数等成员变量。后面的各个排序都继承这个父类,重写sort方法,使用的适合用上转型实例化,比如:Sort quickSort = new QuickSort();然后调用父类sort方法自动调用quickSort子类方法,并计算比较次数 、耗费时间等,打印对象即可获取这些信息
package sort;
import java.util.Arrays;
/**
* @author FatTiger
* @date 2020-12-04 14:54
**/
public abstract class Sort {
protected Integer array[];
private long cmpCount;
private long swapCount;
public long time;
public void sort(Integer array[]) {
if (array == null || array.length < 2) return;
this.array = array;
long begin = System.currentTimeMillis();
sort();
time = System.currentTimeMillis() - begin;
}
protected abstract void sort();
protected int cmp(int i1, int i2) {
cmpCount++;
return array[i1] - array[i2];
}
protected int cmp1(int value1, int value2) {
cmpCount++;
return value1 - value2;
}
protected void swap(int i1, int i2) {
swapCount++;
int tmp = array[i1];
array[i1] = array[i2];
array[i2] = tmp;
}
@Override
public String toString() {
return this.getClass().getName() + "\t{\t" +
"\t比较次数=" + cmpCount +
"\t交换次数=" + swapCount +
"\t耗费时间 =" + time +
'}';
}
public String toStringArr() {
return Arrays.toString(array);
}
}
冒泡排序
执行流程
- 从头开始比较每一对相邻元素,如果第一个比第二个大,就交换他们的位置
- 执行完一轮之后,最后的元素就是最大的元素
public static void bubbleSort(int array[]) {
for (int i = 0; i < array.length; i++) {
for (int begin = 1; begin < array.length - i; begin++) {
if (array[begin] < array[begin - 1]) {
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
}
}
}
}
优化1
如果序列已经完全有序,可以提前终止冒泡排序
每次循环加入标记,如果有一次不符合前面一个比后面一个要小于(大于)的情况,就置为false,在每次循环走完之后判断是否为true,即可做到有序时提前终止排序
public static void bubbleSort(int array[]) {
for (int i = 0; i < array.length; i++) {
boolean sorted = true;
for (int begin = 1; begin < array.length - i; begin++) {
if (array[begin] < array[begin - 1]) {
sorted = false;
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
}
}
if (sorted) {
break;
}
}
}
优化2
如果序列尾部已经局部有序,可以记录最后一次交换的位置,减少比较次数
public static void bubbleSort(Integer array[]) {
for (int end = array.length - 1; end > 0 ; end--) {
int sortedIndex = 1;
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
sortedIndex = begin;
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
}
}
end = sortedIndex;
}
}
稳定性
- 如果相等的两个元素,在排序前后的相对位置保持不变,那么这是稳定的排序算法
- 冒泡是稳定的
原地算法(in-place Algorithm)
不依赖额外的资源或者依赖少数的额外资源,近依靠输出来覆盖输入
代码
public class BubbleSort extends Sort {
@Override
protected void sort() {
for (int end = array.length - 1; end > 0 ; end--) {
int sortedIndex = 1;
for (int begin = 1; begin <= end; begin++) {
if (cmp(begin, begin-1) < 0) {
sortedIndex = begin;
swap(begin, begin-1);
}
}
end = sortedIndex;
}
}
}
选择排序
执行流程
- 从序列中找出最大的那个元素,然后与最末尾的元素交换位置
- 执行完一轮后,最末尾的那个元素就是最大的元素
- 忽略曾经找到的最大元素,重复执行步骤1
public class SelectionSort extends Sort {
@Override
protected void sort() {
for (int end = array.length - 1; end > 0 ; end--) {
int maxIndex = 0;
for (int begin = 1; begin <= end; begin++) {
if (cmp(begin, maxIndex) >= 0) {
maxIndex = begin;
}
}
swap(maxIndex, end);
}
}
}
选择排序的交换次数远远小于冒泡排序,平均性能要优于冒泡
优化
使用堆
堆排序(heap sort)
- 堆排序可以认为是对选择排序的一种优化
执行流程
- 对序列进行原地建堆(heapify)
- 重复执行以下操作,直到堆的元素数量为1
- 交换堆顶元素与尾元素
- 堆的元素数量-1
- 对0位置进行一次siftDown操作
代码
public class HeapSort extends Sort {
private int heapSize;
@Override
protected void sort() {
// 原地建堆
heapSize = array.length;
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
while (heapSize > 1) {
// 交换堆顶元素和尾部元素
swap(0, --heapSize);
// 对0位置进行siftDown(恢复堆的性质)
siftDown(0);
}
}
private void siftDown(int index) {
Integer element = array[index];
int half = heapSize >> 1;
while (index < half) { // index必须是非叶子节点
// 默认是左边跟父节点比
int childIndex = (index << 1) + 1;
Integer child = array[childIndex];
int rightIndex = childIndex + 1;
// 右子节点比左子节点大
if (rightIndex < heapSize &&
cmp1(array[rightIndex], child) > 0) {
child = array[childIndex = rightIndex];
}
// 大于等于子节点
if (cmp1(element, child) >= 0) break;
array[index] = child;
index = childIndex;
}
array[index] = element;
}
}
插入排序(insertion Sort)
插入排序非常类似于扑克牌的排序
执行流程
- 在执行过程中,插入排序会将序列分为2部分
- 头部是已经排好序的,尾部是待排序的
- 从头开始扫描每一个元素
- 每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
while (cur > 0 && cmp(cur, cur-1) > 0) {
swap(cur, cur-1);
cur--;
}
}
逆序对(invertion)
- 什么是逆序对
- 数组<2,3,8,6,1>的逆序对为:<2,1> ❤️,1> <8,1> <8,6> <6,1>,共五个逆序对
- 插入排序的时间复杂度与逆序对的数量成正比关系
- 逆序对的数量越多,插入排序的时间复杂度越高
- 最坏、平均复杂度为O(n2)
- 最好的复杂度是O(n)
- 当逆序对的数量极少时,插入排序的效率特别高
- 甚至比O(nlogn)级别的快速排序还要快
优化
思路是将【交换】转为【挪动】
- 先将待插入的元素备份
- 头部有序数据中比待插入元素大的,都朝尾部方向挪动一个位置
- 将待插入元素放到最终的合适位置
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
int v = array[cur];
while (cur > 0 && cmp(cur, cur-1) > 0) {
array[cur] = array[cur - 1];
cur--;
}
array[cur] = v;
}
二分搜索
如何确定一个元素在数组中的位置?(假设数组里全是整数)
- 如果是无序数组,从第0个位置开始遍历搜索,平均时间复杂度:O(n)
- 如果是有序数组,可以使用二分搜索,最坏时间复杂度:O(logn)
思路
- 假设在[begin,end)范围内搜索某个元素v,mid == (begin + end) / 2
- 如果v < m ,去[begin,mid)范围内二分搜索
- 如果v > m,去[mid+1,end)范围内二分搜索
- 如果v == m,直接返回mid
思考
如果存在多个重复的值返回哪一个?
无规律
插入排序-二分搜索优化
- 在元素v的插入过程中,可以先二分搜索除合适的插入位置,然后再将元素v插入
- 要求二分搜索返回的插入位置:第一个大于v的元素位置
思路
- 假设在[begin,end)范围内搜索某个元素v,mid == (begin + end) / 2
- 如果v < m ,去[begin,mid)范围内二分搜索
- 如果v >= m,去[mid+1, end) 范围内二分搜索
代码
public class InsertionSort extends Sort {
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
int v = array[begin];
int insertIndex = search(begin);
for (int i = begin; i > insertIndex; i--) {
array[i] = array[i - 1];
}
array[insertIndex] = v;
}
}
public int search(int index) {
int v = array[index];
int begin = 0;
int end = index;
while (begin < end) {
int mid = (begin + end) >> 1;
if (cmp1(v, array[mid]) < 0) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin;
}
}
归并排序(Merge Sort)
执行流程
- 不断地将当前序列平均分割成两个子序列
- 直到不能再分割(序列中只剩一个元素)
- 不断地将2个子序列合并成一个有序序列
- 直到最终只剩下一个有序序列
divide实现
/**
* 对[begin,end)范围的数据进行归并排序
* @param begin
* @param end
* @return void
* @author FatTiger
* @date 2020/12/6 13:27
*/
private void sort(int begin, int end) {
if (end - begin < 2) return;
int mid = (begin + end) >> 1;
sort(begin, mid);
sort(mid, end);
merge(begin,mid,end);
}
merge实现
/**
* 将[begin,mid) 和 [mid,end)合并成一个有序的序列
* @param begin
* @param mid
* @param end
* @return void
* @author FatTiger
* @date 2020/12/6 13:39
*/
private void merge(int begin, int mid, int end) {
//左边数组(基于leftArray)
int li = 0,le = mid - begin;
//右边数组(基于array)
int ri = mid, re = end;
//array的索引
int ai = begin;
//备份左边数组
for (int i = li; i < le; i++) {
leftArray[i] = array[begin + i];
}
//如果左边还没有结束
while (li < le) {
//如果右边小拷贝右边元素到array
if (ri < re && cmp1(array[ri], leftArray[li]) < 0) {
array[ai++] = array[ri++];
} else {
//如果左边小或者相等,就将左边元素拷贝到Array
array[ai++] = leftArray[li++];
}
//cmp1位置改成<=就会不稳定
}
}
复杂度分析
代码
public class MergerSort extends Sort {
private int[] leftArray;
@Override
protected void sort() {
leftArray = new int[array.length >> 1];
sort(0, array.length);
}
/**
* 对[begin,end)范围的数据进行归并排序
* @param begin
* @param end
* @return void
* @author FatTiger
* @date 2020/12/6 13:27
*/
private void sort(int begin, int end) {
if (end - begin < 2) return;
int mid = (begin + end) >> 1;
sort(begin, mid);
sort(mid, end);
merge(begin,mid,end);
}
/**
* 将[begin,mid) 和 [mid,end)合并成一个有序的序列
* @param begin
* @param mid
* @param end
* @return void
* @author FatTiger
* @date 2020/12/6 13:39
*/
private void merge(int begin, int mid, int end) {
//左边数组(基于leftArray)
int li = 0,le = mid - begin;
//右边数组(基于array)
int ri = mid, re = end;
//array的索引
int ai = begin;
//备份左边数组
for (int i = li; i < le; i++) {
leftArray[i] = array[begin + i];
}
//如果左边还没有结束
while (li < le) {
//如果右边小拷贝右边元素到array
if (ri < re && cmp1(array[ri], leftArray[li]) < 0) {
array[ai++] = array[ri++];
} else {
//如果左边小或者相等,就将左边元素拷贝到Array
array[ai++] = leftArray[li++];
}
//cmp1位置改成<=就会不稳定
}
}
}
快速排序⭐(quickSort)
执行流程
- 从序列中选择一个轴点元素(pivot)
- 假设每次选择0位置的元素为轴点元素
- 利用pivot将序列分割成2个子序列
- 将小于pivot的元素放在pivot前面(左侧)
- 将大于pivot的元素放在pivot后面(右侧)
- 等于pivot的元素放哪边都可以
- 对子序列进行上面的操作
- 直到不能再分割(子序列中只剩下1个元素)
本质
逐渐将每个元素都变成轴点元素
轴点构造
最坏情况
左右两边元素极度不均匀的情况
解决办法
随机选择轴点元素
代码
public class QuickSort extends Sort {
@Override
protected void sort() {
sort(0, array.length);
}
/**
* 对[begin,end)进行快速排序
* @param begin
* @param end
* @return void
* @author FatTiger
* @date 2020/12/7 11:05
*/
private void sort(int begin, int end) {
if (end - begin < 2) return;
//确定轴点位置
int mid = pivotIndex(begin,end);
//对子序列进行快速排序
sort(begin, mid);
sort(mid + 1, end);
}
/**
* 确定轴点元素位置
* @param begin
* @param end
* @return int 轴点元素的最终位置
* @author FatTiger
* @date 2020/12/7 11:07
*/
private int pivotIndex(int begin, int end) {
//随机选择轴点元素与头位置交换
swap(begin, begin + (int) (Math.random() * (end - begin)));
//备份begin位置的元素
int pivot = array[begin];
//end要指向到最后一个元素
end--;
while (begin < end) {
while (begin < end) {
if (cmp1(pivot,array[end]) < 0) {//右边元素大于轴点元素
end--;
} else {//右边元素小于等于轴点元素
array[begin++] = array[end];
break;
}
}
while (begin < end) {
if (cmp1(pivot,array[begin]) > 0) {//左边元素小于轴点元素
begin++;
} else { //左边元素大于等于轴点元素
array[end--] = array[begin];
break;
}
}
}
//将轴点元素放入最终的位置
array[begin] = pivot;
return begin;
}
}
希尔排序(shell sort)
- 希尔排序把序列看作是一个矩阵,分成m列,逐列进行排序
- m从某个整数逐渐减为1
- 当m为1时,整个序列将完全有序
- 因此,希尔排序也被称为递减增量排序
- 矩阵的列数取决于步长序列
- 比如,如果步长序列为{1,5,19,41,109,…},就代表依次分成109列、41列、19列、5列、1列进行排序
- 不同的步长序列,执行效率也不同
代码
public class ShellSort extends Sort {
@Override
protected void sort() {
List<Integer> stepSequence = shellStepSequence();
for (Integer step : stepSequence) {
sort(step);
}
}
private void sort(Integer step) {
for (int col = 0; col < step; col++) {
//对col列进行排序
for (int begin = col + step; begin < array.length; begin += step) {
int cur = begin;
while (cur > col && cmp(cur,cur-step) < 0) {
swap(cur,cur-step);
cur -= step;
}
}
}
}
//采用shell的步长序列{16,8,4,2,1}
private List<Integer> shellStepSequence() {
ArrayList<Integer> stepSequence = new ArrayList<>();
int step = array.length;
while ((step = step >> 1) > 0) {
stepSequence.add(step);
}
return stepSequence;
}
}
计数排序(counting sort)
- 之前学习的冒泡、选择、插入、归并、快速、希尔、堆排序,都是基于比较的排序
- 平均时间复杂度目前最低是O(nlogn)
- 计数排序、桶排序、基数排序,都不是基于比较的排序
- 他们是典型的用空间换时间,在某些时候,平均时间复杂度可以比O(nlogn)更低
- 核心思想
- 统计每个整数在序列中出现的次数,进而推导除每个整数在有序序列中的索引
最简单的实现
//找出最大值
int max = array[0];
for (int i = 0; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
int counts[] = new int[max + 1];
//统计每个整数出现的次数
for (int i = 0; i < array.length; i++) {
counts[array[i]]++;
}
int j = 0;
//根据整数出现次数,对整数进行排序
for (int i = 0; i < counts.length; i++) {
//放入原数组
while (counts[i] > 0) {
array[j++] = i;
counts[i]--;
}
}
存在的问题
- 无法对负整数进行排序
- 极其浪费内存空间
- 是个不稳定的排序
改进思路
改进实现
public class CountSort extends Sort {
@Override
protected void sort() {
//找出最值
int max = array[0];
int min = array[0];
for (int i = 0; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
if (array[i] < min) {
min = array[i];
}
}
//存储次数
int counts[] = new int[max - min + 1];
//统计每个整数出现的次数
for (int i = 0; i < array.length; i++) {
counts[array[i] - min]++;
}
//累加次数
for (int i = 1; i < counts.length; i++) {
counts[i] += counts[i - 1];
}
//从后往前遍历元素,将它放到有序数组中的合适位置
Integer newArray[] = new Integer[array.length];
for (int i = array.length - 1; i >= 0; i--) {
newArray[ --counts[array[i]-min]] = array[i];
}
for (int i = 0; i < array.length; i++) {
array[i] = newArray[i];
}
}
}
基数排序(Radix Sort)
- 基数排序非常适合用于整数排序(尤其是非负整数)
- 执行流程:依次对个位数、十位数、百位数、千位数、万位数…进行排序(从低位到高位)
排序测试(参考)
public class Main implements Serializable {
public static void main(String[] args) {
Integer[] array = Integers.random(30000, 1, 50000);
testSorts(array,
new InsertionSort(),
new MergerSort(),
new BubbleSort(),
new SelectionSort(),
new HeapSort(),
new QuickSort(),
new ShellSort(),
new CountSort()
);
}
public static void testSorts(Integer array[], Sort... sorts) {
for (Sort sort : sorts) {
Integer[] array1 = Integers.copy(array);
sort.sort(array1);
Asserts.test(Integers.isAscOrder(array1));
}
Arrays.sort(sorts, new Comparator<Sort>() {
@Override
public int compare(Sort o1, Sort o2) {
return (int) (o1.time - o2.time);
}
});
for (Sort sort : sorts) {
System.out.println(sort);
}
}
}