排序分为内部排序和外部排序。
内部排序是指在排序的过程中待排序的所有数据元素全部被放置在内存中。
外部排序是指由于待排序的数据元素太多,不能同时放置在内存中,而需要把一部分的数据元素放置在内存中,把剩余的数据元素放在外设上,整个排序过程需要在内外存之间多次交换数据才能得到最终的排序结果。
排序算法的稳定性:
如果在待排序的序列中,有多个元素相同,经过排序后这些元素的相对顺序保持不变,则这种排序算法是稳定的,否则为不稳定的。
举个简单的列子,现在有一个长度为5的数组{ 5, 2 , 3 , 3 , 8},按从小到大的顺序排好之后是{2,3,3,5,8}。排好序后,‘带下划线的3’仍然在‘不带下划线的3’前面,顺序不变,那么我们就说这个排序是稳定的。若排好序的数组为{2,3,3,5,8}由于两个3的相对顺序改变了,故此时排序是不稳定的。
Ps:关于这个知识点,上个月的腾讯实习生笔试题上有道多项选择题,让选择不稳定排序。
(下面内容只涉及内部排序,详细算法分析见《算法导论》,《数据结构与算法分析》…)
冒泡排序
要点:
1.该算法重复的比较相邻的两个元素,若不满足条件则交换这两个元素。
2.每一轮比较都将最大(最小)元素推向尾部(尾部元素变得有序),因此下一轮进行比较的元素个数将不断减少
3.优化:如果某一轮比较中的没发生交换,则表示整个序列已经有序,提前结束。
4.稳定排序,就地排序,复杂度n^2
代码样例:
void bubblesort( int a[], int len )//a为待排序序列,len为元素个数
{
int i , j, temp;
int flag = 0;
for ( i = 0; i < len-1; ++i ) {//len个数共进行len-1趟排序
flag = 0;
for ( j = 0; j < len-i-1; ++j ) {//每趟len-i个数比较交换n-i-1次
if ( a[j] > a[j+1] ) { //从小到大排序,(a[j] < a[j+1]) 从大到小排序
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
flag = 1;
}
}
if ( flag == 0 ) break;
}
}
选择排序
要点:
第一轮比较,找出序列中所有元素中的最大(最小)值,和第一个数交换;
第二轮比较,找出除第一个数外的所有数里的最大(最小)值,和第二个数交换;
第三轮比较,找出除第一,二个数外的所有数里的最大(最小)值,和第三个数交换;
以此类推…
共需n-1趟比较,每趟比较次数逐渐减少。
不稳定排序(2,8,8,4进行排序),就地排序,复杂度n^2
代码样例:
void selectsort( int a[], int len )
{
int i, j, k, temp;
for ( i = 0; i < len-1; ++i ) {
k = i;
for ( j = i+1; j < len; ++j ) {
if ( a[k] > a[j] ) k = j;
}
if ( k != i ) {
temp = a[k];
a[k] = a[i];
a[i] = temp;
}
}
}
奇偶交换排序
要点:
第一步,将所有偶数下标元素a[i]和a[i+1]进行比较,若不满足排序条件则交换;
第二步,将所有奇数下标元素a[i]和a[i+1]进行比较,如不满足排序条件则交换;
循环执行这两步骤,直到数列有序。
在多处理器环境下该算法很有用。 稳定排序,就地排序,复杂度n^2, 最坏需比较n趟,每趟比较(n-1)/2。
代码样例:
void evenoddsort( int a[], int len )
{
int i, j;
int flag = 1;
int temp;
for ( i = 0; i < len; ++i ) { //最坏需n趟
for ( j = i%2; j < len-1; j += 2 ) { //j = i%2,先偶,后奇
if ( a[j] > a[j+1] ) {
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
flag = 1;
}
}
flag++;
if ( flag == 3 ) break; //优化,提前退出,此时,偶数,奇数排序均已序,故结束。
}
}
插入排序
要点:
模拟抓牌过程,具体参见《算法导论》第九页,通俗易懂 适用于少量已序元素排序 折半插入是一种优化 稳定排序,就地排序,复杂度n^2
代码样例:
直接插入排序:
void insertsort( int a[], int len )
{
int i, j, temp;
for ( j = 1; j < len; ++j ) {
temp = a[j];
for ( i = j-1; i >= 0 && a[i] > temp ; --i ) a[i+1] = a[i];
a[i+1] = temp;
}
}
折半插入排序:
void binarysort( int a[], int len )
{
int i;
for ( i = 1; i < len; ++i ) {
HalfInsertSort( a, 0, i-1, i );
//show( a, len );
}
}
void HalfInsertSort( int a[], int l, int r, int k )
{
int temp = a[k];
if ( l > r ) return;
if ( l == r ) {
if ( temp >= a[l] ){
while ( k > l+1 ) {
a[k] = a[k-1];
k--; //把这两句合成一句,写成a[k] = a[--k];结果悲剧的
} //debug了好久
a[l+1] = temp;
}else {
while ( k > l ) {
a[k] = a[k-1];
k--;
}
a[l] = temp;
}
return;
}
int mid = (l+r+1)>>1;
if ( a[mid] <= temp ) {
HalfInsertSort( a, mid+1, r, k );
}else {
HalfInsertSort( a, l, mid-1, k );
}
}
希尔排序
要点:
关键是增量序列。该排序通过每次使用增量序列中的一个元素,将原始待排序列划分成多个子序列,在对子序列调用插入排序完成子序列排序。比如现在有一个10个元素的数组a[10],增量序列采用的是希尔序列, 则有d[] = { 5, 2, 1 }。第一轮排序时,取增量ß = d[0] = 5, 则数组被划分为(a[0], a[5]),(a[1],a[6]), (a[2],a[7]),(a[3],a[8]),(a[4],a[9])五组子序列,然后分别对每个子序列调用插入排序算法,完成子序列的排序。第二轮排序时,取增量ß = d[1] = 2, 则数组被划分为(a[0],a[2],a[4],a[6],a[8]),(a[1],a[3],a[5],a[7],a[9])两组子序列,然后分别对每个子序列调用插入排序算法,完成子序列的排序。第三轮排序时,取ß = d[2] = 1,则数组被划分为(a[0],a[1],…,a[9])一组序列,调用插入排序算法,整个排序过程结束。 增量序列的三种实现(还有很多),希尔序列的最坏情况n^2, hibbard序列的最坏情况n^(3/2),还有n^(4/3)的。len表示序列长度,则希尔序列a[] = {len/2, len/2/2, len/2/2,…,1},hibbard序列a[] = {1, 3, 7,…2^(k)-1}, 第三种序列a[] = {1, 5, 19, 41, 109, …}, 该序列中的项或者是9*4^(i)-9*2^(i)+1,或者是4^(i)-3*2^(i)+1。其中第三个在实践中用的比较好。《数据结构与算法分析》有很详细的分析。有个疑问,根据希尔排序的定义,这里的后两者序列是不是应该reverse以下呢? 不稳定排序(分组的时候相同的元素可能被分到不同的组里.…),就地排序,比插入排序快,复杂度随增量序列的选择而不同,最坏情况n^2。
代码样例:
void shellsort( int a[], int len )
{
int d;
int id, j, temp;
for ( d = len/2; d >= 1; d >>= 1 ) { //使用希尔序列
for ( id = d; id < len; ++id ) { //在纸上模拟过程会比较清楚
j = id;
temp = a[j];
while ( j >= d && temp < a[j-d] ) {
a[j] = a[j-d];
j -= d;
}
a[j] = temp;
}
}
}
归并排序
要点:
1. 基本操作时合并两个已排序的表。两个已排序的表a、b, 另一个表c用来存放结果,第一次取出a表和b表的最顶端元素进行比较,把较小(较大)的取出放到c表中,第二趟,继续取出a,b表中的最顶端元素比较,把较小(较大)的取出放到c表的下一个位置,重复上述步骤,直到a,b表中有一个表的元素已经取完,接着把另一张表的剩余元素按顺序加到c表中,排序结束。
2. 算法实现过程中通过不断把原序列拆成小序列,在把小序列变得有序,然后合并,从而使整个序列有序。整个过程可简化为:分解,解决,合并。
3. 稳定排序,不就地排序,最坏复杂度nlogn,比较次数几乎是最优的。
代码样例:
void Merge( int a[], int l, int mid, int r )
{
int i = 0, j = 0;
int k = l;
int *b = new int[mid-l+1];
int *c = new int[r-mid];
// for ( i = 0; i < mid-l+1; ++i ) b[i] = a[i+l];
memcpy( b, a+l, (mid-l+1)*sizeof(int) );
// for ( i = 0; i < r-mid; ++i ) c[i] = a[i+mid+1];
memcpy( c, a+mid+1, (r-mid)*sizeof(int) );
// i = j = 0; //注意初始化
while ( i < mid-l+1 && j < r-mid ) {
if ( b[i] < c[j] ) {
a[k++] = b[i++];
}else {
a[k++] = c[j++];
}
}
while ( i < mid-l+1 ) a[k++] = b[i++];
while ( j < r-mid ) a[k++] = c[j++];
delete [] b;
delete [] c;
// show( a, 4 );
}
void mergesort( int a[], int l, int r )
{
if ( l >= r ) return;
int mid = (l+r)>>1;
mergesort( a, l, mid );
mergesort( a, mid+1, r );
Merge( a, l, mid, r );
}
剩下的五种下周再写了...
欢迎大牛来喷...
快速排序
要点:
1.在待排序的n个元素中任意选择一个作为基准元素(通常取第一个),把该元素放入最终的位置上,数据序列被此元素划分成两部分:所有关键字比该元素关键字小的元素放置在前一部分,所有比它大的元素放在后一部分,这个过程称为一趟快速排序。对分成的两部分重复上述过程,知道每部分只有一个元素或空为止。
2.正序,反序时效率都很低,只有随机分布时效率才最高。
3. 不稳定排序,就地排序,最坏复杂度n^2, 最好复杂度nlogn
代码样例:
void quicksort( int a[], int l, int r )
{
if ( l >= r ) return;
int i, j, temp;
temp = a[l];
i = l; j = r;
while ( i < j ) {
while ( i < j && temp < a[j] ) j--;
a[i] = a[j];
while ( i < j && temp > a[i] ) i++;
a[j] = a[i];
}
a[i] = temp;
quicksort( a, l, i-1 );
quicksort( a, i+1, r );
}
堆排序
要点:
用了堆的性质,以最小堆为例,则堆顶元素为当前最小值。通过每次把堆顶元素和堆尾元素交换,得到当前最小值,堆的大小减1,调整堆,继续上述过程,直到堆的大小为1。需要先建立初始堆。需要对堆的性质了解。 a[i]的左孩子a[2*i],右孩子a[2*i+1],双亲a[i/2]. a[0]为树根,即最小值。 不稳定,就地排序,最坏时间复杂度nlogn,不适宜小规模数据。
代码样例:
很多书上都是数组下标从1开始的,写个从1开始的,利用最大堆实现升序排列
void min_heapify( int a[], int root, int tail ) //假设其左右子树均已是堆
{
int i = root;
int j = 2 * i + 1; //左孩子
int temp = a[i];
while ( j <= tail ) {
if ( j < tail && a[j] < a[j+1] ) j++; //比较左右孩子,j取最小值下标
if ( temp < a[j] ) { //子树中的最小值与树根比较
a[i] = a[j];
i = j;
j = 2 * i + 1;
}else {
break; //筛选结束
}
}
a[i] = temp; //送入最终位置
}
void heapsort( int a[], int len )
{
int i, temp;
//构造初始堆
for ( i = (len-1)/2; i >= 0; --i ) {
min_heapify( a, i, len-1 ); //调整,以满足堆的性质
}
for ( i = len-1; i > 0; --i ) {
temp = a[i]; //交换首尾元素
a[i] = a[0];
a[0] = temp;
min_heapify( a, 0, i-1 );
}
}
桶排序
要点:
算法思想:把区间[0,1)划分成n个相同大小的子区间,或称桶。然后将n个输入数分配到各个桶中。先对各个桶中的元素进行排序,然后按次序把各个桶中的元素列出来即可得到结果。 假设输入由一随机过程产生。
代码示例:
void bucketsort( int a[], int len ) //偷懒的用了stl…
{
vector< list<int> > vec;
vec.resize( 10 );
int i, j;
for ( i = 0; i < len; ++i ) {
vec[a[i]/10].push_back( a[i] );
}
for ( i = 0; i < 10; ++i ) {
vec[i].sort();
}
int k = 0;
for ( i = 0; i < 10; ++i ) {
while ( !vec[i].empty() ) {
a[k++] = vec[i].front();
vec[i].pop_front();
}
}
}
计数排序
要点:
1. 基本思想: 对每一个输入元素x,确定出小于x的元素个数,这样就可以直接把x放到它最终的位置上。计数排序假设n个输入元素的每一个都是介于0到k之间的整数,其中k为某个整数。
2. 需要n+k个存储空间。
3.稳定的,不就地排序,复杂度n+k
代码样例:
void countingsort( int a[], int len )
{
int k = 1000; //这里假设最大值为999
int i, j;
int * b = new int[ len ];
int * c = new int[ k ]; //c[i]表示包含i的元素的个数
for ( i = 0; i < k; ++i ) {
c[i] = 0;
}
for ( i = 0; i < len; ++i ) {
c[ a[i] ]++;
}
for ( i = 1; i < k; ++i ) { //计算包含小于等于i的元素个数
c[i] += c[i-1];
}
for ( i = len-1; i >= 0; --i ) { //注意
int temp = c[ a[i] ];
b[temp-1] = a[i];
c[ a[i] ]--;
}
memcpy( a, b, len*sizeof(int) );
delete [] b;
delete [] c;
}
基数排序
要点:
1. 非基于比较的排序,稳定排序,算法复杂度d(n+k) , d为位数, k进制
2. 每次按照每个数字的一位进行排序,先按照最个位,再按照十位,依次类推至最高位。其中每一位的排序必须是稳定排序才能保证算法的正确性。
代码样例:
int maxbit( int a[], int len )
{
int i;
int d = 1;
int p = 10;
for ( i = 0; i < len; ++i ) {
while ( a[i] >= p ) {
p *= 10;
++d;
}
}
return d;
}
void radixsort( int a[], int len )
{
int d= maxbit( a, len );
int *temp = new int[ len ];
int *count = new int[ 10 ];
int i, j, k;
int radix = 1;
for ( i = 1; i <= d; ++i ){
内部为计数排序
for ( j = 0; j < 10; ++j ) {
count[j] = 0;
}
for ( j = 0; j < len; ++j ) {
k = ( a[j]/radix ) % 10;
count[k] ++;
}
for ( j = 1; j < 10; ++j ) {
count[j] += count[j-1];
}
for ( j = len-1; j >= 0; --j ) {
k = (a[j]/radix) % 10;
count[k] --;
temp[ count[k] ] = a[j];
}
for ( j = 0; j < len; ++j ) {
a[j] = temp[j];
}
radix *= 10;
}
delete [] temp;
delete [] count;
}
(转自http://hi.baidu.com/youngcat7478/blog/item/e94951501fc0703343a75b28.html?timeStamp=1312169871671)