对于一般的内部排序应用程序,选用的方法不是插入排序、希尔排序,就是快速排序。他们的选择主要是根据输入的大小来决定。
1、插入排序
对含N个元素的数组使用插入排序,,一共由N-1次排序组成。循环历遍到位置P的时候,保证位置P之前(0-P)的元素都是已经排序的。插入排序,嵌套循环,外层循环历遍到某一个位置,确保该位置之前的元素是排序好的。内层循环,通过和相邻元素交换,实现排序。插入排序,将大于当前元素的元素集体向后移动一格,然后让当前这个待排序的元素插入到空出来的那个位置中。
插入排序的算法复杂度为:O(),由于这个界是精确的,计算可以得复杂度为
(
)
void
InsertionSort(int A[],int N)
{
int j,p;
int temp;
for(p=1;p<N;p++)
{
temp=A[p];
for(int j=p;j>0 && A[j-1]>temp; j--)//此处相当于讲大于当前元素temp的元素集体向后移动一格,然后将temp插入到对应位置
{
A[j]=A[j-1];
}
A[j]=temp;//将 j 定义在循环之外,通过这样的做法,避免明显地使用交换
}
}
2、希尔排序
它通过比较相距一定间隔的元素来工作,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。一种流行的做法是使用折半的方法来进行,虽然这种做法的效果并不好。
void
shellsort(int A[],int N)
{
int i,j,increment;
int temp;
for(increment=N/2;increment>0;increment/=2)
{
for(i=increment;i<N;i++)
{
temp=A[i];
for(j=i; j>=increment;j-=increment)
{
if(temp<A[j-increment])
{
A[j]=A[j-increment];
}
else
break;
}
A[j]=temp;
}
}
}
Hibbard提出一个稍微不同的增量序列,在实践中更好的结果。他的增量为 1, 3, 7, .......,。使用这样的序列,和其他序列的差别在于相邻的增量没有公因子。hibbard增量的希尔排序的最坏情形运行时间为
(
)
这里有一个视频演示,非常形象生动,进行的就是5,3,1序列的排序(其实就是插入排序)。非常有助于理解。https://www.bilibili.com/video/av17004970/
3、归并排序
归并排序以O(NlogN)最坏情形运行时间运行,所使用的比较次数几乎是最优的。
该算法是经典的分治策略,将问题分成小问题,然后递归求解。分治阶段将各个答案合并到一起。例如将一下两个已经排序的数列合并到一起,这个过程是线性的。
1 | 13 | 24 | 26 |
2 | 15 | 27 | 38 |
1 | 2 | 13 | 15 | 24 | 26 | 27 | 38 |
程序中的Merge函数使用是非常精妙的,Merge寒素合并的时候,需要一个临时数组,用于将左右两边排序好的数组合并。这里Merge位于最后一行,因此程序中任意时刻只需要一个临时数组活动。相反,如果Merge的每一个递归调用均在局部声明一个临时数组,那么在任意时刻就可能有logN个临时数组处于活动期,这对内存的占用是很过分的。
尽管归并排序的运行时间是O(NlogN),但是它很难用于内存排序,主要的问题在于,第一、使用归并排序的时候,需要附加合并左右两边数组的时间;第二、使用归并排序,合并的时候要将数据拷贝到一个临时数组中,之后要要将临时数组的数据拷贝到原来的数组中。这导致实际使用的时候速度变慢。在实际使用的时候,对内部排序还是使用快速排序。
补充:内排序是指所有的数据已经读入内存,在内存中进行排序的算法。排序过程中不需要对磁盘进行读写。同时,内排序也一般假定所有用到的辅助空间也可以直接存在于内存中。与之对应地,另一类排序称作外排序,即内存中无法保存全部数据,需要进
//主程序(归并排序)
void
Mergesort( ElementType A[ ], int N )
{
ElementType *TmpArray;
TmpArray = malloc( N * sizeof( ElementType ) );
if( TmpArray != NULL )
{
MSort( A, TmpArray, 0, N - 1 );
free( TmpArray );
}
else
FatalError( "No space for tmp array!!!" );
}
void
MSort( ElementType A[ ], ElementType TmpArray[ ], int Left, int Right )
{
int Center;
if( Left < Right )
{
Center = ( Left + Right ) / 2;
MSort( A, TmpArray, Left, Center );//排左边
MSort( A, TmpArray, Center + 1, Right );//排右边
Merge( A, TmpArray, Left, Center + 1, Right );//合并,这里的merge使用是非常精妙的
}
}
/*乍一看,这里连个比较都没有,让人有些困惑,实际上比较是在Merge函数中进行的。
递归最终将将进行到左右都只有一个元素,合并这两个元素的时候,自然的做了比较*/
void
Merge( ElementType A[ ], ElementType TmpArray[ ], int Lpos, int Rpos, int RightEnd )
{
int i, LeftEnd, NumElements, TmpPos;
LeftEnd = Rpos - 1;
TmpPos = Lpos;
NumElements = RightEnd - Lpos + 1;
/* main loop */
while( Lpos <= LeftEnd && Rpos <= RightEnd )
if( A[ Lpos ] <= A[ Rpos ] )
TmpArray[ TmpPos++ ] = A[ Lpos++ ];
else
TmpArray[ TmpPos++ ] = A[ Rpos++ ];
//上一个循环完了之后,左右两边只有一边有元素可能剩下来(使用二分,两个元素个数最多差1)
while( Lpos <= LeftEnd ) /* Copy rest of first half */
TmpArray[ TmpPos++ ] = A[ Lpos++ ];
while( Rpos <= RightEnd ) /* Copy rest of second half */
TmpArray[ TmpPos++ ] = A[ Rpos++ ];
//将临时数组中的元素拷贝到A中
/* Copy TmpArray back */
for( i = 0; i < NumElements; i++, RightEnd-- )
A[ RightEnd ] = TmpArray[ RightEnd ];
}
行磁盘访问,每次读入部分数据到内存进行排序。算法复杂度分析:
当n=1的时候,T(1)=N
当n=N的时候,T(N)=2T(N/2)+N ,因为将数组分成了两部分,所以有2T(N/2),另外,要将数组合并,所以需要N的时间。于是就找到了一个递推关系式。
计算方法:两边同时除以N
![]()
...........
这样的式子一共有log N 个(以2为底),将所有式子带入第一个式子可以得到
于是可以得到最后的答案:
光说不练假把式 :关于归并排序的一道练习题leetcode 148. 排序链表,C++解答
4、快速排序
快速排序是实践中最快的排序算法,平均运行时间为O(NlogN),最坏情形为O().
快速排序使用一个枢纽元将数据分为两部分,然后递归地快速排序这两部分。枢纽元的选取,随机选取是比较安全。另一种是使用三种元素来确定枢纽元。分别取最左边,最右边,中间的三个元素比价,取介于中间的元素作为枢纽元。
ElementType
Median3( ElementType A[ ], int Left, int Right )
{
int Center = ( Left + Right ) / 2;
if( A[ Left ] > A[ Center ] )
Swap( &A[ Left ], &A[ Center ] );
if( A[ Left ] > A[ Right ] )
Swap( &A[ Left ], &A[ Right ] );
if( A[ Center ] > A[ Right ] )
Swap( &A[ Center ], &A[ Right ] );
/* Invariant: A[ Left ] <= A[ Center ] <= A[ Right ] */
Swap( &A[ Center ], &A[ Right - 1 ] ); /* Hide pivot */
return A[ Right - 1 ]; /* Return pivot */
}
//主程序
void
Qsort( ElementType A[ ], int Left, int Right )
{
int i, j;
ElementType Pivot;
/* 1*/ if( Left + Cutoff <= Right )
{
/* 2*/ Pivot = Median3( A, Left, Right );
/* 3*/ i = Left; j = Right - 1;
/* 4*/ for( ; ; )
{
/* 5*/ while( A[ ++i ] < Pivot ){ }
/* 6*/ while( A[ --j ] > Pivot ){ }
/* 7*/ if( i < j )
/* 8*/ Swap( &A[ i ], &A[ j ] );
else
/* 9*/ break;
}
/*10*/ Swap( &A[ i ], &A[ Right - 1 ] ); /* Restore pivot */
/*11*/ Qsort( A, Left, i - 1 );
/*12*/ Qsort( A, i + 1, Right );
}
else /* Do an insertion sort on the subarray */
/*13*/ InsertionSort( A + Left, Right - Left + 1 );
}
5、桶式排序
一些特殊情况下,排序可以以线性时间运行。例如桶式排序。
例如:对于一个数组A,他的所有元素都是由小于M的正整数够成,那么这就可以使用桶式排序。算法很简单,使用一个大小为M的数组count,并将所有元素初始化为零,循环历遍数组,每当读到的时候,将
的值加一。最后在通过一个循环历遍count数组,就可以完成排序。这个算法的时间界为O(N)。
参考:
学习书籍《数据结构与算法分析——C语言描述》