数据结构与算法(一) 常见的排序算法
排序就是将一组对象按照某种逻辑顺序重新排列的过程。此篇介绍的排序算法就是将 所有元素的主键按照某种方式排列, 排序后索引较大的主键大于等于索引较小的主键。
排序算法可以分为两类: 除了函数调用所需的栈和固定数目的实例变量之外 无需额外内存的 原地排序算法, 以及 需要额外内存空间来存储另一份数组副本的 其他排序算法。
在介绍此篇的排序算法前,首先介绍要用到的排序算法类模版。
public class Template{
private static boolean less(Comparable v,Comparable w){
//比较两个元素主键的大小
return v.comparable(w)<0;
}
private static void exch(Comapable[] array,int i; int j){
Comparable temp=array[i];
array[i]=array[j];
array[j]=temp;
}
public static boolean isSorted(Comparable[] array){
//判断数组元素是否有序
for(int i=1;i<array.length;i++){
if(less(array[i],array[i-1])) return false;
return true;
}
}
}
一、简单选择排序
简单选择排序是一种选择类排序算法,其思想是: 首先, 找到数组中最小的那个元素, 其次, 将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。
选择排序的内循环只是在比较当前元素与目前已知的最小元素(以及将当前索引加 1 和检查是否代码越界)。交换元素的代码写在内循环之外,每次交换都能排定一个元素,因此交换的总次数是 N。选择排序算法具体实现如下:
public class SelectSort{
public static void sort(Comparable[] array){
int len=array.length;
for(int i=0;i<len;i++){
//将array[i]和array[i+1]到array[len-1]中最小的元素交换
int min=i;
for(int j=i+1;j<N;j++){
//求出最小元素
if(less(array[j],array[min]))
min=j;
}
exch(array,i,min);
}
}
}
小结:
(1) 对于长度为 N 的数组, 选择排序需要大约 N2/2 次比较和 N 次交换。
(2) 选择排序的运行时间和输入无关
二、插入排序
插入排序的思想是: 将每一个元素插入到其他已经有序的元素中的适当位置,为了给要插入的元素腾出空间, 我们需要将其余所有元素在插入之前都向右移动一位。
与选择排序一样, 当前索引左边的所有元素都是有序的, 但它们的最终位置还不确定, 为了给更小的元素腾出空间, 它们可能会被移动。 但是当索引到达数组的右端时, 数组排序就完成了。插入排序算法具体实现如下:
public class insertSort{
public static void sort(Comparable[] array){
//将数组array按照升序排列
int len=array.length;
for(int i=0;i<len;i++){
//将array[i]插入到array[i-1]、array[i-2]、array[0]之间
for(int j=i;j>0&&less(array[j],array[j-1];j--))
exch(array,j,j-1);
}
}
}
小结:
(1) 对于随机排列的长度为 N 且主键不重复的数组, 平均情况下插入排序需要~ N2/4 次比较以及~ N2/4 次交换
(2) 和选择排序不同的是, 插入排序所需的时间取决于输入中元素的初始顺序。 例如, 对一个很大且其中的元素已经有序(或接近有序) 的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。
(3) 对于随机排序的无重复主键的数组, 插入排序和选择排序的运行时间是平方级别的,两者之比应该是一个较小的常数。
三、希尔排序
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。 希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的。 这样的数组被称为 h 有序数组。 换句话说, 一个 h 有序数组就是 h 个互相独立的有序数组编织在一起组成的一个数组。希尔排序算法具体实现如下:
public class ShellSort{
public static void sort(Comparable[] array){
int len=array.length;
int h=1;
while(h<len/3) h=3^h+1;
while(h>=1){
//将数组变为h有序
for(int i=h;i<len;i++){
//将array[i]插入到array[i-1*h]、array[i-2*h]....中去
for(int j=i;j>=h&&less(array[j],array[j-h]);j-=h){
exch(array,j,j-h);
}
h=h/3;
}
}
}
}
希尔排序更高效的原因是它权衡了子数组的规模和有序性。 排序之初, 各个子数组都很短, 排序之后子数组都是部分有序的, 这两种情况都很适合插入排序。 子数组部分有序的程度取决于递增序列的选择。
和选择排序以及插入排序形成对比的是, 希尔排序也可以用于大型数组。 它对任意排序(不一定是随机的) 的数组表现也很好。 实际上,希尔排序比插入排序和选择排序要快得多, 并且数组越大, 优势越大。
四、归并排序
归并排序的思想是将要排序的数组先分成两半分别排序,然后将结果归并起来。归并排序的重要性质就是其保证将任意长度为N的数组排序所需要的时间和NlogN成正比,
实现归并的一个简洁办法是将两个不同的有序数组归并到第三个数组中,具体实现为:创建一个适当大小的数组然后将两个有序数组的元素一个个 从小到大放到这个数组。
(1) 当需要用归并方法来将一个大数组进行排序时,需要进行很多次归并,在每一次归并时创建一个数组会带来很大开销,因此需要使用一种原地归并的方法,现介绍一种方法merge(array,low,mid,high),它可以将子数组array[low…mid]和array[mid+1…high]归并成一个有序数组并将结果存放在array[low…high]中。其将所有涉及的元素放到一个辅助数组,再将归并的结果放到原数组中:
public static void merge(Conparable[] array,int low,int mid,int high){
//将array[low...mid]和array[mid+1...high]合并
int i=low,j=mid+1;
for(int k=low;k<=high;k++){
//将数组array[low...high]复制到aux[low..high]中
aux[k]=array[k];
}
for(int k=low;k<=high;k++){
if(i>mid) array[k]=aux[j++]
else if(j>high) array[k]=aux[i++];
else if(less(aux[j],aux[i])) array[k]=aux[j++];
else array[k]=aux[i++];
}
}
该方法在进行归并时使用了四种判断:左半边用完时取右半边元素,右半边用完时取左半边元素,右半边元素小于左半边元素(取右半边元素),左半边元素小于右半边元素(取左半边元素)。
(2)自顶向下的归并
先介绍另一种递归归并,其应用分治思想,下面这段代码是归纳证明算法能够正确地将数组进行排序的基础:如果它能将两个字数组进行排序,他就能够通过归并两个子数组来将整个数组进行排序。
public class Merge{
private static Comparable[] aux; //归并所需的辅助数组
public static void sort(Comparable[] array){
aux=new Comparable[array.length];
sort(array,0,array.length-1);
}
public static void sort(Comparable[] array,int low,int high){
//将数组array进行排序
if(high<low) return ;
int mid=(low+high)/2;
sort(array,low,mid);
sort(array,mid+1,high);
merge(array,low,mid,high);
}
}
小结:(1)对于长度为N的数组,采用自顶向下的归并所需的1/2NlgN至NlgN次比较,且采用自顶向下的归并最多需要访问数组6NlgN次。
(3)自底而上的归并
实现归并排序的另一种思想是先归并那些小型数组,然后再成对归并得到的子数组,直到将整个数组归并到一起。实现这种方法时进行两两归并(将每个元素都看成一个数组),然后再进行四四归并(将两个大小为2的数组归并成一个有四个元素的数组),然后进行八八归并…,
自底而上的归并排序算法的实现如下:
public class MergeBU{
private static Comparable[] aux;
//merge()方法的代码见“原地归并方法”
public static void sort(Comparable[] array){
//进行lgN次两两归并
int N=array.length;
aux=new Comparable[N];
for(int size=1,size <N;size=size+size){//sz子数组大小
for(int low=0;low<N-size;low=low+size+size){
merge(array,low,low+size-1,Math.min(low+size+size-1,N-1));
}
}
}
}
自底而上的归并排序算法会多次遍历整个数组,根据整个数组的大小进行两两归并,子数组的大小size初始值为1,每次加倍。最后一个子数组的大小只有数组大小是size的偶数倍的时候才会等于size(否则会比size小)。
小结:(1)对于长度为N的数组,自底而上的归并排序需要1/2NlgN至NlgN次比较。最多访问数组6NlgN次。
(2)当数组长度为2的幂时,自顶而下和自底而上的归并排序所用的比较次数和数组访问次数正好相同,只是顺序不同。
(3)自底而上的归并排序算法适合用链表组织的数据。
五、快速排序算法
快速排序算法的优点是实现简单,且对于一般的输入数据在一般应用中比其他算法快,此外,快速排序算法还是一种原地排序算法,且长度为N的数组排序所需要的时间和NlgN成正比。
(1)基本的快速排序算法
快速排序使用基于分治思想的排序算法,其将一个数组分为两个子数组,两两部分独立地排序。 其实现方法是:随机找出一个数,可以随机取,也可以取固定位置,一般是取第一个或最后一个称为基准,然后就是比基准小的在左边,比基准大的放到右边,如何放做,就是和基准进行交换,这样交换完左边都是比基准小的,右边都是比较基准大的,这样就将一个数组分成了两个子数组,然后再按照同样的方法把子数组再分成更小的子数组,直到不能分解为止。
public class QuickSort{
//快速排序算法
public static void sort(Comparable[] array){
sort(array,0,array.length-1);
}
private static void sort(Comparable[] array,int low,int high){
if(high<=low) return ;
int j=partition(array,low,high);//切分
sort(array,low,j-1);
sort(array,j+1,high)
}
}
快速排序算法递归地将子数组array[low…high]排序,先用partition()方法将array[j]放到一个合适位置,然后再用递归调用将其他位置的元素排序。
该方法的关键在于切分,这个过程使得数组满足以下三个条件:
(1)对于某个j,array[j]确定;
(2)array[low]到array[j-1]中所有元素都不大于array[j];
(3)array[j+1]到array[high]中所有元素都不小于array[j].
要实现切分,一般策略是先取array[low]作为切分元素,即那个将会被排定的元素,然后从数组的左端开始向右扫描直到找到一个大于等于它的元素,再从数组的右端开始向左扫描直到找到一个小于等于它的元素。这两个元素显然是没有排定的,交换其位置。如此继续,就可以保证切分元素大于等于它左边的元素,小于等于它右边的元素。
快速排序的切分实现如下所示:
private static int partition(Comparable[] array,int low,int high){
//将数组切分成array[low...i-1]、array[i]、array[i+1...high]
int i=low,j=high+1; //为什么时high+1?
Comaprable v=array[low]; //切分元素
while(true){
//扫描左右,检查扫描是否结束并交换元素
while(less(array[++i],v)){
if(i==high) break;
}
while(less(v,array[--j])){
if(j==low) break;
}
if(i>=j) break;
exch(array,i,j);
}
exch(array,low,j);//将v=array[j]放到合适的位置上
return j; //array[low...i-1]<=array[i]<=array[i+1...high]
}
上述切分代码按照array[low]的值v进行切分,当指针i和j相遇时主循环退出,在循环中array[i]小于v时增大i,array[j]大于v时减小j,然后交换array[i]和array[j]来保证i左侧的元素都不大于v,i右侧的元素都不小于v,。当指针相遇时交换array[low]和array[j],切分结束(切分值留在array[j]中)。
小结:(1)长度为N的无重复数组排序,快速排序平均需要~2NlgN次比较(以及1/6次交换)
(2)快速排序最多需要(N^2)/2次比较,但是随机打乱数组能够预防这种情况。
(3)对于小数组进行排序,因为快速排序比插入排序慢,因此应切换到插入排序。