算法总是学学忘忘,不过在遇到一些深入点的问题不理解其背后的原理就比较尴尬了。所以打算重新拾起这一部分,从基础的开始去了解一些其他的算法。从第一堂课开始就是学的排序,因此也将排序作为一个开始吧。
几个基本的排序算法
选择排序
选择排序的思想是从列表中不断的找出最小(大)的值,来依次排列到列表左侧,当移动到最右端的时候排序就完成了。对于一个长度为N的数组,选择排序大约需要进行N²/2次比较合N次交换。
在最坏的情况下,每一个数都不再其应该在的位置上,则每个数都会发生交换。对于比较次数,比如恰好是一个逆序数组,则每次都需要比较到末尾才能找到最大(小)值,因此比较次数为(N-1)+(N-2)+…+2+1 = N(N-1)/2 约为N²/2
简单实现:
1 | /** |
选择排序的特点:输入与其排序时间无关。内层每一次循环仅对当前一次有效,即使对于一个有序、随机或者元素都相同的数组它使用的时间可能一样长。另外它对元素的移动次数是最少的。每一次交换将改变两个元素的位置,改变的次数与元素个数呈线性关系,这是很多其他算法所不具有的。
插入排序
插入排序就是针对下标的左侧进行操作,每向前移动一位,就和前面的元素进行比较并把它放在合适的位置,和选择排序一样,插入排序的左侧始终保持有序。但在排序完成之前,将不会扫描到右侧的数据,当下标移动到右侧的时候,排序就完成了。对于长度为N的数组,平均一次排序需要N²/4次比较和N²/4次交换,最坏的情况下需要N²/2次比较和N²/2次交换,最好的情况下需要N-1次比较和0次交换。
最坏的情况下每一个元素都将会和前面所有元素发生比较和交换(如逆序)因此交换比较的次数都为(N-1)+(N-2)+…+2+1 = N(N-1)/2 ~=N²/2, 一般情况下,每个元素都有可能向后移动半个数组,花费大约为最坏的一半,最好的情况下,数组已经有序,因此只需要N-1次比较就能发现数组已经有序且不发生交换。
简单实现:
1 | /** |
插入排序的特点:插入排序对于那些部分有序(需要交换的元素较少)的数组效率最高,如果一个数组只有几个元素位置不正确,或每个元素离它正确位置都不远时,插入排序速度可能是最优秀的。插入算法还可以进一步改进。
改进的思想:插入排序时,每一次和前一位的比较在满足条件的情况下都发生了一次交换,但要直到循环条件结束才能确定元素的位置,因此可以在内层循环完成后再改变元素位置,在此之前,只需要将大的元素(从小到大排列的情况)往后移动即可。改进后的算法如下:
1 | public static void insertionSortNew(int[] a){ |
这样访问数组的次数将会减半。
希尔排序
希尔排序是基于插入排序演变而来,并且速度相较于插入排序要快很多。由于插入排序对于大规模的乱序数组来说速度比较慢,因为它只会交换相邻的两个元素,如果一个元素距离其正确的位置比较远,则它需要的交换次数将会非常多。希尔排序针对此进行了改进,不交换相邻的元素来对数组进行局部排序,最终将局部有序数组整理为有序数组。
希尔排序的思想在于让数组中任意间隔为h的数组都是有序的,每次按照不同的步长将整个数组划分为多个数组,在步长为1的时候,此时的操作变成了普通的插入排序,但此时的数组已经是基本有序的,因此插入排序在这时候比较高效。
希尔排序的简单实现:
1 | /** |
其中步长h并非完全需要按照这个大小来设定,目前并没有一种真正有力的说法来支持到底怎样的步长是最高效的。但由于这个步长,让希尔排序的速度远大于选择排序的插入排序,并且数组越大的时候优势越明显。