本文主要回顾基本的排序算法。内容包括算法的基本描述,算法的实现,算法的时间复杂度分析三个部分。
1.插入排序
顾名思义,插入排序就是将一个数插入到已经排好序的数组当中,我们将数字存储到一个数组中,从这个数组的第二个元素开始,假设前面的元素已经排好序,然后将这个数字插入到这个已经排好序的数组中。代码的实现如下
void Insert_Sort(int Array[],int N)
{
int i;
int j;
int term;
for(i=1;i<N;i++)
{
term=Array[i];
for(j=i;j>0&&term<Array[j-1];j--)
Array[j]=Array[j-1];
Array[j]=term;
}
}
时间复杂度分析:针对i的每一个值,我们可以得到最多进行i次比较,同时还有最后一行的一次赋值,一共是i+1次操作。所以我们可以计算,针对一个有N个元素的数组来说,最多的操作次数是 Θ(N2) .但是不得不说的是,如果这个数组是已经排好序的,那么我们只需要 O(N) 时间用来检验便可。所以我们可以研究一下插入操作的平均时间复杂度问题,也就是针对不同的数组(排序状态从最好到最差)的平均时间复杂度。不幸的是,平均时间复杂度也为 Θ(N2) .原因如下,插入排序的本质就消除数组中的逆序,有多少逆序,就需要多少次比较,就必须花费多少时间。我们就研究一个数组的平均逆序数是多少。针对一个表,我们考虑它的反序表,显然的是它反序表中的逆序数和这个表的逆序数和为 N(N−1)/2 ,所以平均来说,这个表的逆序数是 N(N−1)/4 ,所以说插入排序平均时间复杂度为 Θ(N2)
2.希尔排序
插入排序一次的比较只改变一个逆序,所以时间复杂度和逆序数成正比。但是,如果我的一次比较可以改变多个逆序,是否会使得时间复杂度降低,那么一种可能的操作就是作比较的两个数距离要足够远(至少不是1),这样是可以改变多个逆序的。希尔排序就是运用这样的思想,来改进插入排序。它引入一个叫做增量序列={
ht,...,h1
}的概念,也就是确定比较的距离(插入排序比较距离就是1,相邻的两个元素进行比较),先从大的比较距离开始,然后不断缩小比较距离,直到比较距离为1.这样的话,就有可能缩短时间。
其实希尔排序是插入排序的一个推广。在确定了比较距离
hk
之后,我们可以将这个数均分成若干份,每份中的元素有
hk
个,那么一趟
hk
希尔排序,就是
hk
个独立数组的插入排序。下面的代码使用
ht=⌊N/2⌋
和
hk=⌊hk+1/2⌋
,也就是希尔增量.
void Shell_Sort(int Array[],int N)
{
int increment;
int i,j;
int term;
for(increment=N/2;increment>0;increment/=2)
for(i=increment;i<N;i++)
{
term=Array[i];
for(j=i;j>=increment;j-=increment)
if(Array[j-increment]>term)
Array[j]=Array[j-increment];
else
break;
Array[j]=term;
}
}
时间复杂度分析:希尔排序的时间复杂度依赖于增量序列的选择。不同的增量对应的时间复杂度也不相同。我们只说明两证增量序列。一种是希尔增量序列,另一种是Hibbard增量序列。
使用希尔增量时希尔排序的最坏运行时间为
Θ(N2)
。在这里,我们只说明最坏运行时间为
O(N2)
,正如希尔排序的描述中所说,每一趟的
hk
排序就是
hk
个独立的数组进行插入排序,每个数组的元素个数为
N/hk
,所以我们可以知道一趟排序所需要的时间为
O(hk∗(N/hk)2)
,所以希尔排序所需要的时间为
O(∑hkN2/hk)=O(N2)
.
使用hibbard增量的希尔排序的最坏情形运行时间为
Θ(N32)
其增量形式为
1,3,..2k−1
.这个证明比较复杂,略。
3.堆排序
要想理解堆排序,必须向明白优先队列实现中的二叉堆。二叉堆将最小的元素放到树的根部,同时二叉堆有一个操作叫做Deletemin,这样可以取出二叉堆中的最小元,并从二叉堆中删除该最小元。但是我们在这里将最大元放到根部,然后每次去除最大元完,进行N次,便可以完成排序。我们把最大元取出之后,放在二叉堆(是一个数组)的最后一个位置上,这样,在经过N次的操作之后,数组便已经排好序。
对于任意给定的一个数组,我们首先必须将它转变成二叉堆(根部是最大)的形式,所以这里需要介绍二叉堆的下滤概念。二叉堆的一个基本要求就是父节点的关键字应该大于其子节点的关键字,但是如果不是这样,我们就需要交换父节点和子节点的位置,使得其符合二叉堆的要求。其实,也就是找出子节点的最小值,和父节点进行比较,如果比它大,交换两者的位置,否则不做改变。用代码实现如下
void Perdown(int Array[],int position,int N)
{
int temp=Array[position];
int child;
for(;2*position+1<=N-1;position=child)
{
child=2*position+1;
if(child<N-1&&Array[child+1]>Array[child])
child++;
if(temp<Array[child])
Array[position]=Array[child];
else
break;
}
Array[position]=temp;
}
然后我们就可以实现堆排序,代码如下:
void Heap_Sort(int Array[],int N)
{
//构造一个二叉堆
int i,j,term;
for(i=N/2;i>=0;i--)
Perdown(Array,i,N);
//进行排序
for(j=N-1;j>0;j--)
{
term=Array[0];
Array[0]=Array[j];
Array[j]=term;
Perdown(Array,0,j);
}
}
时间复杂度分析:在构造一个堆时的时间复杂度为 O(N) ,在进行排序时,每次从根处下滤需要时间是 O(logN) ,进行了一共进行这种操作是N-1次,所以时间复杂度为 O(NlogN)
4.归并排序
归并排序的思想比较简单,就是把两个已经排好序的数组合并在一起,根据这个思想,我们可以用递归的方法来实现对一个数组的排序。容易想到,如果数组只有一个元素,那么我们的排序就算完成了。否则将这个数组中分成两个部分。分别对两个部分进行归并排序,然后将两个部分合并在一起。我们首先实现两个排好序的数组的合并问题。这两个数组我们放在一个数组里边,代码如下:
void Merge(int Array[],int Array_term[],int lfirst ,int center,int rend)
{
int lend=center-1;
int rfirst=center;
int lpos=lfirst;
int rpos=rfirst;
int termpos=lfirst;
int i;
int num=rend-lfirst+1;
while(lpos<=lend&&rpos<=rend)
{
if(Array[lpos]>Array[rpos])
Array_term[termpos++]=Array[rpos++];
else
Array_term[termpos++]=Array[lpos++];
}
while(lpos<=lend)
Array_term[termpos++]=Array[lpos++];
while(rpos<=rend)
Array_term[termpos++]=Array[rpos++];
for(i=0;i<num;i++,rend--)
Array[rend]=Array_term[rend];
}
归并排序代码:
void Mergesort(int Array[],int Array_term[],int first,int end)
{
int middle=(first+end)/2;
if(first<end)
{
Mergesort(Array,Array_term,first,middle);
Mergesort(Array,Array_term,middle+1,end);
Merge(Array,Array_term,first,middle+1,end);
}
}
void Merge_Sort(int Array[],int N)
{
int *Array_term;
Array_term=malloc(sizeof(int)*N);
if(Array_term!=NULL)
{
Mergesort(Array,Array_term,0,N-1);
free(Array_term);
}
else
printf("no space for tmp array!!!\n");
}
时间复杂度分析:归并算法的时间复杂度比较容易分析,一个长度为N的数组的时间复杂度等于两个长度为N/2的数组的排序时间复杂度加上N(表示合并的时间复杂度)
两遍除以N,便可以得到归并算法的时间复杂度为 O(NlogN)
5.快速排序
快速排序是一种分而治之的递归策略。首先,截止条件是当数组中只有一个元素时,那么排序就完成。否则选择出一个枢纽元,将大于这个枢纽元的元素放在一起,形成集合
S2
,将小于这个枢纽元的元素放在一起,形成集合
S1
。分别对集合
S1
和
S2
进行快速排序。这是一般的思路,但是如果数组的长度比较短时,插入排序要好于快速排序,因此截止条件可以改为当数组有
cutoff
个元素时,进行一个插入排序,然后终止排序。
根据算法的描述,我们需要解决两个问题
1.寻找枢纽元
2.寻找比枢纽元大的元素和比枢纽元小的元素
枢纽元的确定有多种方法,我们只介绍三数中值分割法。给定一个数组,我们将数组的最左面元素和最右面元素,以及中间元素取出,确定这三个数的中值,同时完成将三个数的最小值放在最左面,最大数放在最右面,中值放在倒数第二个位置,返回枢纽元。代码如下:
void swap(int *a,int *b)
{
int *term=malloc(sizeof(int));
*term=*a;
*a=*b;
*b=*term;
}
int median(int Array[],int left,int right)
{
if(right==left)
return Array[left];
else
{
int center=(left+right)/2;
if(Array[left]>Array[center])
swap(&Array[left],&Array[center]);
if(Array[center]>Array[right])
swap(&Array[center],&Array[right]);
if(Array[left]>Array[center])
swap(&Array[center],&Array[left]);
swap(&Array[center],&Array[right-1]);
}
return Array[right-1];
}
在枢纽元选取的过程中我们将最小元放在最左端,最大元放在最右端,枢纽元放在倒数第二个位置,是为了解决第二个问题做准备。在确定了枢纽元之后,我们分别从数组的左端和右端出发,给出一个游标
i
和
解决了这两个问题,我们就基本完成快速排序,代码如下
void Fast_Sort(int Array[],int N)
{
fsort(Array,0,N-1);
}
void fsort(int Array[],int left,int right)
{
if(left+cutoff<=right)
{
int pivot=median(Array,left,right);
int i=left,j=right-1;
for(;;)
{
while(Array[++i]<pivot){}
while(Array[--j]>pivot){}
if(i<j)
swap(&Array[i],&Array[j]);
else
break;
}
swap(&Array[i],&Array[right-1]);
fsort(Array,left,i-1);
fsort(Array,i+1,right);
}
else
Insert_Sort(Array+left,right-left+1);
}
时间复杂度分析:快速排序是递归排序,那么我们可以通过确定递推式来确定其时间复杂度。这里我们采用随机的确定枢纽元的方式,以此来获得最坏时间和最好时间,平均时间。
其中N表示解决上述两个问题所用的时间。最坏时间是枢纽元始终是最小,那么递推关系如下:
所以
最好时间. 枢纽元正好是中间元,那么递推关系如下:
所以
平均时间. 随机的选择枢纽元使得T(i)的平均时间为 (1/N)∑N−1j=0T(j) ,那么递推关系如下:
从而得到 T(N)=O(NlogN).
6.桶式排序
这种排序方法的一个必要前提就是必须知道数组的元素的上界。假设元素的上界是
M
,我们定义一个长度为M的数组
7.冒泡排序 首先把数组中的最大元素降到数组的最右端,然后依次将第i大的元素降到倒数第i个位置。代码如下:
void Bubble_Sort(int Array[],int N)
{
int i,j;
for(i=N-1;i>0;i--)
{
for(j=0;j<i;j++)
{
if(Array[j]>Array[j+1])
swap(&Array[j],&Array[j+1]);
}
}
}
时间复杂度分析:最坏时间和平均时间都是 O(N2)
8.选择排序 简单的选择排序的第i趟就是从第i个位置到最后的位置选出最小的元素,然后将这个元素和第i个位置上的元素进行交换。代码如下:
void Select_Sort(int Array[],int N)
{
int i,j,minindex;
for(i=0;i<N-1;i++)
{
minindex=i;
for(j=i+1;j<N;j++)
if(Array[j]<Array[minindex])
minindex=j;
swap(&Array[i],&Array[minindex]);
}
}
时间复杂度分析:最坏时间和平均时间都是 O(N2)