今天来点轻松的!其实看了很多很难的算法,最后很容易把最基本的东西给忽略了,所以新年的第三个小算法专题我们来复习一下常见的排序算法,主要会涉及插入排序、冒泡排序、快速排序、归并排序等等,写一份标准的代码。
本篇文章介绍四个简单排序算法—选择、插入、希尔、冒泡排序。下一篇文章将介绍更多高级排序算法。
文章正在陆续更新中,感谢你的关注~
初级排序—选择排序
概述
每次都遍历一下数组,找到一个最小的数拿出来,与第一个位置的元素进行交换,然后继续在剩余的元素中找第二小的数,与第二个位置的元素进行交换…像这样每次都选择剩余未排序元素中最小者的排序方式就是选择排序。
实现起来比较简单,每次进行遍历,找到最小的元素所在的下标,与当前循环进行的次数对应的下标做值的交换即可。
参考代码
int []a=new int[]{2,1,7,8,10,9};// example for sort
for(int i=0;i<a.length;i++){
int min=i;// 记录本轮最小值的下标
for(int j=i+1;j<a.length;j++){// 更新最小值下标
if(a[j]<=a[i]){
min=j;
}
}
int tmp=a[i];// 把最小值和当前循环对应下标元素做交换
a[i]=a[min];
a[min]=tmp;
}
可以有一个小改进,比如当我们在第二轮交换前,1,2,7,8,10,9
中,我们发现第二小的数2
已经排在2
这个位置上了,因此就不用交换了,我们可以在交换出加入条件if(min!=i)
,满足这个条件才交换。
初级排序—插入排序
概述
插入排序十分适合部分有序的数组排序,它通过构建有序序列,对于未排序数据,在已排序序列中进行遍历,找到合适位置并插入。具体实现可以是:把整个数组分成两个部分:有序部分和无序部分,有序部分初始时有一个值(数组的第一个元素),其他元素都是无序部分,无序元素最后要插入到有序部分中。
循环中将从无序部分(第二个元素)开始,每次取出一个元素i
,与当前有序部分中的元素进行挨个比较,有序部分的元素指针j
从下标0
开始,找到第一个大于i
的位置停下,这个位置就是i
要插入的位置。如果有序部分中的元素都比i
小则无需插入。如上面图中的例子,7与有序部分中的元素2进行比较,比2大,所以不用插入,直接排在有序部分的末尾即可。
继续以图为例,现在要插入1
了,有序部分的第一个元素2
就比1
大,是第一个大于i
的位置,因此1将被插入到这个位置。确定了插入位置就要开始移动元素为插入腾空间了,需要将j
至i-1
下标对应的元素都右移一个位置,这样才能将i
放入j
位置。
本例中需要把2、7
右移,这样才能腾出j=0
这个位置,但是这样做i
所在的位置的值1
会被覆盖,所以我们在移动之前使用遍历tmp
把这个值保存起来。移动后再把这个值写到j=0
位置上,结果如下图所示。
然后在下一轮循环中,我们的i
会增加1
,即有序部分增加1
,无序部分减少1
,如下图所示。
按同样的方式,把6
也与有序部分中的元素进行挨个比较,找到第一个比i
大的元素的位置,插入。结果如下图。
然后把8
和有序部分的元素依次比较,发现都比8
小,因此8
不需要额外移动。最终i走到数组末尾,数组排序完成。这就是一种插入排序的过程。
参考代码
int []a={2,7,1,6,8,3,4};// test data
int tmp=0;// 记录待插入元素 插入时用
for(int i=1;i<a.length;i++){
for(int j=0;j<i;j++){
if(a[i]<a[j]){// 找到第一个比a[i]大的位置停下
tmp=a[i];// 备份待插入元素
for(int k=i-1;k>=j;k--)// 右移
a[k+1]=a[k];
a[j]=tmp;// 插入
}
}
}
拓展:也可以在遍历有序序列时从后往前遍历,即从i-1
开始到0
,找到第一个比tmp
小或者相等的元素就停下,这个位置j就是要插入的位置,然后还是一样进行右移。在下面的希尔排序的代码中我们使用的就是这种倒序的遍历有序序列的方式。
初级排序—希尔排序
先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”
的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。
因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
概述
前面介绍的排序算法都是和相邻的元素进行比较交换,这样适合那种基本有序的数组排序,而对于数组基本无序的情况,相邻元素的交换过于缓慢,需要经过很多次比较才能交换到两个相隔较远的元素,希尔排序正是改进了这一点。它通过交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
希尔排序中有一个增量(步长)的概念,因为希尔排序是通过把数组分成若干个不相邻的子序列分别进行排序的,有一个增量h
。h
刚开始很大,比如为5
,代表每隔5
个元素为一组子序列,如下面的数(第一行),在以5
为增量后形成了5
个子序列(5
个颜色),这五个子序列将分别进行插入排序,即组内(相同颜色)的数字可以进行交换至有序。
我们把相同颜色的子序列内部进行插入排序,然后就得到了一个局部有序的数组,这是我们令间隔h=5
的结果。下一步我们令h=2
,则代表每隔两个元素就为同一个自序列,最终分成两个子序列(蓝色和红色),然后对两种颜色的数字分别插入排序,得到下图最下一行的结果。
这时整个数组已经基本有序了,我们使用一次插入排序,也就是令间隔h=1
,就能很快地完成这个数组的排序了。最终结果如下图:
这个过程就是希尔排序了,它改进了插入排序只能交换相邻元素、对完全无序的数组不友好的缺点,通过设置**间隔h
**来让间隔较远的元素直接“见面”,逐步缩小h
至1
,使原来完全无序的数组变成局部有序的数组,最终使用一次全局的插入排序就能完成排序。
下面说一下间隔h
的选取:有多种h
序列可以选择,更好的间隔序列能带来时间复杂度上的优化。一般我们使用1,4,13,40,121,364······
即**h=3h+1
(h
从1
开始取)就能有很好的性能了,最坏的情况下需要比较N3/2次,不会达到N2**。当然也可以使用其他序列,本文就不展开了。
上面图片中的例子中,有两个相等的元素5
,我将其中一个(位于前面的)5
做了下划线的标记,可最终排序的结果里它跑到了另一个5
的后面,这说明希尔排序是不稳定的排序。
参考代码
int []a={9,1,2,5,7,4,8,3,5};// test data
int h=1;
int len=a.length;
while(h<len/3){// 确定h的最大值,在3h+1序列中找
h=h*3+1;
}
int i,j;
while (h>=1){
for(i=h;i<len;i++){// 子序列 局部插入排序
int tmp=a[i];
// 倒序遍历有序序列,指针j,当遍历到第一个元素小于等于tmp了就停下,插入到j这个位置。
for(j=i-h;j>=0 && a[j]>tmp ;j-=h)//
a[j+h]=a[j];
a[j+h]=tmp;
}
h=h/3;// 每轮结束都缩小间隔h,按h序列缩小
}
初级排序—冒泡排序
概述
是一种我们经常提及的简单排序算法。它属于一种交换排序,即通过对数组中的数据不断进行交换来完成排序。两个数比较大小,通过交换使较大的数“下沉”到数组尾部,较小的数“冒起来”:到数组头部。具体实现步骤如下:
- 用一个指针
i
指向第一个元素,然后比较和它相邻的元素i+1
。如果前一个比后一个大(逆序)就交换他们两个。 - 指针
i
加一,对这个元素和它相邻的元素重复第一步。直到i
走到数组倒数第二个元素(最后一对)。完成这一轮交换后,最大的元素就下沉到数组末尾了。 - 指针i继续加一,重复上述步骤。每次都会成功的将一个较大的数安放到末尾(下图中橙色的元素)。
这个过程通过两层循环控制:
- 第一个循环(外循环),负责把需要冒泡的那个数字排除在外;
- 第二个循环(内循环),负责两两比较交换。
下面的动图形象展示了排序过程。
参考代码
public void bubbleSort(int []r){
for(int i=0;i<r.length-1;i++){// len-1个数冒泡,需要循环len-1次
for(int j=0;j<r.length-1;j++){// j与j+1两两比较,需要循环len-1次
if(r[j]>r[j+1]){// 逆序则进行交换
int temp=r[j];
r[j]=r[j+1];
r[j+1]=temp;
}
}
}
}
改进
我们都知道冒泡算法是有改进空间的。以上面的代码为例,如果一个数组本身就是有序的,或者经过几轮循环已经有序了,上面的代码还是会继续进行比较,直到循环结束。显然我们在发现数组已经有序的时候停下,更节省时间。具体做法是增加一个flag
变量,初始为false
,当一次内循环中没有发生任何两两交换的时候,说明数组是有序的,无需任何交换。我们在发生交换时将flag
置为true
,然后循环外判断如果flag
为false
,说明没有发生过交换,直接break
退出。如下面的代码:
// 1.加入判断,当一轮交换中没有发现任何逆序数字,说明数组已经有序,退出循环。
public void bubbleSort1(int []r){
for(int i=0;i<r.length-1;i++){
boolean flag=false;
for(int j=0;j<r.length-1;j++){
if(r[j]>r[j+1]){// 逆序则交换
flag=true;
int temp=r[j];
r[j]=r[j+1];
r[j+1]=temp;
}
}
if(!flag){
break;
}
}
}
循环的时候也有改进的空间。我们外循环每进行一次,都会在数组末尾固定好一个有序的数字,如上图中的黄色数字,因此我们在后续的遍历时就无需遍历到数组末尾,而是遍历到这些黄色数字之前就可以了,对内部循环条件做如下修改:
// 2.每次交换都会让最大的数字跑到后面,所以每次循环都可以减少一次交换的判断。
public void bubbleSort2(int []r){
for(int i=0;i<r.length-1;i++){
boolean flag=false;
for(int j=0;j<r.length-1-i;j++){
if(r[j]>r[j+1]){// 逆序则交换
flag=true;
int temp=r[j];
r[j]=r[j+1];
r[j+1]=temp;
}
}
if(!flag){
break;
}
}
}
参考
更多文章:
你的喜欢是我创作的动力,喜欢请关注,感谢每一个喜欢~
如有问题欢迎进行交流~
水平所限,如有错误请海涵,欢迎指正~