详解冒泡、插入、选择三种排序- 2021-0-12 21:25- 算法
冒泡排序
冒泡排序其实是说算法的执行过程像冒泡泡一样,将数据从左到右或是从右到左的浮动过去。冒泡算法是通过比较数组中两两相临的数值大小,如果是结果是逆向的则交换两值的位置,重复这个过程知道数组最终有序。
代码实现
/**
* 冒泡算法
* 比较数组中相邻两个值的大小,并交换位置,将最小或最大的数分别放置在数组的首位或者末尾
*
* @author Bai
* @date 2021/8/22 10:49
*/
public class BubbleSort extends BaseSort {
/**
* 冒泡算法:升序排序
*
* @param arr
*/
@Override
void ascSort (int[] arr) {
//边界
if (null == arr || arr.length < 2) {
return;
}
//倒循环:缩小每次循环的范围,去除已经排序完成的位置
for (int e = arr.length; e > 0; e--) {
//正循环: 主要是用于排序
for (int i = 0; i < e - 1; i++) {
//相邻两个值进行比较 ,如果i位置的值大于i+1位置的值,则进行交换
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
/**
* 冒泡算法:正序排序
*
* @param arr
*/
@Override
void descSort (int[] arr) {
//边界
if (null == arr || arr.length < 2) {
return;
}
//倒循环:缩小每次循环的范围,去除已经排序完成的位置
for (int e = arr.length; e > 0; e--) {
//正循环: 主要是用于排序
for (int i = 0; i < e - 1; i++) {
//相邻两个值进行比较 ,如果i位置的值大于i+1位置的值,则进行交换
if (arr[i] < arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
}
时间复杂度O(n²)&空间复杂度O(1)
冒泡算法的平均时间复杂度是O(n²),最坏时间复杂度也是O(n²),而最好的时间复杂度是O(n)。冒泡算法需要循环两次来实现这个功能,第一次倒循环:缩小每次循环的范围,去除已经排序完成的位置;第二次正循环:实现相邻下标数据比较,并交换位置。循环一次的时间复杂度为O(n),最坏情况下就是两次循环都要执行一遍,所以冒泡排序最坏的时间复杂度为O(n²)。
最好时间复杂度O(n)
最好时间复杂度O(n)的成立是建立在 数组本身已经是有序的基础上,只需要执行一遍最外层的循环,即可结束。使用一个变量记录内层循环的执行状态,是否进行了swap操作,如果循环一轮下来并未进行交换操作,那么就可以认为这个数组已经是有序的。
/**
* 冒泡算法:算法最优写法
* 算法最好情况下时间复杂度是O(n),也就是数组本身已经是有序的状态下,只需要对数组进行一次循环,也就是O(n)
*
* @param arr
*/
public static void bestSort (int[] arr) {
//边界
if (null == arr || arr.length < 2) {
return;
}
//倒循环:缩小每次循环的范围,去除已经排序完成的位置
for (int e = arr.length; e > 0; e--) {
//初始假设这个数组是有序
boolean orderly = true;
//正循环: 主要是用于排序
for (int i = 0; i < e - 1; i++) {
//相邻两个值进行比较 ,如果i位置的值大于i+1位置的值,则进行交换
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
orderly = false;
}
}
//循环一次后,如果数组数组是有序的,则无须再排序
if (orderly) {
break;
}
}
}
测试
/**
* 算法测试类
* @author Bai
* @date 2021/9/11 14:17
*/
public abstract class BaseSort {
private Integer forCnt = 100000;
public BaseSort () {
}
public BaseSort (Integer forCnt) {
this.forCnt = forCnt;
}
/**
* 升序排序
*
* @param arr
*/
abstract void ascSort (int[] arr);
/**
* 降序排序
*
* @param arr
*/
abstract void descSort (int[] arr);
public void ascForCheck () {
for (int i = 0; i < forCnt; i++) {
//升序
int[] arr = getArr();
int[] arr1 = arr.clone();
ascSort(arr);
ascCheck(arr, arr1);
}
}
public void descForCheck () {
for (int i = 0; i < forCnt; i++) {
//升序
int[] arr = getArr();
int[] arr1 = arr.clone();
descSort(arr);
descCheck(arr, arr1);
}
}
public static void run (BaseSort baseSort) {
baseSort.ascForCheck();
baseSort.descForCheck();
}
/**
* 该方法只使用 两个变量不是同一内存,否则会出现问题
*
* @param arr
* @param i
* @param j
*/
public static void swap (int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static int[] getArr () {
Random random = new Random();
int[] arr = new int[random.nextInt(1000)];
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(10000);
}
return arr;
}
public static void ascCheck (int[] arr, int[] arr1) {
Arrays.sort(arr1);
check(arr, arr1);
}
public static void descCheck (int[] arr, int[] arr1) {
Integer[] trans = trans(arr1);
Arrays.sort(trans, Collections.reverseOrder());
check(arr, trans(trans));
}
public static void check (int[] arr, int[] arr1) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] != arr1[i]) {
System.out.println("↓----排序失败----↓");
System.out.println(JSONObject.toJSONString(arr));
System.out.println(JSONObject.toJSONString(arr1));
System.out.println("↑----排序失败----↑");
break;
}
}
}
public static Integer[] trans (int[] arr1) {
Integer[] integers = new Integer[arr1.length];
for (int i = 0; i < arr1.length; i++) {
integers[i] = arr1[i];
}
return integers;
}
public static int[] trans (Integer[] arr1) {
int[] integers = new int[arr1.length];
for (int i = 0; i < arr1.length; i++) {
integers[i] = arr1[i];
}
return integers;
}
public static void print (Object o) {
System.out.println(JSONObject.toJSONString(o));
}
}
public static void main (String[] args) {
run(new BubbleSort());
//最好情况下排序,时间复杂度是O(n)
int[] best = getArr();
Arrays.sort(best);
bestSort(best);
}
插入排序
插入排序的基本思想是将一个记录插入到有序表中,其实跟我们打牌时整理纸牌的顺序有点相似,假设第一张纸牌是有序的,将第二张与第一张有序的纸牌进行比较并整理,重复这个过程,将第三张纸牌与前两张进行比较寻找合适的位置插入,以此类推完成排序。
代码实现
package com.learn.test.algorithm;
/**
* 插入算法
* 比较像整理纸牌,是对未排序的数据,在有序队列中从后向前扫描找到合适位置插入。
*
* @author Bai
* @date 2021/8/22 13:53
*/
public class InsertionSort extends BaseSort {
/**
* 升序排序
*
* @param arr
*/
@Override
void ascSort (int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {
for (int j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
swap(arr, j, j - 1);
}
}
}
@Override
void descSort (int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j] < arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
public static void main (String[] args) {
run(new InsertionSort());
//优化算法check
for (int i = 0; i < 100000; i++) {
int[] arr = getArr();
int[] clone = arr.clone();
ascSortOp(arr);
ascCheck(arr, clone);
}
}
}
时间复杂度O(n²)&空间复杂度O(1)
插入排序跟冒泡算法一样,也是循环执行两次,它的复杂度也是O(n²),因为没有使用额外的空间,所以空间复杂度也是O(n)。上面这一版的算法其实还是可以进行优化一下的, 现在执行过程中会进行很多次的交换增加了时复杂度,下面优化使用变量来替代swap(),减少时间复杂度。
/**
* 算法优化
* 使用临时变量替代swap操作
*
* @param arr
*/
public static void ascSortOp (int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {
//记录最左边需要交换的下标
int lastMinIndex = i;
for (int j = i - 1; j >= 0 && arr[i] < arr[j]; j--) {
lastMinIndex = j;
}
if (lastMinIndex != i) {
//交换i位置的数据到lastMinIndex位置,将lastMinIndex与i之间的数集体后移一位
int tem = arr[i];
for (int j = i; j > lastMinIndex; j--) {
arr[j] = arr[j - 1];
}
arr[lastMinIndex] = tem;
}
}
}
选择排序
选择排序的思想很简单,就是每次在未排序的序列中找到最大或最小的值,放到序列的起始位置,接着继续在剩下未排序的序列中寻找最大或最小的值放到已排序序列的末尾,重复这个过程,直到完成排序。
代码实现
package com.learn.test.algorithm;
import java.util.Arrays;
/**
* 选择排序
* 每次寻找未排序的数字中最小或最大的数值,放到未排序的数值首位或是末尾
*
* @author Bai
* @date 2021/8/22 13:20
*/
public class SelectionSort extends BaseSort {
/**
* 升序
*/
@Override
void ascSort (int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
//每次循环之后最小值
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
/**
* 降序
*/
@Override
void descSort (int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
//每次循环之后最大值
int maxIndex = i;
for (int j = i + 1; j < arr.length; j++) {
maxIndex = arr[j] > arr[maxIndex] ? j : maxIndex;
}
swap(arr, i, maxIndex);
}
}
/**
* 升序
*/
public static void ascSortV2 (int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
int end = arr.length - 1;
for (int i = 0; i < end; i++) {
//每次循环之后最小值
int minIndex = i;
int minMax = i;
for (int j = i + 1; j < end + 1; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
minMax = arr[j] > arr[minMax] ? j : minMax;
}
if (i == minMax && end == minIndex) {
swap(arr, minIndex, minMax);
} else if (i == minMax) {
swap(arr, end, minMax);
if (i != minIndex) {
swap(arr, i, minIndex);
}
} else if (minIndex != minMax) {
if (i != minIndex) {
swap(arr, i, minIndex);
}
if (end != minMax) {
swap(arr, end, minMax);
}
}
end--;
}
}
public static void main (String[] args) {
run(new SelectionSort());
//同时找出 最大 最小 进行选择
v2();
}
private static void v2 () {
for (int i = 0; i < 100000; i++) {
//升序
int[] arr = getArr();
int[] arr1 = arr.clone();
ascSortV2(arr);
Arrays.sort(arr1);
check(arr, arr1);
}
}
}
三种算法的区别
时间复杂度
从时间复杂度上来说三者的平均时间复杂度都是O(n²),最坏的时间复杂度也是O(n²),而冒泡和插入的最好时间复杂度则都是O(n²)
空间复杂度
稳定度
选择排序是不稳的,冒泡排序和插入排序是稳定的