冒泡,插入,选择这三种基础的排序算法,比较简单效率不高,工做中通常不会使用,可是当作算法来研究仍是能了解一些知识的,本文以<数据结构与算法之美>为基础,详细解析一下.java
首先要引入几个概念git
若是待排序数组中有相同的元素,排序事后它们的相对顺序不发生变化. 好比 2,9,3,4,8,3
排序事后为2, 3 , 3, 4, 8, 9
这两个3的相对顺序不变.这样就是具备稳定性. 稳定性能够保证复杂数据结构排序时的相对有序性. 好比咱们要对一笔订单先按金额排列,金额相同的再按时间排序
就能够先将这笔订单按照时间排序,再采用具备稳定性的排序算法按金额进行排序. 猜想Java8中的compare().thenCompare()就是采用这种原理实现github
排序过程当中只占用常数级的额外空间或者不占用额外空间算法
数组中具备有序关系的元素对的个数,数学表达式为 有序元素对:a[i] <= a[j], 若是i < j。
2,4,3,1,5,6
的有序度为11,有序元素对为(2,4),(2,3),(2,5),(2,6),(4,5),(4,6),(3,5),(3,6),(1,5),(1,6),(5,6)
对 6,5,4,3,2,1
这个彻底逆序的数组,有序度为0
对 1,2,3,4,5,6
这个彻底有序的数组,有序度为(n-1)+(n-2)+(n-3)+...+0 = n*(n-1)/2 = 15
,即满有序度数组
和有序度定义正好相反 逆序度 = 满有序度 - 有序度数据结构
为了方便描述,这里约定数组为arr[0~n],n≥1,排列方式为正序(从小到大).
下面的描述不会举例讲解,推荐到 visualgo查看动画演示性能
首先将数组在逻辑划分为左侧的未排序
部分和右侧的已排序
部分
冒泡排序中对未排序
部分的一次遍历称为一次冒泡,初始时未排序部分为arr[0~n-1]
,未排序部分为空.冒泡一次后,未排序部分为arr[0~n-2]
,已排序部分为arr[n-1~n-1
],冒泡x
(1≤x<n
)次后,未排序部分为arr[0~n-1-x)
,已排序部分为arr[n-x~n-1]
,最终达成所有排序
每次冒泡未排序
部分时,只会操做相邻的元素,方向为 arr[0]→ arr[n-1],若是a[i]>a[i+1]
就进行交换,这样一次冒泡操做后,未排序
部分中的最大值就会移动到已排序
部分且在最左侧(即已排序部分的最小值).
同时,若是一次冒泡后没有进行过任何交换说明整个数组已经有序了,结合这个特性能够省去没必要要的冒泡,核心代码以下学习
public void sort(Comparable[] a) { if (a.length <= 1) { return; } for (int i = a.length - 1; i > 0; i--) { boolean changeFlag = false; for (int j = 0; j < i; j++) { if (a[j + 1].compareTo(a[j]) < 0) { swap(a, j + 1, j); changeFlag = true; } } if (!changeFlag) { break; } } }
最坏状况下,数组彻底倒序, 时间复杂度为 (n-1)+(n-2)+(n-3)+...+1 = (n^2)/2 ~= O(n^2) 数字表示交换的次数
最好状况下,数组已经有序,只须要一次冒泡, 时间复杂度为 O(n) 这里不会有交换,以比较的次数做为计量单位
平均状况下,用几率推算会很困难,这里采用上面提到的有序度
和逆序度
,并将元素的交换次数当作计量单位, 观察能够看到,每交换一次,都会使原来逆序的一组元素变为有序,逆序度即为交换次数,那么在最好状况下逆序度为0,最坏状况下逆序度为n(n-1)/2,平均时间复杂度为二者取平均值
(0 + n(n-1)/2)/2 = O(n^2)测试
冒泡排序涉及到两个元素的交换,交换时只使用了一个单位临时空间,是原地排序动画
冒泡排序中元素位置发生变化交换时,而且交换仅在a[i]>a[i+1]时才发生,a[i]==a[i+1]时时不会发生的,所以是稳定的
将数组分为左侧
的已排序部分和右侧
的未排序部分
初始时已排序部分为arr[0~0]
,未排序部分为arr[1~n-1]
一次插入后,已排序部分为arr[0~1]
,未排序部分为arr[2~n-1]
,x(1≤x<n)
次插入后,已排序部分为arr[0x],未排序部分为arr(x+1n-1]
从未排序部分的起始位置开始(即arr[1]),每次插入时,方向为 arr[n-1]→ arr[0],找出第一个小于等于arr[i]
的元素,有两种状况
public void sort(Comparable[] arr) { if (arr.length <= 1) { return; } for (int i = 1; i < arr.length; i++) { Comparable insertValue = arr[i]; int insertPos = i; for (int j = i - 1; j >= 0; j--) { if (insertValue.compareTo(arr[j]) < 0) { arr[j + 1] = arr[j]; insertPos = j; } else { insertPos = j + 1; break; } } arr[insertPos] = insertValue; } }
以元素移动的次数做为计量单位
最坏状况下,数组彻底倒序,时间复杂度为 0 + 1 + 2 + 3 +...+ (n-1) = n*(n-1)/2 ~= O(n^2)
最好状况下,数组彻底顺序, 时间复杂度为 1 + 1 + 1 + ... + 1= n ~=O(n)
平均状况下, 咱们仍然使用逆序数这个概念,再观察依旧能够得知: 逆序数等于移动次数,因此时间复杂度为 (0 + n(n-1)/2)/2 ~= O(n^2)
插入排序中,没有使用额外的空间,所以是原地排序
插入排序中,只有移动操做会改变元素位置,而断定条件是小于等于arr[i]
,符合状况1,a[i]会被设置到a[j+1],从而保证相对顺序的稳定.
将数组分为左侧的已排序
部分和右侧的未排序
部分
选择排序中选择表示对未排序部分的一次遍历,初始时已排序部分为空,未排序部分为arr[0~n-1]
,
一次选择后,已排序部分为arr[0~0]
,未排序部分为arr[1~n-1]
,x(1≤x<n)
次选择后,已排序部分为arr[0~x-1]
,未排序部分为arr(x~n-1]
第x(1≤x<n)
次选择时,找出未排序数组中的最小值,放入已排序数组(与arr[x-1]
交换),核心代码以下
public void sort(Comparable[] arr) { if (arr.length <= 1) { return; } for (int i = 1; i < arr.length ; i++){ int minPos=i-1; Comparable minValue=arr[i-1]; for(int j=i;j<arr.length;j++){ if (minValue.compareTo(arr[j])>0){ minPos=j; minValue=arr[j]; } } swap(arr,minPos,i-1); } }
以minValue被替换的次数做为计量单位.
最坏状况下为 (n-1) + (n-2) + ... + 0 =n(n-1)/2 ~= O(n^2)
最好状况下为 1+1+1...+1 =n ~= O(n)
平均状况下,仍然使用逆序数, 能够发现: 逆序数等于替换的次数,所以时间复杂度为(0 + n(n-1)/2)/2 ~= O(n^2)
选择排序中,minValue使用了一个单位的额外空间,所以是原地排序
选择排序中,没有约束能够确保稳定, 好比 4, 3, 2, 4, 1
排序事后,两个4
的相对位置会发生变化
三者的相同之处
已排序和未排序
部分,在排序过程当中逐步将未排序部分转化为已排序部分.三者的不一样之处
不稳定
的选择>插入>冒泡
, 可是选择排序不稳定,仅此一点不少场景下就不适用,实用性大大下降. 所以插入排序时三者中较好的选择.冒泡,插入,选择是三种基本的排序算法,研究他们对学习更高效的排序算法有帮助,相关源码已经上传到这里