整理转载自:
https://blog.csdn.net/c99463904/article/details/77946903
https://www.cnblogs.com/ysocean/p/7896269.html
http://www.cnblogs.com/ysocean/p/8005694.html
目录
一、冒泡排序
冒泡排序(Bubble Sort)是一种交换排序,基本思想是两两比较相邻的元素,如果他们的顺序错误就把他们交换过来,直到排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端
冒泡排序算法的过程如下:
(1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
(3)针对所有的元素重复以上的步骤,除了最后一个。
(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
代码实现:
简单实现
void sort(int[] a) {
for(int i=0;i<a.length;i++) {
for(int j=0;j<a.length-i-1;j++) {
if(a[j]>a[j+1]) {
int temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
}
}
}
这应该是最简单的排序代码了,不过这个代码效率是非常低下的,所以我们需要进行改进。
冒泡排序优化
试想一下,有这么一个数组{2,1,3,4,5,6,7,8},也就是说,除了第一和第二个元素需要交换,其他的已经是正常的顺序了 ,如果我们用上面的算法,毫无疑问它会将每个循环再执行一次,这就耗费了大量的时间,所以我们可以设置一个标志位,当没有任何数据交换时说明已经有序,不需要进行后面的循环操作。
void bestsort(int[] a) {
boolean flag=true;
for(int i=0;i<a.length&&flag;i++) {
flag=false;
for(int j=0;j<a.length-i-1;j++) {
if(a[j]>a[j+1]) {
int temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
flag=true;
}
}
}
}
冒泡排序时间复杂度分析
当最好的情况,也就是排序的数组本身是有序的,那么我们需要比较一轮,也就是n-1次,没有数据交换时间复杂度为O(n),最坏的情况,也就是排序的数组为逆序时,此时需要比较n-1+n-2+……2+1次,也就是n(n-1)/2,并且还需要移动,此时时间复杂度为O(n^2).这时候我们就知道了冒泡排序是一种效率多么低下的算法,尽管有很多人对它进行各种各样的优化,但是排序的特性在这里,性能依然大大相差于其他算法。
二、选择排序
选择排序是每一次从待排序的数据元素中选出最小的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
分为三步:
①、从待排序序列中,找到关键字最小的元素
②、如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换
③、从余下的 N - 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束
选择排序的思想是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
简单选择排序
public static void selectsort(int[] arr) {
for (int i=0;i<arr.length;i++) {
int minindex=i;
for(int j=i+1;j<arr.length;j++) {
if(arr[minindex]>arr[j]) {
minindex=j;
}
}
if(i!=minindex) {
int temp=arr[i];
arr[i]=arr[minindex];
arr[minindex]=temp;
}
}
}
简单选择排序时间复杂度分析
从算法上来看,选择排序交换移动次数相当少,分析时间复杂度,无论是最好还是最坏情况,比较次数都是一样的多,为n(n-1)/2次,而交换次数最好情况为0,最坏情况逆序为n-1次,因此总的来看,选择排序的时间复杂度为O(n^2),虽然说和冒泡排序同为O(n^2),但是选择排序的性能还是要优于冒泡排序。
三、插入排序
直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止
代码:
public static int[] sort(int[] array){
int j;
//从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
for(int i = 1 ; i < array.length ; i++){
int tmp = array[i];//记录要插入的数据
j = i;
while(j > 0 && tmp < array[j-1]){//从已经排序的序列最右边的开始比较,找到比其小的数
array[j] = array[j-1];//向后挪动
j--;
}
array[j] = tmp;//存在比其小的数,插入
}
return array;
}
插入排序时间复杂度分析
当最好的情况,也就是要排序的表本身就有序时,那么只需要比较n-1次,此时时间复杂度为O(n),当最坏情况发生时,即待排序的数组为逆序,此时需要比较1+2+3+。。。(n-1)次,时间复杂度为O(n^2),但是同样为O(n^2),插入排序的性能要比冒泡和选择排序性能要好。
上面讲的三种排序,冒泡、选择、插入用大 O 表示法都需要 O(N2) 时间级别。一般不会选择冒泡排序,虽然冒泡排序书写是最简单的,但是平均性能是没有选择排序和插入排序好的。
选择排序把交换次数降低到最低,但是比较次数还是挺大的。当数据量小,并且交换数据相对于比较数据更加耗时的情况下,可以应用选择排序。
在大多数情况下,假设数据量比较小或基本有序时,插入排序是三种算法中最好的选择。
后面我们会讲解高级排序,大O表示法的时间级别将比O(N2)小。
四、希尔排序
希尔排序是基于直接插入排序的,它在直接插入排序中增加了一个新特性,大大的提高了插入排序的执行效率。
分析一下上面的直接插入排序,首先我们将需要插入的数放在一个临时变量中,这也是一个标记符,标记符左边的数是已经排好序的,标记符右边的数是需要排序的。接着将标记的数和左边排好序的数进行比较,假如比目标数大则将左边排好序的数向右边移动一位,直到找到比其小的位置进行插入。
这里就存在一个效率问题了,如果一个很小的数在很靠近右边的位置,比如上图右边待排序的数据 1 ,那么想让这个很小的数 1 插入到左边排好序的位置,那么左边排好序的数据项都必须向右移动一位,这个步骤就是将近执行了N次复制,虽然不是每个数据项都必须移动N个位置,但是每个数据项平均移动了N/2次,总共就是N2/2,因此插入排序的效率是O(N2)。
那么如果以某种方式不必一个一个移动中间所有的数据项,就能把较小的数据项移动到左边,那么这个算法的执行效率会有很大的改进。
希尔排序应运而生了,希尔排序通过加大插入排序中元素的间隔,并在这些有间隔的元素中进行插入排序,从而使数据项能够大跨度的移动。当这些数据项排过一趟序后,希尔排序算法减小数据项的间隔再进行排序,依次进行下去,最后间隔为1时,就是我们上面说的简单的直接插入排序。
下图显示了增量为4时对包含10个数组元素进行排序的第一个步骤,首先对下标为 0,4,8 的元素进行排序,完成排序之后,算法右移一步,对 1,5,9 号元素进行排序,依次类推,直到所有的元素完成一趟排序,也就是说间隔为4的元素都已经排列有序。
当我们完成4-增量排序之后,在进行普通的插入排序,即1-增量排序,会比前面直接执行简单插入排序要快很多。
对于10个元素,我们选取4的间隔,那么100个数据,1000个数据,甚至更多的数据,我们应该怎么选取间隔呢?
希尔的原稿中,他建议间隔选为N/2,也就是每一趟都将排序分为两半,因此对于N=100的数组,逐渐减小的间隔序列为:50,25,12,6,3,1。这个方法的好处是不需要在开始排序前为找到初始序列的间隔而计算序列,只需要用2整除N。但是这已经被证明并不是最好的序列。
间隔序列中的数字互质是很重要的指标,也就是说,除了1,他们没有公约数。这个约束条件使得每一趟排序更有可能保持前一趟排序已经排好的结果,而希尔最初以N/2的间隔的低效性就是没有遵守这个准则。
所以一种希尔的变形方法是用2.2来整除每一个间隔,对于n=100的数组,会产生序列45,20,9,4,1。这比用2会整除会显著的改善排序效果。
还有一种很常用的间隔序列:knuth 间隔序列 3h+1
但是无论是什么间隔序列,最后必须满足一个条件,就是逐渐减小的间隔最后一定要等于1,因此最后一趟排序一定是简单的插入排序。
下面我们通过knuth间隔序列来实现希尔排序:
knuth间隔序列的希尔排序算法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
测试结果:
1 2 3 4 |
|
间隔为2h的希尔排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
测试结果:
通过我们的分析,大家应该明白,希尔排序的关键就是增量,将相隔某个增量的数据组成一个子序列,形成跳跃式的移动,使得移动次数变少,效率变高。这里的增量的选取非常关键,可究竟选取什么增量才是最好?目前还是一个数学难题,到现在位置还没有找到一种最好的增量,不过大量的研究表明,当增量为 dlta[k]=2^(t-k+1)-1(0<=k<=t<=[log2(n+1)])时,可以有很不错的效率,时间复杂度为O(n^(3/2)),效率相比前面几种有了大大的提高,不过因为是跳跃式移动,希尔排序并不是一种稳定的排序算法。
五、归并排序
归并算法的中心是归并两个已经有序的数组。归并两个有序数组A和B,就生成了第三个有序数组C。数组C包含数组A和B的所有数据项。
归并排序的思想是把一个数组分成两半,排序每一半,然后用上面的sort()方法将数组的两半归并成为一个有序的数组。如何来为每一部分排序呢?这里我们利用递归的思想:
把每一半都分为四分之一,对每个四分之一进行排序,然后把它们归并成一个有序的一半。类似的,如何给每个四分之一数组排序呢?把每个四分之一分成八分之一,对每个八分之一进行排序,以此类推,反复的分割数组,直到得到的子数组是一个数据项,那这就是这个递归算法的边界值,也就是假定一个数据项的元素是有序的。
public static void main(String[] args) {
int[] a= {6,8,2,3,9,1,5,4,7};
System.out.println(Arrays.toString(mergeSort(a,0,a.length-1)));
}
public static int[] mergeSort(int[] c,int start,int last){
if(last > start){
//也可以是(start+last)/2,这样写是为了防止数组长度很大造成两者相加超过int范围,导致溢出
int mid = start + (last - start)/2;
mergeSort(c,start,mid);//左边数组排序
System.out.println(start+" "+last);
mergeSort(c,mid+1,last);//右边数组排序
merge(c,start,mid,last);//合并左右数组
}
return c;
}
public static void merge(int[] c,int start,int mid,int last){
int[] temp = new int[last-start+1];//定义临时数组
int i = start;//定义左边数组的下标
int j = mid + 1;//定义右边数组的下标
int k = 0;
while(i <= mid && j <= last){
if(c[i] < c[j]){
temp[k++] = c[i++];
}else{
temp[k++] = c[j++];
}
}
//把左边剩余数组元素移入新数组中
while(i <= mid){
temp[k++] = c[i++];
}
//把右边剩余数组元素移入到新数组中
while(j <= last){
temp[k++] = c[j++];
}
//把新数组中的数覆盖到c数组中
for(int k2 = 0 ; k2 < temp.length ; k2++){
c[k2+start] = temp[k2];
}
}
归并排序时间复杂度分析
归并排序的时间复杂度为O(nlogn),因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。
六、快速排序
快速排序是对冒泡排序的一种改进,由C. A. R. Hoare在1962年提出的一种划分交换排序,采用的是分治策略(一般与递归结合使用),以减少排序过程中的比较次数。
①、快速排序的基本思路
一、先通过第一趟排序,将数组原地划分为两部分,其中一部分的所有数据都小于另一部分的所有数据。原数组被划分为2份
二、通过递归的处理, 再对原数组分割的两部分分别划分为两部分,同样是使得其中一部分的所有数据都小于另一部分的所有数据。 这个时候原数组被划分为了4份
三、就1,2被划分后的最小单元子数组来看,它们仍然是无序的,但是! 它们所组成的原数组却逐渐向有序的方向前进。
四、这样不断划分到最后,数组就被划分为多个由一个元素或多个相同元素组成的单元,这样数组就有序了。
具体实例:
对于上图的数组[3,1,4,1,5,9,2,6,5,3],通过第一趟排序将数组分成了[2,1,1]或[4,5,9,3,6,5,3]两个子数组,且对于任意元素,左边子数组总是小于右边子数组。通过不断的递归处理,最终得到有序数组[1 1 2 3 3 4 5 5 6]
②、快速排序的算法实现
假设被排序的无序区间为[A[i],......,A[j]]
一、基准元素选取:选择其中的一个记录的关键字 v 作为基准元素(控制关键字);怎么选取关键字?
二、划分:通过基准元素 v 把无序区间 A[I]......A[j] 划分为左右两部分,使得左边的各记录的关键字都小于 v;右边的各记录的关键字都大于等于 v;(如何划分?)
三、递归求解:重复上面的一、二步骤,分别对左边和右边两部分递归进行快速排序。
四、组合:左、右两部分均有序,那么整个序列都有序。
上面的第 三、四步不用多说,主要是第一步怎么选取关键字,从而实现第二步的划分?
划分的过程涉及到三个关键字:“基准元素”、“左游标”、“右游标”
基准元素:它是将数组划分为两个子数组的过程中,用于界定大小的值,以它为判断标准,将小于它的数组元素“划分”到一个“小数值的数组”中,而将大于它的数组元素“划分”到一个“大数值的数组”中,这样,我们就将数组分割为两个子数组,而其中一个子数组的元素恒小于另一个子数组里的元素。
左游标:它一开始指向待分割数组最左侧的数组元素,在排序的过程中,它将向右移动。
右游标:它一开始指向待分割数组最右侧的数组元素,在排序的过程中,它将向左移动。
注意:上面描述的基准元素/右游标/左游标都是针对单趟排序过程的, 也就是说,在整体排序过程的多趟排序中,各趟排序取得的基准元素/右游标/左游标一般都是不同的。
对于基准元素的选取,原则上是任意的。但是一般我们选取数组中第一个元素为基准元素(假设数组是随机分布的)
③、快速排序图示
上面表示的是一个无序数组,选取第一个元素 6 作为基准元素。左游标是 i 哨兵,右游标是 j 哨兵。然后左游标向左移动,右游标向右移动,它们遵循的规则如下:
一、左游标向右扫描, 跨过所有小于基准元素的数组元素, 直到遇到一个大于或等于基准元素的数组元素, 在那个位置停下。
二、右游标向左扫描, 跨过所有大于基准元素的数组元素, 直到遇到一个小于或等于基准元素的数组元素,在那个位置停下。
第一步:哨兵 j 先开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵 j 先开始出动,哨兵 j 一步一步的向左挪动,直到找到一个小于 6 的元素停下来。接下来,哨兵 i 再一步一步的向右挪动,直到找到一个大于 6 的元素停下来。最后哨兵 i 停在了数字 7 面前,哨兵 j 停在了数字 5 面前。
到此,第一次交换结束,接着哨兵 j 继续向左移动,它发现 4 比基准数 6 要小,那么在数字4面前停下来。哨兵 i 也接着向右移动,然后在数字 9 面前停下来,然后哨兵 i 和 哨兵 j 再次进行交换。
第二次交换结束,哨兵 j 继续向左移动,然后在数字 3 面前停下来;哨兵 i 继续向右移动,但是它发现和哨兵 j 相遇了。那么此时说明探测结束,将数字 3 和基准数字 6 进行交换,如下:
到此,第一次探测真正结束,此时已基准点 6 为分界线,6 左边的数组元素都小于等于6,6右边的数组元素都大于等于6。
左边序列为【3,1,2,5,4】,右边序列为【9,7,10,8】。接着对于左边序列而言,以数字 3 为基准元素,重复上面的探测操作,探测完毕之后的序列为【2,1,3,5,4】;对于右边序列而言,以数字 9 位基准元素,也重复上面的探测操作。然后一步一步的划分,最后排序完全结束。
通过这一步一步的分解,我们发现快速排序的每一轮操作就是将基准数字归位,知道所有的数都归位完成,排序就结束了。
④、快速排序完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
⑤、优化分析
假设我们是对一个逆序数组进行排序,选取第一个元素作为基准点,即最大的元素是基准点,那么第一次循环,左游标要执行到最右边,而右游标执行一次,然后两者进行交换。这也会划分成很多的子数组。
那么怎么解决呢?理想状态下,应该选择被排序数组的中值数据作为基准,也就是说一半的数大于基准数,一般的数小于基准数,这样会使得数组被划分为两个大小相等的子数组,对快速排序来说,拥有两个大小相等的子数组是最优的情况。
三项取中划分
为了找到一个数组中的中值数据,一般是取数组中第一个、中间的、最后一个,选择这三个数中位于中间的数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
处理小划分
如果使用三数据取中划分方法,则必须遵循快速排序算法不能执行三个或者少于三个的数据,如果大量的子数组都小于3个,那么使用快速排序是比较耗时的。联想到前面我们讲过简单的排序(冒泡、选择、插入)。
当数组长度小于M的时候(high-low <= M), 不进行快排,而进行插入排序。转换参数M的最佳值和系统是相关的,一般来说, 5到15间的任意值在多数情况下都能令人满意。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
七、各排序算法复杂度
算法分类:
复杂度: