一、插入排序
插入排序主要包括直接插入排序和希尔排序两种。希尔排序应用较少,这里不作介绍。
直接插入排序是一种简单的排序方法,它把数组a中待排序的n个元素(n<=a.length)看成一个有序表和一个无序表,开始时有序表中只包含一个元素a[0] ,无序表中包含包含n-1个元素a[1]~a[n-1],排序过程中每次从无序表中取出第一个元素,把它插入到有序表中的适当位置,使之成为新的有序表,这样经过n-1次插入后,无序表就变为空表,有序表就包含了全部n个元素,至此排序完毕。
直接插入排序方法的关键之处是如何在第i次(1<=i<=n-1)把无序表a[i]~a[n-1]中的第一个元素a[i]插入到当前有序表a[0]~a[i-1]中的适当位置。有两种方法可以采用:第一种方法是首先从有序表表尾开始,依次向前使每一个元素a[j](i-1>=j>=0)的排序码同x(x对象中暂存a[i]的值)的排序码进行比较,直到x.stn>=a[j].stn或j=-1为止,此时下标为j+1元素位置就是x的插入位置,找到插入位置后,再使a[i-1]向前到a[j+1]的每个元素均后移一个位置,以便空出j+1下标位置保存x(即原a[i]元素);第二种方法是从有序表表尾开始把元素的比较和移动结合在一起进行,当x.stn<a[j].stn时,就把a[j]元素后移一个位置,接着同前一元素进行比较,直到此条件不成立或者j等于-1时,已经空出的下标为j+1的元素位置就是x的插入位置,把x直接赋给该元素即可。进行第i次插入后,有序表为a[0]~a[i],元素个数为i+1个,无序表为a[i+1]~a[n-1],元素个数为n-i-1个。
下面采用第二种向有序表中插入元素的方法,编写出直接插入排序的算法描述为:
//对数组a中的n个元素进行直接插入排序
public static void insertSort(Object [] a,int n)
{
if(n>=a.length)
{
System.out.println("n值有误,停止执行!");
System.exit(1);
}
for(int i=0;i<n;i++) //外循环共进行n-1次
{
Object x=a[i]; //暂存待插入元素a[i]的值到x中
int j;
for(j=i-1;j>=0;j--) //扫描有序表,寻找插入位置
{
if(((Comparable)x).compareTo(a[j])<0)//实际比较的是排序码
{
a[j+1]=a[j]; //元素值后移
}
else
{
break; //查询到插入位置时离开内循环
}
}
a[j+1]=x; //把原a[i]的值写入到下标为j+1的位置
}
}
直接插入排序的平均时间复杂度为O(n^2),但当待排序记录已经或接近按从小到大有序时,所用的计较和移动次数较少,其时间复杂度接近O(n),而当待排序记录为降序或接近降序排列时,所用的比较和移动次数较多,虽然仍是n的平方数量级,但系数值较大。所以直接插入排序更适合于原始数据基本有序(即升序)的情况。
在直接插入排序中,只使用一个临时工作单元s,暂存待插入的元素,所以其空间复杂度为O(1)。另外,直接插入排序是稳定的,因为具有同一排序码的后一元素必然插在具有同一排序码的前一元素的后面,即相对次序保持不变。
最后还需要指出,直接插入排序的方法不仅适用于数组,而且使用与单链表,不过在单链表上进行直接插入排序时,不是移动记录的位置,而是修改相应的指针(引用)。
二、选择排序
1、直接选择排序
直接选择排序同插入排序一样,也是一种简单的排序方法。它每次从当前待排序的区间中选择出具有最小排序码的元素,把该元素与该区间的第一个元素交换位置。第1次(即开始)待排序区间包含所有元素a[0]~a[n-1],经过选择和交换后,a[0]为具有最小排序码的元素;第2次待排序区间为a[1]~a[n-1],经过选择和交换后,a[1]为仅次与a[0]的具有最小排序码的元素,a[2]为仅次于a[0]和a[1]的具有最小排序码的元素;依次类推,经过n-1次选择和交换后,a[0]~a[n-1]就成为了有序表,整个排序过程结束。
算法思想: 双重循环,外层i控制当前序列最小(最大)值存放的数组元素位置,内层循环j控制从i+1到n序列中选择最小的元素所在位置k。
直接选择排序的算法描述为:
public static void selectSort(Object []a,int n)
{
//采用直接选择排序的方法对数组a中的n的元素排序
if(n>a.length)
{
System.out.println("n值有误,停止执行!");
System.exit(1);
}
for(int i=1;i<n-1;i++) //i表示次数,共进行n-1次选择和交换
{
int k=i-1; //用k保存最小排序码元素的下标,初值为i-1
for(int j=i+1;j<n-1;j++)
{
//从当前排序区间中顺序查找出具有最小排序码的元素a[k]
if(((Comparable)a[j]).compareTo(a[k])<0)
{
k=j;
}
if(k!=i-1) //把a[k]对调到当前排序区间的第1个位置,即i-1位置
{
Object x=a[i-1];
a[i-1]=a[k];
a[k]=x;
}
}
}
}
在直接选择排序中,共需要进行n-1次选择和交换,每次选择需要比较n-1次,其中1<=i<=n-1,每次交换最多需要移动三次记录,故:总的比较次数:C=1/2(n^2-n)
总移动次数(即最大值):M=3(n-1)
可见,直接选择排序的时间复杂度为O(n^2)
由于在直接选择排序中存在着前后元素之间的互换,因而可能会改变具有相同排序码元素的前后相对位置,所以此方法是不稳定的。
2、堆排序
堆排序是利用堆(这里采用大根堆)的特性进行排序的过程。堆排序包括构成初始堆和利用堆排序两个阶段。
构成初始堆就是把待排序的元素序列{ R0,R1,R2,R3......Rn-1}按照堆的定义调整为堆{ R',R1',R2',R3'......Rn-1' } ,其中对应的排序码Si'>=S2i-1'和Si'>=S2i+2',0<=i<2/n(向下取整)-1。为此需从对应的完全二叉树中编号最大的分支结点(即编号为n/2(向下取整)-1的结点)起,至整个树根结点(即编号为0的结点)止,依次对每个分支结点进行“筛”运算,以便形成以每个分支结点为根的堆,当最后对树根结点进行筛运算后,整个树就构成了一个堆。
下面讨论如何对每个分支结点Ri(0<=i<n/2(向下取整)-1)进行筛运算,以便构成以Ri根根的堆。因为当对Ri进行筛运算时,比它编号大的分支结点都已进行过筛运算,即已形成了以各个分支结点为根的堆,其中包括以Ri的左、右孩子结点R2i-1和R2i+2为根的堆,当然若孩子结点为叶子结点或空子树,则该叶子结点和空子树已经是一个堆,所以对Ri进行筛运算是在其左、右子树均为堆的基础上实现的。
为堆中下标为i的元素进行筛选运算的过程是:首先把Ri元素的排序码Si与两个孩子中排序码较大者Sj(j=2i+1或2i+2)进行比较,若Si>Sj,则以Si为根的子树已经调整为堆,筛运算完毕;否则Ri和Rj互换位置,互换后可能会破坏以Rj(此时的Rj的值为原来的Ri的值)为根的堆,接着再把Rj与它的两个孩子中排序码较大者进行比较,依次类推,直到父节点的排序码大于等于孩子结点中较大的排序码或者孩子结点为空时为止。这样,以Ri为根的子树就被调整为一个堆。在对Ri进行的筛选中,若它的排序码较小,则会被逐层下移,就像过筛子一样,小的被漏下去,大的被选上来,所以把构成堆的过程形象地称为筛运算。
图11-2给出了对待排序元素的排序码序列(45,36,18,53,72,30,48,93,15,36)构成初始堆的全过程。因结点数n=10,所以从编号为4的结点起至树根结点为止,依次对每个结点进行筛运算。图11-2(a)为按照原始排序码序列所构成的完全二叉树,图11-2(b)~(f)为依次对每个分支结点进行筛运算后所得到的结果,其中,图11-2为最后构成的初始堆。
假定待排序的n个元素存放于一维数组a中,则对a[i]进行筛运算的算法描述为:
public static void sift(Object []a,int n,int i)
{
//对a[n]数组中的a[i]元素进行筛运算
Object x=a[i]; //把待筛结点的值暂存于x中
int j=2*i+1; //a[i]是a[i]的左孩子
while(j<=n-1) //当a[i]的左孩子不为空时执行循环
{
if(j<n-1 && ((Comparable)a[j]).compareTo(a[j+1])<0)
{
j++;
}
if(((Comparable)x).compareTo(a[j])<0)
{
a[i]=a[j]; //将a[j]调到双亲位置上
i=j;
j=2*i+1; //修改i和j的值,以便继续向下筛
}
else
{
break; //查找到x的最终位置为i,退出循环
}
}
a[i]=x; //被筛结点的值放入最终位置
}
根据堆的定义和上面建堆的过程可以知道,编号为0的结点a[0](即堆顶)是堆中n个结点中排序码最大的结点。所以利用堆排序的过程比较简单,首先把a[0]与a[n-1]对换,使a[n-1]为排序码最大的结点,接着对a[0](即对调前的a[n-1])在前n-1个结点中进行筛运算,又得到a[0]为当前区间a[0]至a[n-2]内具有最大排序码的结点,再接着把a[0]同当前区间内的最后一个结点a[n-2]对换,使a[n-2]为次最大排序码结点,这样经过n-1次对换和筛选运算后,所有结点成为有序,排序结束。
假定在图11-2(f)已构成堆的基础上进行堆排序,则前3次对换和筛选的过程如图11-3所示,剩余6次对换和筛运算的过程请自行完成。
堆排序的算法描述为:
//堆排序算法
public static void heapSort(Object []a,int n)
{
//利用堆排序的方法对数组a中的n个元素进行排序
if(n>a.length)
{
System.out.println("n值有误,停止执行!");
System.exit(1);
}
for(int i=n/2-1;i>=0;i--) //建立初始堆
{
sift(a,n,i);
}
for(int i=1;i<n-1;i++) //进行n-1次循环,完成堆排序
{
Object x=a[0]; //将树根结点的值同当前区间内最后一个结点的值对换
a[0]=a[n-i];
a[n-i]=x;
sift(a,n-i,0); //筛a[0]结点,得到前n-1个结点的堆
}
}
在整个堆排序中,共需要进行约3n/2次筛运算,每次筛运算进行父子或兄弟结点的排序码的比较次数和记录的移动次数都不会超过完全二叉树的高度,所以每次筛运算的时间复杂度为O(log2n),故整个堆排序过程的时间复杂度为O(nlog2n)。另外,由于在堆排序中需要进行前后不同位置间元素的移动和交换,所以它是一种不稳定的排序方法。
直接选择排序和堆排序都属于选择排序,下面比较一下他们的差别。在直接选择排序中,共需进行n-1次选择,每次从待排序的区间(对应无序表)中选择一个最小值,而选择最小值的方法是通过顺序比较实现的,其时间复杂度为O(n),所以整个直接选择排序的时间复杂度为O(n^2)。在建立初始堆后的堆排序中,同样需要进行n-1次选择,每次从待排序区间(即当期筛运算的区间)中选择一个最大值,而选择最大值的方法是在各子树已是堆的基础上对根结点进行筛运算(即树型比较)实现的,其时间复杂度为O(log2n),所以整个堆排序的时间复杂度为O(nlog2n)。显然,堆排序比直接选择排序的速度要快得多。另外,直接选择排序和堆排序都是不稳定的,空间复杂度也都为O(1)。