排序算法
基本概念
void X_Sort(ElementType A[], int N)
- N是正整数
- 只讨论基于比较的排序(<、=、>有定义)
- 只讨论内部排序
- 稳定性:任意两个相等的数据,排序前后的相对位置不发生变化
- 没有一种排序是任何情况下都表现最好的
简单排序
冒泡排序
每一趟冒泡可以确定至少一个元素的位置,确定一个最大值,也就是说,排序过程中序列尾部是排好序的。冒泡排序是稳定的算法。
void Bubble_Sort(ElementType A[], int N){
int P,i;
for(P=N-1;P>=0;P--){
int flag=0;
for(i=0;i<P;i++){ //一趟冒泡
if(A[i]>A[i+1]){
Swap(A[i],A[i+1]);
flag=1; //标识发生了交换
}
}
if(flag==0) //全程无交换
break;
}
}
最好情况:顺序 T=O(N),最坏情况:逆序 T=O(N^2)
插入排序
从1号元素开始,依次取出元素与其前面的元素比较,将其插入到合适的位置,排序过程中序列前部是排好序的。插入排序是稳定的算法。
void Insertion_Sort(ElementType A[], int N){
int P,i;
ElementType Tmp;
for(P=1;P<N;P++){
Tmp=A[P]; //取出未排序序列中的第一个元素
for(i=P;i>0&&A[i-1]>Tmp;i--)
A[i]=A[i-1]; //依次与已排序序列中的元素比较并右移
A[i]=Tmp; //放入适合的位置
}
}
最好情况:顺序 T=O(N),最坏情况:逆序 T=O(N^2)
简单排序的平均时间复杂度为O(N^2)。
希尔排序
希尔排序是由Donald Shell提出的,所以叫希尔排序。
要进行希尔排序,首先要定义增量序列Dm > Dm-1 > ··· > D1 = 1,然后就在待排序列上对每个增量序列Dk进行“Dk-间隔”排序(k=m,m-1,···,1)。其中要注意的是,“Dk-间隔”有序的序列,在执行“Dk-1-间隔”排序后,仍然是“Dk-间隔”有序的。
那么,希尔排序的关键就是确定增量序列,之后的每个“Dk-间隔”排序就可以采用简单排序来做。
- 原始希尔排序 Dm=[N/2], Dk=[Dk+1/2] 向下取整
void Shell_Sort(ElementType A[], int N){
for(D=N/2;D>0;D/=2){ //希尔增量序列
for(P=D;P<N;P++){ //插入排序
Tmp=A[P];
for(i=P;i>=D&&A[i-D]>Tmp;i-=D)
A[i]=A[i-D];
A[i]=Tmp;
}
}
}
最坏情况:T=O(N^2)
但是这样选取增量序列有个问题,增量元素不互质,那么小增量可能根本不起作用。
这样也就产生了跟多增量序列
-
Hibbard增量序列
- Dk=2^k-1 —— 相邻元素互质
- 最坏情况:T=O(N^3/2)
- 猜想:Tavg=O(N^5/4)
-
Sedgewick增量序列
- {1,5,19,41,109,···} —— 9×4i-9×2i+1或4i-3×2i+1
- 猜想:Tavg=O(N^7/6), Tworst=O(N^4/3)
void Shell_Sort(ElementType A[], int N){
//希尔排序 - 用Sedgewick增量序列
int Si,D,P,i;
ElementTypde Tmp;
//只列出一小部分增量
int Sedgewick[]={929,505,209,109,41,19,5,1,0};
for(Si=0;Sedgewick[Si]>=N;Si++)
; //初始的增量Sedgewick[Si]不能超过待排序列的长度,把超过的部分滤除
for(D=Sedgewick[Si];D>0;D=Sedgewick[++Si]){
for(P=D;P<N;P++){ //插入排序
Tmp=A[P];
for(i=P;i>=D&&A[i-D]>Tmp;i-=D)
A[i]=A[i-D];
A[i]=Tmp;
}
}
}
堆排序
堆排序是利用最小堆的特性来对待排序列进行排序。
- 算法1
用待排序列中的元素建最小堆,再从堆中返回最小元素到新的数组中,最后把新的排好序的数组复制到原序列中。
void Heap_Sort(ElementType A[], int N){
BuildHeap(A); //O(N)
for(i=0;i<N;i++)
TmpA[i]=DeleteMin(A); //O(logN)
for(i=0;i<N;i++) //O(N)
A[i]=TmpA[i];
}
T=O(NlogN),需要额外O(N)的空间,并且复制元素需要时间。
- 算法2
用待排序列中的元素建最大堆,再对堆直接操作,把根结点最大值和堆的最后一个元素交换,则确定了最大值的位置,然后对其余元素进行调整(下滤),依次进行上述操作。相比算法1不需要额外的空间。
void Swap(ElementType *a, ElementType *b){
ElementType t=*a;
*a=*b;
*b=t;
}
void PercDown(ElementType A[], int p, int N){
//改编自堆的下滤操作PrecDown(MaxHeap H, int p),参考堆的那篇博客
//将N个元素的数组中以A[p]为根的子堆调整为最大堆
int Parent,Child;
ElementType X;
X=A[p]; //取出根结点的值
for(Parent=p;Parent*2+1<N;Parent=Child){
Child=Parent*2+1;
if((Child!=N-1)&&(A[Child]<A[Child+1]))
Child++; //Child指向左右子结点的最大者
if(X>=A[Child])
break; //找到了合适位置
else
A[Parent]=A[Child]; //下滤
}
A[Parent]=X;
}
void Heap_Sort(ElementType A[], int N){
int i;
for(i=N/2-1;i>=0;i--)
PercDown(A,i,N); //建最大堆,从最后一个父结点开始往前,依次下滤
for(i=N-1;i>0;i--){
Swap(&A[0],&A[i]); //交换最大值和末尾的值,相当于删除最大堆顶
PercDown(A,0,i); //对剩余子堆进行调整(下滤)
}
}
堆排序处理N个不同元素的随机排列的平均比较次数是2NlogN-O(NloglogN)。虽然堆排序给出最佳平均复杂度,但实际效果不如用Sedgewick增量序列的希尔排序。
归并排序
归并排序先将待排序列分成多个序列,每个序列各自排序,最后多个有序序列合并成一个有序序列。归并算法的核心便是有序子列的归并。
void Merge(ElementType A[], ElementType TmpA[], int L, int R, int RightEnd){
//L=左边起始位置,R=右边起始位置,RightEnd=右边终点位置
//将有序的A[L]-A[R-1]和A[R]-A[RightEnd]归并成一个有序序列
int LeftEnd,NumElements,Tmp;
int i;
LeftEnd=R-1; //左边重点位置
Tmp=L; //有序序列的起始位置
NumElements=RightEnd-L+1;
while(L<=LeftEnd&&R<=RightEnd){
if(A[L]<=A[R])
TmpA[Tmp++]=A[L++]; //将左边元素复制到TmpA
else
TmpA[Tmp++]=A[R++]; //将右边元素复制到TmpA
}
while(L<=LeftEnd)
TmpA[Tmp++]=A[L++]; //直接复制左边剩下的
while(R<=RightEnd)
TmpA[Tmp++]=A[R++]; //直接复制右边剩下的
for(i=0;i<NumElements;i++,RightEnd--)
A[RightEnd]=TmpA[Rightend]; //将有序的TmpA复制回A
}
归并排序有两种实现方式,递归算法和非递归算法。
递归算法
归并排序的递归算法采用的是分而治之的思想。
void MSort(ElementType A[], ElementType TmpA[], int L, int RightEnd){
//核心递归排序函数
int Center;
if(L<RightEnd){
Center=(L+RightEnd)/2;
MSort(A, TmpA, L, Center); //递归解决左边
MSort(A, TmpA, Center+1, RightEnd); //递归解决右边
Merge(A, TmpA, L, Center+1, RightEnd); //合并两段有序序列
}
}
T=O(NlogN),是稳定的算法。
统一函数接口
void Merge_Sort(ElementType A[], int N){
ElementType *TmpA;
TmpA=(ElementType *)malloc(N*sizeof(ElementType));
if(TmpA!=NULL){
MSort(A, TmpA, 0, N-1);
free(TmpA);
}
else
printf("空间不足");
}
非递归算法
归并排序的非递归算法由循环实现。
void Merge_pass(ElementType A[], ElementType TmpA[], int N, int length){ //length是当前有序子序列的长度
//两两归并相邻有序子序列
int i,j;
for(i=0;i<=N-2*length;i+=2*length)
Merge(A, TmpA, i, i+length, i+2*length-1);
if(i+length<N) //归并最后2个子列
Merge(A, TmpA, i, i+length, N-1);
else //最后只剩1个子列
for(j=i;j<N;j++)
TmpA[j]=A[j];
}
void Merge_Sort(ElementType A[], int N){
int length;
ElementType *TmpA;
length=1; //初始化子序列长度
TmpA=(ElementType *)malloc(N*sizeof(ElementType));
if(TmpA!=NULL){
while(length<N){
Merge_pass(A, TmpA, N, length);
length*=2;
Merge_pass(TmpA, A, N, length);
length*=2;
}
free(TmpA);
}
else
printf("空间不足");
}
快速排序
快速排序采用分而治之的思想,先选取一个主元pivot,比主元小的元素放在前面,而比主元大的元素放在后面。问题就是如何选取主元,这里有个方法是取头、中、尾的中位数,比如8、12、3的中位数是8。另外,如果数据规模较小,快速排序的效率可能还不如插入排序快,所以,当递归的数据规模充分小,则停止递归,直接调用简单排序。
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]);
//此时,A[Left]<=A[Center]<=A[Right]
Swap(&A[Center],&A[Right-1]); //将基准Pivot藏到右边
//只需要考虑A[Left+1],···,A[Right-2]
return A[Right-1];
}
void Qsort(ElementType A[], int Left, int Right){
int Pivot,Cutoff,Low,High;
if(Cutoff<=Right-Left){ //如果序列元素充分多,则进入快速排序
Pivot=Median3(A,Left,Right); //选取主元pivot
Low=Left;
High=Right-1;
while(1){ //将序列中比基准小的移到基准左边,大的移到基准右边
while(A[++Low]<Pivot);
while(A[--High]>Pivot);
if(Low<High)
Swap(&A[Low],&A[High]);
else
break;
}
Swap(&A[Low],&A[Right-1]); //将基准放到正确的位置
Qsort(A,Left,Low-1); //递归解决左边
Qsort(A,Low+1,Right); //递归解决右边
}
else
Insertion_Sort(A+Left,Right-Left+1); //元素太少,用插入排序
}
void Quick_Sort(ElementType A[], int N){ //统一接口
Qsort(A,0,N-1);
}
如果pivot选取的好,那么快速排序的时间复杂度为T=O(NlogN)。
基数排序
/* 假设元素最多有MaxDigit个关键字,基数全是同样的Radix */
#define MaxDigit 4
#define Radix 10
typedef struct Node *PtrToNode;
struct Node{ //桶元素结点
int key;
PtrToNode next;
};
struct HeadNode{ //桶头结点
PtrToNode head, tail;
};
typedef struct HeadNode Bucket[Radix];
int GetDigit(int X, int D){
/* 默认次位D=1, 主位D<=MaxDigit */
int d, i;
for(i=1; i<=D; i++){
d = X % Radix;
X /= Radix;
}
return d;
}
void LSDRadixSort(ElementType A[], int N){
//基数排序-次位优先
int D, Di, i;
Bucket B;
PtrToNode tmp, p, List = NULL;
for(i=0; i<Radix; i++) /* 初始化每个桶为空链表 */
B[i].head = B[i].tail = NULL;
for(i=0; i<N; i++){ /* 将原始序列逆序存入初始链表List */
tmp = (PtrToNode)malloc(sizeof(struct Node));
tmp->key = A[i];
tmp->next = List;
List = tmp;
}
/* 下面开始排序 */
for(D=1; D<=MaxDigit; D++){ /* 对数据的每一位循环处理 */
/* 下面是分配的过程 */
p = List;
while(p){
Di = GetDigit(p->key, D); /* 获得当前元素的当前位数字 */
/* 从List中摘除 */
tmp = p; p = p->next;
/* 插入B[Di]号桶尾 */
tmp->next = NULL;
if(B[Di].head == NULL)
B[Di].head = B[Di].tail = tmp;
else{
B[Di].tail->next = tmp;
B[Di].tail = tmp;
}
}
/* 下面是收集的过程 */
List = NULL;
for(Di=Radix-1; Di>=0; Di--){ /* 将每个桶的元素顺序收集入List */
if(B[Di].head){ /* 如果桶不为空 */
/* 整桶插入List表头 */
B[Di].tail->next = List;
List = B[Di].head;
B[Di].head = B[Di].tail = NULL; /* 清空桶 */
}
}
}
/* 将List倒入A[]并释放空间 */
for(i=0; i<N; i++){
tmp = List;
List = List->next;
A[i] = tmp->key;
free(tmp);
}
}
//基数排序-主位优先
void MSD(ElementType A[], int L, int R, int D){
/* 核心递归函数: 对A[L]...A[R]的第D位数进行排序 */
int Di, i, j;
Bucket B;
PtrToNode tmp, p, List = NULL;
if (D==0) return; /* 递归终止条件 */
for(i=0; i<Radix; i++) /* 初始化每个桶为空链表 */
B[i].head = B[i].tail = NULL;
for(i=L; i<=R; i++){ /* 将原始序列逆序存入初始链表List */
tmp = (PtrToNode)malloc(sizeof(struct Node));
tmp->key = A[i];
tmp->next = List;
List = tmp;
}
/* 下面是分配的过程 */
p = List;
while (p){
Di = GetDigit(p->key, D); /* 获得当前元素的当前位数字 */
/* 从List中摘除 */
tmp = p; p = p->next;
/* 插入B[Di]号桶 */
if(B[Di].head == NULL) B[Di].tail = tmp;
tmp->next = B[Di].head;
B[Di].head = tmp;
}
/* 下面是收集的过程 */
i = j = L; /* i, j记录当前要处理的A[]的左右端下标 */
for(Di=0; Di<Radix; Di++){ /* 对于每个桶 */
if(B[Di].head){ /* 将非空的桶整桶倒入A[], 递归排序 */
p = B[Di].head;
while(p){
tmp = p;
p = p->next;
A[j++] = tmp->key;
free(tmp);
}
/* 递归对该桶数据排序, 位数减1 */
MSD(A, i, j-1, D-1);
i = j; /* 为下一个桶对应的A[]左端 */
}
}
}
void MSDRadixSort(ElementType A[], int N){ //统一接口
MSD(A, 0, N-1, MaxDigit);
}
排序算法的比较
排序算法 | 平均时间复杂度 | 最坏情况下时间复杂度 | 额外空间复杂度 | 稳定性 |
---|---|---|---|---|
简单选择排序 | O(N^2) | O(N^2) | O(1) | 不稳定 |
冒泡排序 | O(N^2) | O(N^2) | O(1) | 稳定 |
直接插入排序 | O(N^2) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N^d) | O(N^2) | O(1) | 不稳定 |
堆排序 | O(NlogN) | O(NlogN) | O(1) | 不稳定 |
快速排序 | O(NlogN) | O(N^2) | O(logN) | 不稳定 |
归并排序 | O(NlogN) | O(NlogN) | O(N) | 稳定 |
基数排序 | O(P(N+B)) | O(P(N+B)) | O(N+B) | 稳定 |