本文整理来源 《轻松学算法——互联网算法面试宝典》/赵烨 编著java
插入排序分为两种,一种是直接插入排序,一种是二分法插入排序。这两种排序实际上都是插入排序,惟一的不一样就是插入的方式不同。面试
插入排序就是往数列里面插入数据元素。通常咱们认为插入排序就是往一个已经排好顺序的待排序的数列中插入一个数以后,数列依然有序。算法
二分插入排序应该也是用来分治法的思想去排序的。实际上二分就是使用二分查找来找到这个插入的位置,剩下的插入的思想其实和直接插入排序同样。shell
因此完成插入排序,就是须要找到这个待插入元素的位置。数组
插入排序实际上把待排序的数列分红了两部分,一部分已排好序,另外一部分待排序。性能
直接插入排序的整个执行过程:测试
首先外层是一个大循环,循环这个待排序的部分数列,内层是分别与前1个元素进行比较、移动,直到找到位置进行插入位置。优化
public class InsertSort { private int[] array; public InsertSort(int[] array) { this.array = array; } public void sort() { if (array == null) { throw new RuntimeException("array is null"); } int length = array.length; if (length > 0) { for (int i = 1; i < length; i++) { int temp = array[i]; int j = i; for (; j > 0 && array[j - 1] > temp; j--) { array[j] = array[j - 1]; } array[j] = temp; } } } public void print(){ for (int anArray : array) { System.out.println(anArray); } } }
测试代码this
public class InsertSortTest { @Test public void main(){ int[] arrays = {5,9,1,9,5,3,7,6,1}; InsertSort insertSort = new InsertSort(arrays); insertSort.sort(); insertSort.print(); } }
插入排序的操做很简单,并且咱们经过实例及原理能够知道,插入排序在数列近似有序时,效率会很是高,由于这样会减小比较和移动的次数。code
插入排序的时间复杂度是$O({n}^2)$
,咱们会发现这个实现是双重嵌套循环,外层执行n遍,内层在最坏的状况下执行n遍,并且除了比较操做还有移动操做。最好的状况是数列近似有序,这时一部份内层循环只须要比较及移动较少的次数就能够完成排序。若是数列自己已经排好序,那么插入排序也能够达到线性实现复杂度及$O(n^2)$
,因此咱们应该明确认识到,使用插入排序算法进行排序时,数列越近似有序,性能越高。
插入排序的空间复杂度时$O(1)$
,是常量级,因为在采用插入排序时,咱们只须要使用一个额外的空间来存储这个"拿出来"的元素,因此插入排序只须要额外的一个空间去作排序,这是常量级的空间消耗。
插入排序时稳定的,因为数组内部本身排序,把后面的部分按先后顺序一点点地比较、移动,能够保持相对顺序不变,因此插入排序是稳定的排序算法。
插入排序算法主要是比较和移动的两个操做,会致使时间复杂度很大。可是插入排序在序列自己有序时可以达到$O(n)$
的时间复杂度,也就是说实际上若是序列自己有必定的有序性,那么使用插入排序的效率会更高,若是序列自己很短,那么插入排序的效率会很高。
希尔排序也是一种插入排序算法,也叫作缩小增量排序,是直接插入排序的一种更高效的改进算法。
希尔排序在插入排序的基础上,主要经过两点来改进排序算法:一是在插入排序在对近似有序的数列进行排序时,排序的性能会比较好;二是插入排序的性能比较低效,及每次只能将数据移动一位。
希尔排序的基本思想是:把待排序的数列按照必定的增量分割成多个数列。可是这个子数列不是连续的,二是经过前面提到的增量,按照必定相隔的增量进行分割的,而后对各个子数列进行插入排序,接着增量逐渐减少,而后依然对每部分进行插入排序,在减少到1以后直接使用插入排序处理数列。
特别强调,这里选择增量的要求是每次都要减小,知道最后一次变为1为止。首选增量通常为$\frac{n}{2}$
,n为待排序的数列长度,而且每次增量都为上次的$\frac{1}{2}$
。
希尔排序实际上只是插入排序的改进,在算法实现上,咱们须要额外操做的只有对增量的处理及对数列的分块处理。
public class ShellSort { private int[] array; public ShellSort(int[] array) { this.array = array; } public void sort() { int temp; for (int k = array.length / 2; k > 0; k /= 2) { for (int i = k; i < array.length; i++) { for (int j = i; j >= k; j -= k) { if (array[j - k] > array[j]) { temp = array[j - k]; array[j - k] = array[j]; array[j] = temp; } } } } } public void print() { for (int anArray : array) { System.out.println(anArray); } } }
public class ShellSortTest { @Test public void main(){ int[] arrays = {5,9,1,9,5,3,7,6,1}; ShellSort shellSort= new ShellSort(arrays); shellSort.sort(); shellSort.print(); } }
其实希尔排序只使用了一种增量的方式去改进插入排序,从上述对该算法的描述及实例中,咱们可以清楚的知道实际上希尔排序在内部仍是使用插入排序进行处理的。可是这个增量确实有它 意义,无论数列有多长,刚开始时增量会很大,可是数列总体已经开始趋于有序了,因此插入排序的速度仍是会愈来愈快的。
在时间复杂度上,因为增量的序列不必定,因此时间复杂度也不肯定。这在数学上还没法给出确切的结果。咱们能够采用每次除以2的方式,可是研究,有如下几种推荐序列:
N/3+1
,N/3^2+1
,N/3^3+1
••••••(听说在序列数N<100000时最优)$2^{k}-1$
,$2^{(k-1)}-1$
,$2^{k-2}-1$
••••••(设k为总趟数)对于每次除以2的增量选择,希尔排序的最好状况固然是自己有序,每次区分都不用排序,时间复杂度是$O(n)$
;可是在最坏的状况下依然每次都须要移动,时间复杂度与直接插入排序在最坏状况下的时间复杂度同样$O(n^{2})$
。
可是通常认为希尔排序的平均时间复杂度时$O({n}^{1.3})$
。固然,希尔排序的时间复杂度与其增量序列有关,通常咱们知道希尔排序会比插入排序快一些,这就足够了。
在希尔排序的实现中仍然使用了插入排序,只是进行了分组,并无使用其余空间,因此希尔排序的空间复杂度一样是$O(1)$
,是常量级的。
在希尔排序中会进行分组、排序,因此一样的元素,其相对位置可能会发生变化,这是由于一样值的元素若不在一个组中,则有可能后面的元素会被移动到前面。因此希尔排序是不稳定的算法。
在使用希尔排序时,须要选择合适的增量序列做为排序辅助,而这也是一个比较复杂的抉择。因此希尔排序在实际使用中并不经常使用。
选择排序是一种很是简单的排序算法,就是在序列中依次选择最大(或者最小)的数,并将其放到待排序的数列的起始位置。
简单选择排序的原理很是简单,即在待排序的数列中寻找最大(或者最小)的一个数,与第1个元素进行交换,接着在剩余的待排序的数列中继续找最大(最小)的一个数,与第2个元素交换。依次类推,一直到待排序的数列中只有一个元素为止。
也就是说,简单选择排序可分为两部分,一部分是选择待排序的数列中最小的一个数,另外一部分是让这个数与待排序的数列部分的第1个数进行交换,直到待排序的数列只有一个元素,至此整个数列有序。
public class SelectSort { private int[] array; public SelectSort(int[] array) { this.array = array; } public void sort() { int length = array.length; for (int i = 0; i < length; i++) { int minIndex = i; for (int j = i + 1; j < array.length; j++) { if (array[j] < array[minIndex]) { minIndex = j; } } if (minIndex != i) { int temp = array[minIndex]; array[minIndex] = array[i]; array[i] = temp; } } } public void print() { for (int anArray : array) { System.out.println(anArray); } } }
测试代码
public class SelectSortTest { @Test public void main(){ int[] arrays = {5,9,1,9,5,3,7,6,1}; SelectSort selectSort = new SelectSort(arrays); selectSort.sort(); selectSort.print(); } }
因为在简单选择排序中,咱们通常在本来的待排序的数组上排序并交换,基本上使用的都是常量级的额外空间,因此其空间复杂度时$O(1)$
。
在最好的状况下,每次都要找的最大(或者最小)的元素就是待排序的数列的第1个元素,也就是说数列自己有序,这样咱们只须要一次遍历且不须要交换,便可实现一趟排序;而在最坏的状况下,每次在数列中要找的元素都不是第1个元素,每次都须要交换。比较的次数只与数列的长度有关,而在外部遍历整个数列,也与长度有关,因此这样的双重循环无论在什么状况下,时间复杂度都是$O(n^{2})$
,可是因为选择有序不须要一个一个地往前移动,而是直接交换,而比较所消耗的CPU要比交换所消耗的CPU小一些,因此选择排序的时间复杂度相对于冒泡排序会好一些。
经过选择排序的思想,咱们知道选择排序的一个重要步骤是在待排序的数列中寻找最大(或者最小)的一个元素,那么如何寻找这个元素就成为一个能够优化的点。
另外,咱们每次都要寻找两个值中的一个最大值,一个是最小值。这时若是须要将数列的最后一个元素进行交换。这样咱们一次就能寻找两个元素进行交换,把最大值与待排序的数列的最后一个元素进行交换。这样咱们一次就可以寻找两个元素,使外层循环的时间缩短了一半,性能也提升了不少。经过一次遍历就能够找到两个最值,而且没有其余性能损耗。
简单选择排序并不很常见,它只是选择排序的一个思想基础,选择排序还有其余方案能够实现。
类别 | 排序方法 | 时间复杂度(平均|最好|最坏) | 空间复杂度(辅助存储) | 稳定性 |
---|---|---|---|---|
插入排序 | 直接插入 | $O(n^2)$ |
$O(n)$ |
$O(n^2)$ |
插入排序 | 希尔排序 | $O(n^{1.3})$ |
$O(n)$ |
$O(n^2)$ |
选择排序 | 简单选择 | $O(n^2)$ |
$O(n^2)$ |
$O(n^2)$ |
选择排序 | 堆排序 | $O(nlogn)$ |
$O(nlogn)$ |
$O(nlogn)$ |
交换排序 | 冒泡排序 | $O(n^2)$ |
$O(n)$ |
$O(n^2)$ |
交换排序 | 快速排序 | $O(nlogn)$ |
$O(nlogn)$ |
$O(nlogn)$ |
通常状况下在选择排序算法时有限选择快速排序,虽然堆排序的空间复杂度更低,可是堆排序没有快速排序简单。