排序简介
排序就是将一组对象通过某种逻辑顺序重新排序的过程。排序在生活是随处可见的,那么如何在某种场景下选择出最适合且高效的排序需要掌握各种排序的优劣势。尽管现在第三方类库、工具都集成了排序,不需要程序员自己实现,但是排序的思想所提供的一种程序化的算法思路是无论什么时代都应该去学习、去实践的。
大O表示法
在介绍大O表示法时应该先明白两个概念:时间复杂度和空间复杂度。
时间复杂度:指一个算法在执行的过程中所需要的计算工作量(执行时间)
空间复杂度:指一个算法在执行的过程中所需要的内存空间。
大O表示法表示程序的执行时间或占用空间随数据规模的增长趋势,它一般是与被操作数有关,它的展示形式如n²,log(n)。同时在描述一个算法的时间复杂度时,大家会想明明选择排序的时间复杂度为n²/常数,为什么最后被描述成了n²呢?其实这正是大O表示法的规定,常数是忽略不计的。因为在被操作数很大情况下常数计算与否确实不会对算法的时间复杂度造成偏差。而常见的复杂度如n²和log(n)也不会因为常数的忽略不计而造成大小反转的情况。
在下文中我在使用大O表示法时会把常数带上,这会帮助大家更易于通过时间复杂度来反推算法的执行过程。
实现
排序基类
/**
* @Description:排序练习基类
* @Auther: guopengfei
* @Date: 2019/7/8
*/
public abstract class BaseSort implements Sortable{
//排序过程中的比较次数及交换次数
protected int comparisonTimes = 0, exchangeTimes = 0;
/**
* 比较两数大小
*/
protected boolean less(Comparable c1, Comparable c2) {
return c1.compareTo(c2) < 0;
}
/**
* 交换数组两下标的值
*/
protected void swap(Comparable[] a, int i, int j) {
Comparable temp = a[i];
a[i] = a[j];
a[j] = temp;
}
/**
* 打印排序好的数组
*/
protected void show(Comparable[] arr) {
System.out.println(Arrays.toString(arr));
System.out.println("comparisonTimes=" + comparisonTimes);
System.out.println("exchangeTimes=" + exchangeTimes);
}
/**
* 判断数组是否有序
*/
protected boolean isSorted(Comparable[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
if (!less(arr[i], arr[i + 1])) {
return false;
}
}
return true;
}
/**
* 排序抽象方法
*/
protected abstract void sort(Comparable[] arr);
}
选择排序
选择排序是排序算法的入门算法。它的优点是易理解、好实现;缺点是面对被操作数庞大的情况效率极低。
排序过程:对于一个无序数组{4,9,5,3}。当前索引为0,首先找到数组中最小的值3,然后将这个最小值与索引0元素交换位置即4与3交换位置{3,4,9,5},因为这时数组索引0的元素已经是整个数组最小的了,所以它不需要再参加排序操作了。其次从索引1开始,在{4,9,5}中寻找最小值与4交换位置(如果这个元素为数组最小值时则它自己与自己交换),如此反复,直到索引到达数组最后一个元素则排序也完成了。
/**
* @Description:选择排序实现
* @Auther: guopengfei
* @Date: 2019/7/8
*/
public class SelectSort extends BaseSort implements Sortable{
@Override
public void sort(Comparable[] arr) {
for (int i = 0; i < arr.length; i++) {
int min = i;
for (int j = i; j < arr.length; j++) {
comparisonTimes++;
if (less(arr[j], arr[min])) {
min = j;
}
}
swap(arr, i, min);
exchangeTimes++;
}
show(arr);
}
}
选择排序示例图
i | 5 | 9 | 1 | 3 | 7 | 6 | 2 | 8 | 4 |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 9 | 5 | 3 | 7 | 6 | 2 | 8 | 4 |
1 | 1 | 2 | 5 | 3 | 7 | 6 | 9 | 8 | 4 |
2 | 1 | 2 | 3 | 5 | 7 | 6 | 9 | 8 | 4 |
3 | 1 | 2 | 3 | 4 | 7 | 6 | 9 | 8 | 5 |
4 | 1 | 2 | 3 | 4 | 5 | 6 | 9 | 8 | 7 |
5 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
6 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
7 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
8 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
红色元素是选择排序中需要交换的次数,对角线左下方的所有元素为已经有序的元素,对角线右上角的元素是需要比较的元素(未排序)。
效率
- 时间复杂度为n²/2,n*(n-1)*(n-2)… = n²/2 即对角线右上角的元素
- 比较次数为n²/2,n*(n-1)*(n-2)… = n²/2 即对角线右上角的元素都进行比较
- 交换次数为n,也就是对角线上的元素是交换的地方(在诸多排序算法中是很少的交换次数)
冒泡排序
排序过程:对于一个无序数组,从数组左侧开始,将第一个与第二个元素相比较,如果当前元素较大,则交换两者位置,再用第二个元素与第三个元素相比较,依次类推,这样遍历一次数组就会有一个最大值被交换到了数组的最右端。接着再从数组左侧开始比较、交换,每次交换到数组右端的最大值不用参加下次排序操作。
/**
* @Description:冒泡排序实现
* @Auther: guopengfei
* @Date: 2019/7/29
*/
public class BubbleSort extends BaseSort implements Sortable{
@Override
protected void sort(Comparable[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length-i-1; j++) {
comparisonTimes++;
if(!less(arr[j],arr[j+1])){
swap(arr,j,j+1);
exchangeTimes++;
}
}
}
show(arr);
}
}
冒泡排序示例图
i | 5 | 9 | 1 | 3 | 7 | 6 | 2 | 8 | 4 |
---|---|---|---|---|---|---|---|---|---|
0 | 5 | 1 | 3 | 7 | 6 | 2 | 8 | 4 | 9 |
1 | 1 | 3 | 5 | 6 | 2 | 7 | 4 | 8 | 9 |
2 | 1 | 3 | 5 | 2 | 6 | 4 | 7 | 8 | 9 |
3 | 1 | 3 | 2 | 5 | 4 | 6 | 7 | 8 | 9 |
4 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
5 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
6 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
7 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
8 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
效率
- 时间复杂度也是平方级别的n²/2,n*(n-1)*(n-2)… = n²/2 即红色对角线左上角的元素
- 比较次数为n²/2,n*(n-1)*(n-2)… = n²/2 即对角线左上角的元素都进行比较
- 交换次数为n²/4,因为在要交换元素中有可能这个元素是数组的开头,要走n、n-1才能到合适的位置,亦或者这个元素已经在合适的位置上,则一次也不用交换。所以平均交换次数为n²/2/2即n²/4
插入排序
排序过程:从数组左侧第一个数开始,依次跟它前面的数比较大小,若它小于前一个元素,则交换它与前一个元素的位置,接着再与上一个数比大小,直到放到合适的位置或走到数组的最左侧为止。当最后一个元素也放到了适合的位置上,排序也就完成了。
倒置:插入排序引入倒置的概念,倒置即数组中顺序颠倒的两个元素,即对于数组{4,3,2,1}中有两对倒置,分别是4-1,3-2
/**
* @Description:插入排序实现
* @Auther: guopengfei
* @Date: 2019/7/10
*/
public class InsertSort extends BaseSort implements Sortable {
@Override
protected void sort(Comparable[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = i; j>0 && less(arr[j],arr[j-1]); j--) {
swap(arr,j,j-1);
comparisonTimes++;
exchangeTimes++;
}
comparisonTimes++;
}
show(arr);
}
}
效率
- 时间复杂度为N²/4
- 交换次数与数组中倒置的数量相同。
- 每次交换都意味着一次比较,但每次比较都会比交换多一次(即当前元素不小于前一个元素且未到数组左端),则1到N-1之间都有可能有这一次额外的比较所以比较的次数大于等于倒置的数量,小于倒置的数量+N-1。
希尔排序
希尔排序是插入排序的一种变体,又称“缩小增量排序”。插入排序对基本有序的数组排序效率是很高的,固希尔排序先将数组变为基本有序的数组,再对其进行插入排序。当n很大时,希尔排序的效率要远远高于插入排序。
排序过程:先将数组变为h有序数组,h为递增序列(增量),什么是h有序数组?对于数组任意间隔h的元素都是有序的。h = 3 *h +1(1,4,13,40…)这个公式是总结出来当h取这些值可以有效的提高算法的性能,但其实目前并无法证明h这样取值是最好的。其思路与插入排序是一致的,只是增量不同,插入排序的增量为1,而希尔排序的增量是h。当h=1时,即为插入排序。
h有序数组示例
G | U | O | P | E | N | G | F | E | I | |
---|---|---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
h=4 | E | I | G | F | E | N | O | P | G | U |
h=1 | E | E | F | G | G | I | N | O | P | U |
其中h=4时,下标0,4,8三个组成一个h有序数组,它们的值EGU是有序的。同理下标1,5,9也为子h有序数组,它们的值也是有序的,以此类推。这就是h有序数组。
/**
* @Description:希尔排序实现
* @Auther: guopengfei
* @Date: 2019/7/11
*/
public class ShellSort extends BaseSort implements Sortable {
@Override
protected void sort(Comparable[] arr) {
int h = 1, n = arr.length;
while (h < n / 3) {
h = h * 3 + 1;
}
while (h >= 1) {
for (int i = h; i < n; i++) {
for (int j = i; j >= h && less(arr[j], arr[j - h]); j -= h) {
swap(arr, j, j - h);
comparisonTimes++;
exchangeTimes++;
}
comparisonTimes++;
}
h = h / 3;
}
show(arr);
}
}
效率
Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,今仍然是数学难题。——引用百度百科