欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【算法导论】 内部排序算法总结

程序员文章站 2022-06-04 09:25:55
...

排序名称 时间复杂度 空间复杂度 稳定性
直接插入排序 O(n^2) O(1) 稳定
折半插入排序 O(n^2) O(1) 稳定
希尔排序 O(n^2) O(1) 不稳定
冒泡排序 O(n^2) O(1) 稳定
快速排序 O(n*logn) O(1) 不稳定
简单选择排序 O(n^2) O(1) 不稳定
堆排序 O(n*logn) O(1) 不稳定
归并排序 O(n*logn) O(n) 稳定
计数排序 O(k+n) O(k+n) 稳定
基数排序 O( d(k+n) ) O(k+n) 稳定
桶排序 O(n) O(n+m) 稳定
Ps:三种插入排序、冒泡排序的最优情况下时间复杂度为O( n ),快速排序最差时间复杂度为O( n^2 ) .
       基数排序、计数排序、桶排序会耗费较多的空间。
【算法导论】 内部排序算法总结
(参见:https://blog.csdn.net/hguisu/article/details/7776068)


比较排序的下界

比较排序通过元素之间的比较确定元素的位置。例如给定两个元素a、b,共有a≥b,a>b,a=b,a<b,a≤b五种比较方式,假设排序中给定的元素都是互异的,则不存在a=b这种比较方式。剩下的≥、>、<、≤四中比较方式是等价的,因为通过它们得到的a、b的相对次序信息都是相同的。

假设采用≤这种比较方式(下同),若a≤b为真,将a调整到b的位置前,若a≤b为假,将a调整到b之后,a、b有序。

决策树模型:比较排序可以抽象为一棵决策树(决策树是完全二叉树),决策树的所有内部结点以 i : j 表示(1≤ i,j≤n),表示对a[ i ]、a[ j ] 进行比较,若i≤j为真,进入其的左子树,若i≤j为假,进入其右子树。直至一系列比较后,到达叶子节点。叶子节点存储一个序列<π1、π2 . . . πn>(1≤ k,πk≤n),表示<a_π1 、a_π2 . . . a_πn>已经有序。以数组a中有3个元素为例,见下图:

【算法导论】 内部排序算法总结

对于数列a1、a2、a3,比较a1≤a2是否为真,若为真继续比较a2≤a3,若a2≤a3为真那么数列的顺序为:a1、a2、a3. 若为假则在右子树继续进行判断。假设现有数组a = { 5, 7, 3} 进行排序,首先比较a1≤a2,为真,继续比较a2≤a3,为假,继续比较a1≤a3,为假,此时到达叶节点<3,1,2>,则可得出排序后的结果:a3、a1、a2,即3、5、7。

对于任意n个元素的排序,都可以构建这样一颗决策树,且叶子节点的数目至少为n!个(对于每种不同的情况都能在叶子节点上找到对应的输出,例如上述决策树共有6个叶子结点,包含1、2、3的所有可能组合)。

那么一个比较排序算法在最坏情况下的比较次数为h(h为决策树的高度),且 h ≥ log(n!) = Ω( n logn );

结论: 最坏情况下,任何比较排序算法都需要做Ω( n logn )次比较。

堆排序和归并排序的运行时间都为O( n logn),因此两者都是渐进最优的比较排序算法。

Ps:O --渐进上界           Ω --渐进下界             Θ--渐进紧确界


插入排序----直接插入排序 (Straight Insertion Sort)

对于数组a[1...n],对于k(k从1开始,到n结束),依次调整a[k]的位置,使得a[1...k]为有序数组。

当k = n时,则完成排序,a[1...n] 成为有序数组。

直接插入排序写起来比较简单,适用于数据量较少、对时间要求不高的场景。

/** 
 * 直接插入排序 
 * zhanw15 @2018/4/9 
 */  
#include<stdio.h>  
#define N 11  
  
void insort( int *a, int n)  
{  
    int i, j, temp; //临时保存a[i]  
    for( i=0; i<n; i++) {  
        temp = a[i];  
        for( j=i-1; j>=0 && a[j]>temp; j--) {  
            a[j+1] = a[j];  
        } //向前移动大于temp的元素  
          
        a[j+1] = temp;  
    }  
}  
  
int main()        
{        
    int i=0;      
    int a[N] = { 13, 24, 35, 15, 27, 39, 30, 4, 23, 21, 6};        
      
    for( i=0; i<N; i++) printf( "%d ", a[i]);        
    printf( "\n");  
      
    insort( a, N);        
    for( i=0; i<N; i++) printf( "%d ", a[i]);        
    printf( "\n");  
      
    return 0;        
}    
//output:    
// 13 24 35 15 27 39 30 4 23 21 6    
// 4 6 13 15 21 23 24 27 30 35 39

插入排序----折半插入排序 (Binary Insertion Sort)

折半插入是对直接插入排序的一种改进,即选择插入位置的时候利用折半法进行查找。

折半插入的时间复杂度仍为O(n^2),因为在这里衡量时间复杂度的基本操作为元素的移动。

/**
 * 折半插入排序
 * zhanw15 @2018/4/9
 */
#include<stdio.h>
#define N 11

int binsearch( int *a, int num, int begin, int end)
{
	int left=begin, right=end, mid;
	
	/* 折半查找第一个比num大的元素的位置 */
	while( left<=right) {
		mid = (left+right)/2;
		if( a[mid]<=num) left=mid+1;
		else right=mid-1;
	}
	return left;
}

void binsort( int *a, int n)
{
	int i, j, t, temp; //临时保存a[i]
	for( i=0; i<n; i++) {
		temp = a[i];
		t = binsearch( a, a[i], 0, i-1);
		for( j=i-1; j>=t; j--) {
			a[j+1] = a[j];
		} //向前移动大于temp的元素
		
		a[t] = temp;
	}
}

int main()      
{      
    int i=0;    
    int a[N] = { 13, 24, 35, 15, 27, 39, 30, 4, 23, 21, 6};      
	
    for( i=0; i<N; i++) printf( "%d ", a[i]);      
    printf( "\n");
	
    binsort( a, N);      
    for( i=0; i<N; i++) printf( "%d ", a[i]);      
    printf( "\n");
	
    return 0;      
}  
//output:  
// 13 24 35 15 27 39 30 4 23 21 6  
// 4 6 13 15 21 23 24 27 30 35 39 


插入排序----希尔排序(Shell's Sort)

希尔排序采用分组插入的方法对直接插入排序进行改进。若要排序的数组为n,首先选取d1<n作为增量,对于所有k%d (0≤k<n)相同的元素a[ k ] 划分为一组,在组内进行直接插入排序( 共d 个组)。然后选取第二个增量 d2<d1重复上述分组排序过程,直到最后取 dk = 1,排序结束。

不同的增量选取方式会影响到希尔排序的性能,下面以希尔增量的方式进行演示。希尔排序适用于中等输入规模的情况。

Ps:希尔增量是希尔给出的增量方式,第一个增量为n/2,之后的每个增量是前一个的1/2,最后一个增量为1。

/**
 * 希尔排序 ( 希尔增量 )
 * zhanw15 @2018/4/9
 */
#include<stdio.h>
#define N 11

void shell( int *a, int n)
{
    int d, i, j, temp;   // d为增量
	/* 增量从 n/2 开始, 到 1 结束 */
    for ( d=n/2; d>0; d/=2) {
		/* d组同时进行直接插入排序 */
		for ( i=0; i<n; i++) {
			temp = a[i];
            for ( j=i-d; j>=0 && a[j]>temp; j-=d) {
                a[j+d]=a[j];
            } // 大于要插入值的元素依次向后移
			a[j+d]=temp; // 插入元素
        }
    }
}

int main()      
{      
    int i=0;    
    int a[N] = { 13, 24, 35, 15, 27, 39, 30, 4, 23, 21, 6};      
	
    for( i=0; i<N; i++) printf( "%d ", a[i]);      
    printf( "\n");
	
	shell( a, N);      
    for( i=0; i<N; i++) printf( "%d ", a[i]);      
    printf( "\n");
	
    return 0;      
}  
//output:  
// 13 24 35 15 27 39 30 4 23 21 6  
// 4 6 13 15 21 23 24 27 30 35 39 


选择排序----简单选择排序 (Straight Select Sort)

对于数组a[1...n],对于k(k从1开始,到n结束),找出a[k...n]中最小元素min,与 a[ k ] 交换位置;

当k = n-1时,则完成排序,a[1...n] 成为有序数组。

与时间复杂度相同的冒泡和直接插入比较,移动元素数量较少,而且是已知的(n-1次交换);但需要较多次的元素比较。

/**  
 * 简单选择排序 
 * zhanw15 @2018/4/9
 */     
#include<stdio.h>    
#define N 11   

void swap( int* a, int i, int j)    
{    
    int temp=a[j];    
    a[j]=a[i];    
    a[i]=temp;    
}     

void selsort( int *a, int n)
{
	for( int i=0; i<n-1; i++) {
		int min = i;    // a[j...n]最小元素位置
		for( int j=i; j<n; j++) {
			min = a[min]<a[j]?min:j;
		}
		swap( a, i, min);
	}
}

int main()    
{    
    int i=0;  
    int a[N] = { 13, 24, 35, 15, 27, 39, 30, 4, 23, 21, 6};    

    for( i=0; i<N; i++) printf( "%d ", a[i]);    
    printf( "\n");    

    selsort( a, N);    
    for( i=0; i<N; i++) printf( "%d ", a[i]);    
    printf( "\n");    
        
    return 0;    
}
//output:
// 13 24 35 15 27 39 30 4 23 21 6
// 4 6 13 15 21 23 24 27 30 35 39


选择排序----堆排序 (Heap Sort)

堆(heap)的定义:

    对于由n个元素的序列{k1, k2, ki, …, kn}当且仅当满足下关系时,称之为堆:

    ( ki <= k2i, ki <= k2i+1)或者( ki >= k2i, ki >= k2i+1), i = 1,2,3,4...n/2

通过定义可以看出堆就是一个队列,但是它的元素有相互之间的约束。我们可以把这个队列“装进”一颗完全二叉树中,并从根节点依次编号来理解堆。

堆的几个重要性质:
    1) 堆中某节点的值总是不小于(或不大于)其子节点的值;

    2) 堆的左右子树也都是堆。

对于堆中某节点的值总是不小于其子节点的值的堆我们称为大根堆;反之则称为小根堆。

当把堆放在数组中,我们将将堆从0开始编号,则有:

    1) 对于编号为k的节点,有2*k+1和2*k+2为其子节点(2*k+2<=n)

    2) 对于编号为k的节点,有k/2-1为其父节点(k/2-1>=0)

怎么进行堆排序?

    1) 将输入进来的元素构建成一个堆;

    2) 取出堆顶(即最大值或最小值),并将堆最末尾元素放到堆顶位置;

    3) 将剩余节点重新调整为一个堆;

    4) 重复2、3两个步骤到堆空为止。

怎么进行建堆?

    首先将数据存入到一个数组中,将其看作一个完全二叉树,则其所有的叶子节点肯定是一个堆。然后在调整叶子节点的父节点使其成为一个堆,依次向上,直到堆构建完成(从下到上的一个过程)。

怎么去除堆顶并将剩余结点调整为一个堆?

    通过将堆的根节点和末尾结点交换,然后将堆的长度减一,将剩下的节点作为一个堆进行调整。从堆顶开始,比较其和左右子节点的大小,将更小(或更大)的节点调整到父节点的位置,然后继续调整根节点改变的左子树(或右子树),只到形成一个堆(从上到下的一个过程)。

优点:对数据有序性不敏感,最差时间复杂度仍为O(n*logn),建堆过程比较费时间,因此适用于数据量比较大的排序。

缺点:相对于快排速度较慢。

/**  
 * 堆排序(大根堆)   
 * zhanw15 @2018/4/7   
 */     
#include<stdio.h>    
#define N 11   

void swap( int* a, int i, int j)    
{    
    int temp=a[j];    
    a[j]=a[i];    
    a[i]=temp;    
}     
    
/* 调整begin元素位置,使其重新成为一个堆 */     
void adjust( int* a, int begin, int end)    
{    
    int son = begin*2+1;    
        
    /* 将根节点向下沉,直至完全构成一个堆 */    
    while( son<=end) {    
        if( son!=end && a[son+1]>a[son]) ++son;    
            
        if( a[son]<=a[begin]) break;    
        else swap( a, son ,begin);    
            
        begin = son;    
        son = begin*2+1;    
    }    
}    
    
void heapsort( int* a, int n)    
{    
    int last=n-1, i;    
        
    /* 建堆(大根堆)*/     
    for( i=last/2-1; i>=0; i--) {    
        adjust( a, i, last);    
    }    
    
    /* 取堆顶元素,调整堆 */     
    while( last!=0) {    
        swap( a, last, 0);    
        adjust( a, 0, --last);    
    }    
}    
    
int main()    
{    
    int i=0;  
    int a[N] = { 13, 24, 35, 15, 27, 39, 30, 4, 23, 21, 6};    
        
    for( i=0; i<N; i++) printf( "%d ", a[i]);    
    printf( "\n");    
        
    heapsort( a, N);    
    for( i=0; i<N; i++) printf( "%d ", a[i]);    
    printf( "\n");    
        
    return 0;    
}
//output:
// 13 24 35 15 27 39 30 4 23 21 6
// 4 6 13 15 21 23 24 27 30 35 39


交换排序----冒泡排序 (Bubble Sort)

对于数组a[1...n],对于k(k从1开始,到n结束),从n开始、到k结束,依次交换两相邻元素的位置,使较小的元素在前,较大的元素在后。每趟交换后,a[ k ] 总是 a[k...n]中最小的元素;

当k = n时,则完成排序,a[1...n] 成为有序数组。

冒泡排序是一种稳定排序算法,且当数组有序时,冒泡排序不需要交换任何元素就能完成排序。

缺点:当数组无需或逆序时,需要较多的元素移动才能完成排序。

/**  
 * 冒泡排序
 * zhanw15 @2018/4/9
 */     
#include<stdio.h>    
#define N 11   

void swap( int* a, int i, int j)    
{    
    int temp=a[j];    
    a[j]=a[i];    
    a[i]=temp;    
}     

void busort( int *a, int n)
{
	for( int i=0; i<n-1; i++) {
		for( int j=n-1; j>i; j--) {
			if( a[j]<a[j-1]) swap( a, j, j-1);
		}
	}
}

int main()    
{    
    int i=0;  
    int a[N] = { 13, 24, 35, 15, 27, 39, 30, 4, 23, 21, 6};    

    for( i=0; i<N; i++) printf( "%d ", a[i]);    
    printf( "\n");    

    busort( a, N);    
    for( i=0; i<N; i++) printf( "%d ", a[i]);    
    printf( "\n");    
        
    return 0;    
}
//output:
// 13 24 35 15 27 39 30 4 23 21 6
// 4 6 13 15 21 23 24 27 30 35 39


交换排序----快速排序 (Quick Sort)

快排的平均时间复杂度约为O(1.39nlogn),相比于堆排序和合并排序效率要高很多。但是最差情况下可能会出现 O(n^2);

快速排序中一趟排序的几种常用方法(皆以最后一个元素为基准):

1) 选定最后一个元素s为基准,两个指针i、j分别指向第一个和最后一个元素;

        1、向前移动 i 直到出现 a[ i ]≤s,然后交换 a[ i ] 和 s的位置;

        2、向后移动 j 直到出现 a[ j ]>s,然后交换 a[ j ] 和 s的位置;

     重复上述两个步骤直到直到 i > j 为止,此时一趟排序完成。

2)选定最后一个元素s为基准,两个指针i、j分别指向第一个和最后一个元素;

     向后移动 j 直到出现 a[ j ]>s,向前移动 i 直到出现 a[ i ]≤s,然后交换a[ i ] 和 a[ j ] 的位置;

     重复上步骤直到 i > j 为止,取消最后一次交换,并交换 a[ i ] 和 s 的位置,一趟排序完成。

3)选定最后一个元素s为基准,两个指针 i、j同时指向第一个元素;

     向前移动 j 直到出现 a[ j ]≤s,交换 a[ j ] 和 a[ i ] 的位置,并使 i++;

     重复上步骤直到 j = n为止,一趟排序完成。


 2 、3 两种方法都是先将数组分为 小于基准元素 和 大于基准元素 的两部分后,再将 基准元素 移动到中间位置的,避免了频繁的移动基准元素,因此相对 1 来说更省时。

下面的代码采用第三种方式进行快速排序。

/**  
 * 快速排序   
 * zhanw15 @2018/4/9
 */     
#include<stdio.h>    
#define N 11   
  
  
void swap( int* a, int i, int j)    
{    
    int temp=a[j];    
    a[j]=a[i];    
    a[i]=temp;    
}     

int partition( int* a, int p, int r)
{
	int i=p-1, j;	// i指向比基准小的元素末尾
	
	for( j=p; j<r; j++) {  	// 以a[r-1]为基准
		if( a[j]<=a[r-1]) swap( a, ++i, j);
	}
	
	return i;     // 返回中间值
}

/* 快排    注明: 不存在a[r]元素, 此写法仅为使用方便 */
void qsort( int* a, int p, int r)
{
	if( p>=r-1) return;
	
	int q = partition( a, p, r);
	qsort( a, p, q);
	qsort( a, q+1, r);
}

int main()    
{    
    int i=0;  
    int a[N] = { 13, 24, 35, 15, 27, 39, 30, 4, 23, 21, 6};    
        
    for( i=0; i<N; i++) printf( "%d ", a[i]);    
    printf( "\n");    
        
    qsort( a, 0, N);    
    for( i=0; i<N; i++) printf( "%d ", a[i]);    
    printf( "\n");    
        
    return 0;    
}
//output:
// 13 24 35 15 27 39 30 4 23 21 6
// 4 6 13 15 21 23 24 27 30 35 39

归并排序 (Merge Sort)

归并排序是分治策略的一种应用,归并排序分为两个过程:分解(利用递归将数组分解成不想交的两部分,直到分解到不能分解为止)、合并。

合并的过程:

1)申请数组空间用于存放合并后的数组元素;

2)两个指针i、j分别指向要合并的两个数组a、b头;

3)比较a[ i ] 和 b[ j ] 的大小,将较大的元素放入合并空间并使其指针+1.

重复3)操作直至数组合并完成,并重复1)、2)、3)操作直到归并完成,则排序完成。


归并排序具有稳定性,但需要消耗较大的辅助空间;适用于对稳定性有要求的排序。

/**  
 * 归并排序
 * zhanw15 @2018/4/9
 */
#include<stdio.h>
#define N 11

/* 对数组进行合并 */
void merge( int* a, int p, int q, int r)
{
	int temp[r-p]; //开辟存储合并后元素空间
	int i=p, j=q, k=0;  //三个指针分别指向三个数组
	
	while(i<q && j<r) { //将较小的元素放入合并空间
		if( a[i]<=a[j]) temp[k++]=a[i++];
		else temp[k++]=a[j++];
	}

	/* 将剩余元素放入合并空间 */
	while( i<q) temp[k++]=a[i++];
	while( j<r) temp[k++]=a[j++];
	
	for( i=0; i<r-p; i++) { //将合并后的元素放入数组a
		a[p+i] = temp[i];
	}
}

void mersort( int* a, int p, int r)
{
	if( p>=r-1) return;
	
	/* 先排左半部分,再排右半部分,再合并 */ 
	int q = ( p+r)/2;
	mersort( a, p, q);    // 分解
	mersort( a, q, r);    // 分解
    
	merge( a, p, q, r);   // 合并
}

int main()
{
    int i=0;
    int a[N] = { 13, 24, 35, 15, 27, 39, 30, 4, 23, 21, 6};
        
    for( i=0; i<N; i++) printf( "%d ", a[i]);
    printf( "\n");
    
    mersort( a, 0, N);
    for( i=0; i<N; i++) printf( "%d ", a[i]);
    printf( "\n");
    
    return 0;
}
//output:  
// 13 24 35 15 27 39 30 4 23 21 6  
// 4 6 13 15 21 23 24 27 30 35 39 


计数排序 (Counting sort)

计数排序的基本思想:

对于输入中的一个元素x,确定比其小的元素个数c,那么将x放在第c+1个位置,排序完成。注意有可能多个元素有相同的值,我们记录所有值出现的次数,然后确定元素的位置。

排序的基本过程:

1)对于一个输入为a[ 1...n ] 的数组,首先寻找其最大值max和最小值min,建立一个数组c[ 0...(max-min) ] 大小临时存储空间,来记录每个值出现的次数。

2)遍历数组a中的所有元素,将他们值出现的次数记录到c数组中。方法:使用c[a[k]-min]++,将值为x的元素投影到c数组的x-min位置。

3)确定值为k元素所在的位置。值为 k 元素的个数记录在了c[k-min]中,因此使用{ for i=1 to max-min, c[i] += c[i-1] } 后,c[i] 的值为 小于等于 i+min 的元素的个数。

4) 使用数组b[ 1...n ]记录排序后的数组a 。使用{ for i=n to 0,  b[--c[a[i]-min]] = a[i] }来将元素a[i]调整到特定的位置(每次c[]--,表示a[i]元素已调整好位置,剩下相同值的元素-1)。若有必要,将b数组重新赋值给a数组。

注意:由于是从a数组最后一个元素开始调整的,若还有相同值的元素则会被放在c[]--的位置,因此计数排序是稳定的。

/** 
 * 计数排序
 * zhanw15 @2018/4/9 
 */  
#include<stdio.h>  
#define N 11  
  
void countsort( int *a, int n)
{
	int min, max=min=a[0];
	for( int i=1; i<n; i++) {
		min = min<a[i]?min:a[i];
		max = max>a[i]?max:a[i];
	}
	
	int b[n], c[max-min+1];  // c数组记录元素出现次数
	for( int i=0; i<=max-min; c[i]=0,i++);   //清零
	
	for( int i=0; i<n; i++) c[a[i]-min]++;   //计数
	
	for( int i=1; i<=max-min; i++) {
		c[i] +=c[i-1];          //确定值为i元素所在位置
	}
	
	for( int i=n-1; i>=0; i--) {
		b[--c[a[i]-min]] = a[i];  // 将a[i]调整到相应位置
	}
	
	for( int i=0; i<n; a[i]=b[i],i++);  // 回调给a
}
  
int main()        
{        
    int i=0;      
    int a[N] = { 13, 24, 35, 15, 27, 39, 30, 4, 23, 21, 6};        
      
    for( i=0; i<N; i++) printf( "%d ", a[i]);        
    printf( "\n");  
      
    countsort( a, N);        
    for( i=0; i<N; i++) printf( "%d ", a[i]);        
    printf( "\n");  
      
    return 0;        
}    
//output:    
// 13 24 35 15 27 39 30 4 23 21 6    
// 4 6 13 15 21 23 24 27 30 35 39

基数排序(Radix Sort)

给定一系列d位10进制数进行排序,当然我们可以使用计数排序对其进行排序,但是当数列中的max和min相差很大时,时间复杂度O( k+n ) 中的 k = max-min+1 将变的很大,将带来很大的时空开销。我们可以采用另一种策略,将数列中的数 x 看作: x = x0*10^0 + x1*10 + x2*10^2 + . . . + xd*10^d,那么先对所有数字的最低位x0进行排序,然后对x1进行排序,因此类推,直到d位全部排序完成,那么这系列数也排序完成。我们对每位上的数字使用计数排序,因为是十进制,那么计数排序的时间复杂度为O( 10+n ),共进行d轮排序,则整体时间复杂度为O( d( 10+n) )。

上述方法被称为基数排序,10就是上述排序中的基数。基数排序可以用作以任何数为基数的排序上,例如对字符串进行排序,可选用基数为26,先排序最后一位的字母,再向前依次排序。

引理(算法导论引理8.4):给定一个b位数和任何正整数r≤b,如果RADIX-SORT使用的稳定排序算法对数据取值区间是0到k的输入进行排序耗时Θ( n+k )  (这里即使用计数排序),那么它就可以在Θ( (b/r)(n+2^r) )  时间内完成排序(即化为r为基数的基数排序)。

通过上述引理,可以通过选取不同的基数将Θ( (b/r)(n+2^r) )达到时间复杂度最小化。

  • 对于d<logn时,对任意r≤d结果都为Θ( (b/r) n ),那么选择r=d使时间复杂度最低,为Θ( n );
  • 对于d≥logn时,选择r=logn时会获得最低的时间复杂度,Θ( bn/logn );----自行证明
/**
 * 基数排序
 * zhanw15 @2018/4/9
 */
#include<stdio.h>
#include<math.h>
#define N 11

/* 获取数组中最大元素位数 radix为基数 */
int getDigit( int radix, int *a, int n)
{
	int d = 0;   
	for( int i=0; i<n; i++) {
		while( a[i]>=pow(radix,d)) d++;
	}
	return d;
}

/* 获取数字以radix为基数的第k位的值 */
int getNum( int num, int radix, int k) {
	return num%(int)pow(radix,k)/(int)pow(radix,k-1);
}

void radixsort( int *a, int n)
{
	int radix=10, d=getDigit( radix, a, n);
	
	int b[n], c[radix];   // b数组暂存排序结果,c记录元素出现次数
	for( int i=0; i<d; i++) {   // 进行 d 轮计数排序
	
		for( int j=0; j<radix; j++) c[j]=0;             // 清零
		for( int j=0; j<n; j++) c[getNum(a[j],radix,i+1)]++;  // 计数
		for( int j=1; j<radix; j++) c[j] += c[j-1];     // 确定位置
		for( int j=n-1; j>=0; j--) b[--c[getNum(a[j],radix,i+1)]]=a[j];  // 移动位置
		for( int j=0; j<n; j++) a[j] = b[j];            // 回调
	}
}

int main()      
{      
    int i=0;         // 相比于其它排序改动了一下数据大小
    int a[N] = { 133, 224, 35, 175, 27, 39, 30, 114, 293, 21, 1256};      
	
    for( i=0; i<N; i++) printf( "%d ", a[i]);      
    printf( "\n");
	
    radixsort( a, N);      
    for( i=0; i<N; i++) printf( "%d ", a[i]);      
    printf( "\n");
	
    return 0;      
}  
// output:  
// 133 224 35 175 27 39 30 114 293 21 1256
// 21 27 30 35 39 114 133 175 224 293 1256


桶排序(Bucket Sort)

假设输入数据服从均匀分布,那么我们可以将数据投影到 [ 0, 1) 区间上,并将 [ 0, 1) 这段区间划分为n个大小相同的子区间(这些子区间被称为)。我们将落入相同桶的元素进行排序,然后依次收集每个桶中的数据,最终得到排序好的结果。

假设每个桶的大小为di,每个桶内执行插入排序,那么桶排序时间复杂度为:O(n)  + nO(E(d^2))

其中O(n) 为将数据放入桶的时间复杂度,插入排序时间复杂度为O(d^2),n个桶的插入排序为nO(E(d^2)),其中E()表示期望。当输入数据均匀分布时,E(d^2) = 2-1/n,那么桶排序的时间复杂度为:O(n)

【算法导论】 内部排序算法总结

PS:看网上很多人把桶排序和基数排序混为一谈,基数排序是从最小基数项开始,到最大基数项结束,对数组整体进行排序,然后得到有序序列。而桶排序是先通过投影将n个数分组,然后再组内对数据进行排序,最后进行合并。基数排序的思想是对整体部分(基数项)进行排序而桶排序的思想是将整体分割,分别进行排序。

相关标签: 内部排序