目录
排序的定义:对任意连续有限序列内的元素进行重新排列,使得该序列按关键字非递减或非递增。
排序的稳定性:待排序序列中存在两个不同位置的元素 且 x在y 前面 ,若排序之后,仍然x在y前面,则称该排序算法是稳定的。
九大排序性质一览表
注意:本文排序时所有数组下标均从1开始使用。所有叙述及代码以从小到大排序为例。
1.直接插入排序
【思路】
设 a[ 1...i-1 ] 有序,接下来将 a[i] 移动到 [ 1...i-1 ] 之间合适的位置,使得(满足此处使得排序稳定),其中。
【步骤】
1. 初始a[1]单独有序,若a[2]<a[1],则将a[2]移动到a[1]前面,否则不动
2. a[ 1...i-1 ] 已有序,用变量temp暂存a[i],然后依次考虑i-1, i-2 ... x, y,后移一步,直到有
3. 将temp填到y位置
【复杂度分析】
时间复杂度:最好情况(原序列已有序)为O(n),最坏情况(原序列倒序)为
空间复杂度:仅使用了常数个辅助变量,故O(1)
【代码】
//直接插入排序
void InsertSort(int a[],int n)
{
for(int i=2;i<=n;i++)
{
int j,temp=a[i]; //暂存a[i]
for(j=i-1;j>=1&&a[j]>temp;j--) //后移,考虑a[j]移到a[j+1]
a[j+1]=a[j];
a[j+1]=temp; //填空
}
}
2.折半插入排序
【思路】
与直接插入排序基本类似,仅增加了折半查找的优化。
设 a[ 1...i-1 ] 有序,则可使用折半查找法快速找到ai的合适位置,使得(满足此处使得排序稳定),其中。
但是折半查找仅仅减少了比较次数,元素依然需要依次移动,所以在数据量不太大的情况下,算法效率与直接插入法相差不大。
【步骤】
1. 初始a[1]单独有序,若a[2]<a[1],则将a[2]移动到a[1]前面,否则不动
2. a[ 1...i-1 ] 已有序,用变量temp暂存a[i],然后依次考虑i-1, i-2 ... x, y,后移一步,直到有
3. 将temp填到y位置
【复杂度分析】
时间复杂度:最好情况(原序列已有序)为O(n),最坏情况(原序列倒序)为
空间复杂度:仅使用了常数个辅助变量,故O(1)
【代码】
void BiInsertSort(int a[],int n)
{
for(int i=2;i<=n;i++)
{
int L=1,R=i,mid,temp=a[i];
while(L<R) //按递增有序,找到大于a[i]的第一个位置
{
mid=(L+R)>>1;
if(a[mid]>a[i])R=mid;
else L=mid+1;
} //查找完成,有a[R-1]<=a[i]<a[R],即a[i]要放在a[R]前面
for(int j=i-1;j>=R;j--)
a[j+1]=a[j];
a[R]=temp; //填空
}
}
3.希尔排序
【思路】
又名 缩小增量排序。
设有严格递减的增量序列,其中。然后对原序列进行 t 次排序操作。
对于第 i 次排序操作,所有相隔项的元素为一组,各组内进行直接插入排序。例如,序列,,则为一组,为一组。
该算法的效果很大程度上取决于增量序列的设定,希尔提出了较好的增量序列
【步骤】
1. 选取增量序列,使用上述希尔提出的增量序列。遍历增量序列
2. 对于第i次排序,增量为,依次对同组的元素进行插入排序
【复杂度分析】
时间复杂度:没有标准答案,当n在某个特定范围内,约为,最坏情况为
空间复杂度:仅使用了常数个辅助变量,故O(1)
【代码】
void ShellSort(int a[],int n)
{
for(int di=n/2;di>=1;di>>=1) //增量序列
for(int i=di+1;i<=n;i++) //前di个为每一组的第一个,组内有序,故始于第二个
{
int j,temp=a[i];
for(j=i-di;j>=1&&a[j]>temp;j-=di) //后移,考虑a[j]移到a[j+di]
a[j+di]=a[j];
a[j+di]=temp; //填空
}
}
4.冒泡排序
【思路】
顾名思义。对于乱序序列,首先将最大数一步一步移动到末尾(第一次冒泡);其次将次大数移到末尾的前一个(第二次冒泡);以此类推,共须n-1次冒泡。移动过程中,相邻元素大小逆序时交换。
【步骤】
1. 对于第i次冒泡,必然已有i-1大数排在末尾,故遍历前n-(i-1)个数,将最大值移到第n-(i-1)位置
2. 冒泡n-1次,序列便有序
【重要优化】
1. 设标志位。对于第i次冒泡,设标志位flag=false; 本次冒泡过程中只要发生过元素移动(即交换),则修改flag=true; 若本次冒泡结束后flag=false; 则说明序列已有序,算法可以结束。
2. 双向冒泡。既然上述是把大数移动到末尾,当然把小数移动到开头也是对的,故可双向同时进行,向后冒一次泡,顺便向左冒一次泡。但该优化对算法效率没有明显改善,我不喜欢,下面代码中没有使用。
【复杂度分析】
时间复杂度:最好情况(原序列有序)为,最坏情况(原序列逆序)为
空间复杂度:仅使用了常数个辅助变量,故O(1)
【代码】
void BubbleSort(int a[],int n)
{
for(int i=1;i<n;i++)
{
for(int j=1;j<n-(i-1);j++)
{
if(a[j]>a[j+1])
a[j]^=a[j+1]^=a[j]^=a[j+1]; //位运算交换a[j]与a[j+1],仅适用于整型
}
}
}
5. 快速排序
【思路】
也许快速排序在于快吧。快排采用分治思想。
对于序列A[ L,...., R ],不妨将其一分为二,满足 左半边所有值 < 右半边组所有值,则左右两边之间是有序的;进而如果左半边有序,右半边也有序,那么整个序列就有序了。怎样使左右两边各自有序呢? 让左右两边各自再去一分为二。最终,总会被分到只有一个元素,而一个元素必然是有序的,再往回想...
【步骤】
1. 对数组 a[ L, R ] 内进行排序,随机选一个中分值a[mid]。注:严蔚敏奶奶的代码总以第一个数为中分值。
2. 根据中分值,将小于它的数往左移,大于它的数往右移,最后将a[mid]放在中间,则左边任意值<=a[mid]<=右边任意值
3. 将左右两边分别进行步骤1
4. 若L=R,则表示单个元素,有序,不再执行步骤1。
【复杂度分析】
时间复杂度:最好情况(原序列随机乱序)为,最坏情况(中分值总选最小或最大值)为
空间复杂度:因递归,平均情况下,最坏情况下O(n)
【代码1-递归(参考严蔚敏奶奶的代码)】
void QuickSort(int a[],int L,int R)
{
if(L<R)
{
int temp=a[L],low=L,high=R; //取第一个数为中分值
while(low<high)
{
while(low<high&&a[high]>=temp)--high;
a[low]=a[high];
while(low<high&&a[low]<=temp)++low;
a[high]=a[low];
}
a[low]=temp; //中分值;下面递归就没有必要放进两边了
QuickSort(a,L,low-1);
QuickSort(a,low+1,R);
}
}
【代码2-非递归(借助队列,模拟二叉树层次遍历)】
void QuickSortQueue(int a[],int n)
{
int queue[n*4],front=0,rear=0; //模拟队列
queue[rear++]=1; queue[rear++]=n; //入队两个数 区间端点
while(front<rear)
{
int L=queue[front++],R=queue[front++]; //取端点
int low=L,high=R,temp=a[low]; //取第一个数为中分值
while(low<high)
{
while(low<high&&a[high]>=temp)--high;
a[low]=a[high];
while(low<high&&a[low]<=temp)++low;
a[high]=a[low];
}
a[low]=temp;
if(L<low){queue[rear++]=L;queue[rear++]=low-1;} //子端点入队
if(low<R){queue[rear++]=low+1;queue[rear++]=R;}
}
}
【代码3-非递归(借助栈,模拟二叉树先序遍历)】
void QuickSortStack(int a[],int n)
{
int stack[n*4],top=0; //模拟栈
stack[top++]=1; stack[top++]=n; //入栈两个数 区间端点
while(top>0)
{
int R=stack[--top], L=stack[--top]; //先出栈的是R
int low=L,high=R,temp=a[low];
while(low<high)
{
while(low<high&&a[high]>=temp)--high;
a[low]=a[high];
while(low<high&&a[low]<=temp)++low;
a[high]=a[low];
}
a[low]=temp;
if(low<R){stack[top++]=low+1; stack[top++]=R;}
if(L<low){stack[top++]=L; stack[top++]=low-1;}
}
}
【代码4-单链表的快排】
#include<stdio.h>
#include<stdlib.h>
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
//带有头结点的单链表快速排序算法
void LQSort(LinkList h,LinkList end){
//h所指结点不参与排序,h后继点作为分界结点
//end结点不参与排序,作为结束条件
//相当于开区间(h,end)排序
if(h->next==end)return;
LinkList pre=h, now=h->next;
while(now!=end){
if(now->data < h->next->data)
{
pre->next = now->next;
now->next = h->next;
h->next = now;
//上三句使用头插法将结点now转移到左边
now = pre->next;
}else{ //now本身就在右边,无需移动
pre = now;
now = now->next;
}
}
LQSort(h,h->next);
LQSort(h->next,end);
}
int main()
{
LinkList head = (LinkList)malloc(sizeof(LNode));
head->next = NULL;
int num;
printf("请输入若干个数字,输入0结束:\n");
scanf("%d",&num);
while(num!=0){
LinkList p = (LinkList)malloc(sizeof(LNode));
p->data = num;
p->next = head->next;
head->next = p;
scanf("%d",&num);
}
for(LinkList p=head->next;p!=NULL;p=p->next){
printf("%3d ",p->data);
}
printf("\n\n");
LQSort(head,NULL); //排序
for(LinkList p=head->next;p!=NULL;p=p->next){
printf("%3d ",p->data);
}
printf("\n\n");
}
6.二路归并排序
【思路】
归并算法也是分治思想,但也可以说是快排的逆向思维。
若a[ L...M ]和a[ M+1...R ]分别有序,则可通过循环将两序列合并为有序,时间复杂度为O(n)。
如何将两个有序序列合并为一个有序序列?请查看文末附录
【步骤】
1. 初始状态,每个单个元素各自有序
2. 将两个元素合并为有序,作为一组
3. 将两组合并为一组,合并之后有序。以此类推,直到整个序列有序
【复杂度分析】
时间复杂度:
空间复杂度:合并过程使用了辅助数组,故O(n)
【代码-递归】
int sup[100];
void MergeSort(int a[],int L,int R)
{
if(L>=R)return;
int M=(L+R)/2;
MergeSort(a,L,M);
MergeSort(a,M+1,R);
int top=0,i=L,j=M+1;
while(i<=M&&j<=R)
{
while(i<=M&&a[i]<=a[j])sup[top++]=a[i++];
while(j<=R&&a[j]<=a[i])sup[top++]=a[j++];
}
while(i<=M)sup[top++]=a[i++];
while(j<=R)sup[top++]=a[j++];
for(i=L;i<=R;i++)a[i]=sup[i-L]; //复制回原数组a
}
【代码-非递归】
int sup[100];
void MergeSort2(int a[],int n)
{
for(int k=1;k<n;k<<=1)
{
for(int s=1;s<=n;s+=k*2)
{
if(n<s+k)break; //第二段空
int i=s,j=s+k,R1=s+k-1,R2=min(s+2*k-1,n),top=0;
while(i<=R1&&j<=R2)
{
while(i<=R1&&a[i]<=a[j])sup[top++]=a[i++];
while(j<=R2&&a[j]<=a[i])sup[top++]=a[j++];
}
while(i<=R1)sup[top++]=a[i++];
while(j<=R2)sup[top++]=a[j++];
for(i=s;i<=R2;i++)a[i]=sup[i-s]; //复制回原数组a
}
}
}
7.简单选择排序
【思路】
设 a[ 1...i-1 ] 有序且为序列a最小的i-1个数,接下来从i~n中选出最小数放在i位置,则a[ 1...i ]有序。
换个说法就是:依次给位置 i 找到其排序后应该存放的元素
【步骤】
1. 初始状态乱序,找到最小数放a[1]位置。
2. 从剩下的数中找到最小值放在第2位置;以此类推,直到n个位置都放上正确的数
【复杂度分析】
时间复杂度:最好情况(原序列有序)为,最坏情况(逆序)为
空间复杂度:O(1)
【代码】
void SelectSort(int a[],int n)
{
for(int i=1;i<n;i++)
{
int k=i; //记下最小值下标
for(int j=i+1;j<=n;j++)
if(a[j]<a[k])k=j;
if(k!=i) a[i]^=a[k]^=a[i]^=a[k]; //交换a[j]与a[k]
}
}
8.堆排序
【思路】
堆:将待排序数组a看作完全二叉树的顺序存储结构,即 结点 i 的左孩子是 i*2,右孩子是 i*2+1,且任意结点的数值总是大于任意孩子的数值(若有孩子)
此时,堆顶a[1]必为最大值,取出a[1],重新调整堆,再取堆顶,得次大值。以此类推
【步骤】
1. 将原序列看作完全二叉树的顺序存储,调整为大顶堆
2. 将堆顶a[1]与a[n]交换,删除第n个结点,重新调整为大顶堆。
3. 直到大顶堆仅剩一个结点,数组a中元素便有序。
【复杂度分析】
时间复杂度:
空间复杂度:O(1)
【代码】
void HeapAdjust(int a[],int p,int maxVex) //堆的结点p向下调整
{
int temp=a[p];
while(p*2<=maxVex) //有孩子则尝试调整
{
int kid=p*2; //先假设左孩子大于父结点
if(kid<maxVex&&a[kid+1]>a[kid])kid=kid+1; //右孩子更大
if(temp>=a[kid])break; //p树已为大顶堆
a[p]=a[kid];
p=kid;
}
a[p]=temp; //填空
}
void HeapSort(int a[],int n)
{
for(int i=n/2;i>=1;i--)//调整完全二叉树为堆
HeapAdjust(a,i,n);
for(int i=n;i>1;i--)
{
a[i]^=a[1]^=a[i]^=a[1]; //交换
HeapAdjust(a,1,i-1);
}
}
9.基数排序
【思路】
长度为n的待排序序列,每个元素x有d个关键字。举个例子,若元素是数字123,按十进制位分解可得3个关键字1,2,3。
先考虑优先级最低的关键字,进行一次稳定的排序,继而考虑优先级次低的关键字,类推
【复杂度分析】
时间复杂度:,d是元素关键字个数.
空间复杂度:O(n+r),r是关键字种类,例如按十进制分解,则r=10
【代码】
//基数排序 按十进制位分解
void BaseSort(int a[],int n)
{
int b[n+1],u[10],v[10],maxNum=0;
for(int i=1;i<=n;i++)maxNum=max(maxNum,a[i]);
for(int ten=1;ten<=maxNum;ten*=10)
{
for(int i=0;i<10;i++)u[i]=v[i]=0;
for(int i=1;i<=n;i++)u[a[i]/ten%10]++;
for(int i=1;i<10;i++)u[i]+=u[i-1];
for(int i=1;i<=n;i++)b[i]=a[i];
for(int i=1;i<=n;i++)
{
int x=b[i]/ten%10;
if(x==0)a[v[x]+1]=b[i];
else a[ u[x-1]+v[x]+1 ]=b[i];
v[x]++;
}
}
}