1.插入排序(insertion sort)
void insertsort(int A[N])
{
for(int i=2;i<=N;i++)//进行N-1趟排序
{
int temp=A[i],j=i;//temp临时存放A[i],j从i开始往前枚举
while( j>1 && temp<A[j-1] ) //只要temp小于前一个元素A[j-1]
{
A[j]=A[j-1];//把A[j-1]后移一位至A[j]
j--;
}
A[j]=temp;//插入位置为j
}
}
算法复杂度O( N2 )
2.简单选择排序
void selectsort(int A[],int n)
{
for(int i=0;i<n;i++)
{
int k=i;
for(int j=i+1;j<n;j++)
{
if(A[j]<A[k])
k=j;
}
Swap(&A[i],&A[k]);
}
}
3.希尔排序(Shellsort)
void Shellsort(int A[N])
{
int i,j,Increment,tmp;
for(Increment=N/2;Increment>0;Increment/=2)
{
for(i=Increment;i<N;i++)
{
tmp=A[i];
for(j=i;j>=Increment;j-=Increment)
{
if(tmp<A[j-Increment])
A[j]=A[j-Increment];
else
break;
}
A[j]=tmp;
}
}
}
优劣
不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为O( n^(3/2) ),希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O( n² )复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,至今仍然是数学难题。
4.堆排序(HeapSort)
1.堆
堆实际上是一棵完全二叉树,其任何一非叶节点满足性质:
Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2]
即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。
堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。
2.堆排序的思想
利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。
其基本思想为(大顶堆):
1)将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
代码如下:
Heap Sort
//堆筛选函数
//已知H[start~end]中除了start之外均满足堆的定义
//本函数进行调整,使H[start~end]成为一个大顶堆
typedef int ElemType;
void HeapAdjust(ElemType H[], int start, int end)
{
ElemType temp = H[start];
for(int i = 2*start + 1; i<=end; i=2*i+1)
{
//因为假设根结点的序号为0而不是1,所以i结点左孩子和右孩子分别为2i+1和2i+2
if(i<end && H[i]<H[i+1])//左右孩子的比较
{
++i;//i为较大的记录的下标
}
if(temp > H[i])//左右孩子中获胜者与父亲的比较
{
break;
}
//将孩子结点上位,则以孩子结点的位置进行下一轮的筛选
H[start]= H[i];
start = i;
}
H[start]= temp; //插入最开始不和谐的元素
}
void HeapSort(ElemType A[], int n)
{
//先建立大顶堆
for(int i=n/2-1; i>=0; --i)
{
HeapAdjust(A,i,n-1);
}
//进行排序
for(int i=n-1; i>0; --i)
{
//最后一个元素和第一元素进行交换
ElemType temp=A[i];
A[i] = A[0];
A[0] = temp;
//然后将剩下的无序元素继续调整为大顶堆
HeapAdjust(A,0,i-1);
}
}
堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1…n]中选择最大记录,需比较n-1次,然后从R[1…n-2]中选择最大记录需比较n-2次。事实上这n-2次比较中有很多已经在前面的n-1次比较中已经做过,而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。
5.归并排序(MergeSort)
1.递归实现
const int maxn = 100;
//将数组A的[L1,R1]与[L2,R2]区间合并为有序区间(此处L2即为R1+1)
void merge(int A[],int L1,int R1,int L2,int R2)
{
int i=L1,j=L2;//i指向A[L1],j指向A[L2]
int temp[maxn] , index=0;//temp临时存放合并后的数组,index为其下标
while(i<=R1&&j<=R2)
{
if(A[i]<=A[j])
{
temp[index++]=A[i++];
}
else
{
temp[index++]=A[j++];
}
}
while(i<=R1) temp[index++]=A[i++];
while(j<R2) temp[index++]=A[j++];
for(i=0;i<index;i++)
{
A[L1+i]=temp[i];//将合并后的序列赋值回数组A
}
}
void mergeSort(int A[],int left,int right)//将array数组当前区间[left,right]进行归并排序
{
if(left<right)
{
int mid=(left+right)/2;
mergeSort(A,left,mid);
mergeSort(A,mid+1,right);
merge(A,left,mid,mid+1,right);//将左右子区间合并
}
}
2.非递归实现:
void mergesort(int A[])
{//step为组内元素个数,step/2为左子区间元素个数,注意等号可以不取
for(int step=2;step/2<=N;step*=2)
{//每step个元素一组,组内前step/2和step/2个元素进行合并
for(int i=1;i<=N;i+=step)//对每一组
{
int mid=i+step/2-1;//左子区间元素个数为step/2
if(mid+1<=N) //右区间存在元素则合并
{//左子区间为[i,mid],右子区间为[ mid+1 , min(i+step-1,N) ]
merge(A,i,mid,mid+1,min(i+step-1,N));
}
}
}
}
**时间复杂度为O(nlog₂n) 这是该算法中最好、最坏和平均的时间性能。
空间复杂度为 O(n)
比较操作的次数介于(nlogn) / 2和nlogn - n + 1。
赋值操作的次数是(2nlogn)。归并算法的空间复杂度为:0 (n)
归并排序比较占用内存,但却是一种效率高且稳定的算法。
6.快速排序
快速排序实在实践中最快的已知排序算法,它的平均运行时间是O(NlogN),它的最坏情形的性能是O(
N2
).快速排序算法在元素排列比较随机时效率最高,但是当序列中元素接近有序时,会达到最坏情况,产生这种情况的主要原因是在于枢纽元没有把当前区间划分成两个长度接近的子区间,这里介绍一种安全的作法—三数中值分割法
/*实现三数中值分割方法*/
/*使用三数中值分割法消除了预排序输入的坏情形(在这种情形下 ,这些分割都是一样的)*/
/*并且减少了快速排序大约5%的运行时间*/
/*Return median of Left,Center,and Right*/
/*Order these and hide the pivot*/
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 pivot1
return A[Right-1];
}
主要算法实现:
int Partition(ElementType A[], int Left, int Right) {
int pivotkey;
pivotkey = median3(A,Left,Right);/*一般使用第一个元素A[Left],这里利用了三数中值分割法进行了优化*/
while (Left<Right) {
while (A[Right] > pivotkey && Left<Right)
Right--;
A[Left]=A[Right];
while (A[Left] < pivotkey && Left<Right)
Left++;
A[Right]=A[Left];
}
A[Right-1]=pivotkey;
return Left;
}
void QSort(ElementType A[], int Left, int Right) {
int pivot;//枢轴
if (Left<Right) {
pivot = Partition(A, Left, Right);//将A[Left...Right]一分为二,算出枢轴值
QSort(A, Left, pivot - 1);//对低子表递归排序
QSort(A, pivot + 1, Right);//对高子表递归排序
}
}
7.总结:
一般平均的最快排序:快速排序
初始序列有序的最快:直接插入排序
最可能出现空间不足:归并排序
与初始序列无关的排序:二分插入排序,直接选择排序,堆排序