在笔试面试的过程中,常常会考察一下常见的几种排序算法,包括冒泡排序,插入排序,希尔排序,直接选择排序,归并排序,快速排序,堆排序等7种排序算法,下面将分别进行讲解。另外,我自己在学习这几种算法过程中,主要参考了MoreWindows Blog中的排序算法,在此向他表示感谢,他写的很详细全面,有兴趣可参考这里写链接内容中的白话经典算法部分。
提示:以下排序均是以排序完成后序列从左往右非递减为例讲解
1:冒泡排序(bubbleSort)
冒泡排序是每次将乱序中的最大的数字通过两两交换的方式往后移动,直到序列有序为止。犹如水中的气泡从下往上浮时,越来越大。该算法共执行了n趟,每趟执行n-i次比较,所以其复杂度为O(n^2)。
基本的冒泡排序算法程序如下所示:
//冒泡排序
void bubbleSort(int a[],int n)
{
int i,j;
for(i=0;i<n;++i)//共执行n趟
for(j=1;j<n-i;++j)//每趟执行n-i次比较,选出一个最大值
{
if(a[j]<a[j-1])//通过两两比较使得大的数据“上浮”
swap(a[j],a[j-1]);
}
}
冒泡排序虽然简单,但当序列部分有序或者基本有序时,基本的冒泡排序算法会做一些无用功,可对原算法进行一些改进,从而可以提高排序效率。
改进1:在冒泡排序中,通过前后2个数据的两两交换,来完成排序过程,而如果某一趟并没有发生交换,说明此时序列已经有序,就可以终止排序过程。
改进的冒泡排序程序如下所示:
//改进的冒泡排序
void bubbleSort_advanced(int a[],int n)
{
int i,j;
bool flag;
i=n;
flag=true;//设定标志位
while(flag)//当标志位为真时才执行某一趟
{
flag=false;
for(j=1;j<i;++j)
{
if(a[j]<a[j-1])
{
swap(a[j],a[j-1]);
flag=true;//当交换发生时,标志位为真
//显然如果某一趟没有发生交换,说明排序已经完成
}
}
i--;
}
}
改进2:待排序序列可能部分有序,我们可以在某一趟排序过程中确定序列最后有序的位置,从而可以减少排序趟数,简化排序过程。
改进的冒泡排序程序如下:
//改进的冒泡排序
//其主要区别体现在对最后排序位置的提取上
void bubbleSort_advanced_2(int a[],int n)
{
int i,j,flag;//flag表示某趟排序的最后位置
flag=n;
while(flag>0)
{
i=flag;
for(j=1;j<i;++j)
{
if(a[j]<a[j-1])
{
swap(a[j],a[j-1]);
flag=j;//表示flag之后的数据都已经有序,flag永远小于i
}
}
if(i==flag)
return;//与改进方法1结合,i==flag说明某一趟没有发生交换,即排序完成
}
}
2:插入排序(insertSort)
插入排序可以简单概括为:假定序列下标i之前数据是有序的,则从i-1位置数据开始,依次将其与i进行比较并交换(当该值不满足插入条件,即该位置值大于i位置值时),最终找到一个合适的位置插入下标i数据,以形成一个更大的有序序列。
插入排序程序如下所示:
void InsertSort(int a[],int n)
{
int i,j;
for(i=1;i<n;++i)//从1开始,认为a[0]是有序的
if(a[i]<a[i-1])
{
int temp=a[i];
for(j=i-1;j>=0&&a[j]>temp;--j)
a[j+1]=a[j];//在找到合适的插入点前,数据都往后移一位
//a[j]为小于等于temp(不满足a[j]>temp)的第一个位置
a[j+1]=temp;//找到了合适的插入点
}
}
3:希尔排序(shellSort)
希尔排序算法可以概括为:先将整个待排序序列分割成若干个子序列(一般分成2个),分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中整个元素增量为1时,再对全体元素进行一次直接插入排序。
希尔排序程序如下所示:
void shellSort(int a[],int n)
{
int gap,i,j;
for(gap=n/2;gap>0;gap/=2)
for(i=gap;i<n;++i)//从gap位置开始比较
for(j=i-gap;j>=0&&a[j]>a[j+gap];j-=gap)
swap(a[j],a[j+gap]);
}
4:直接选择排序(selectSort)
选择排序简单的说就是每次找到序列中的最小值,然后将该值放在有序序列的最后一个位置,以形成一个更大的有序序列。选择排序进行n趟,每趟从i+1开始,每趟找到最小值下标min_index,再将a[min_index]与a[i]交换。
选择排序程序如下所示:
void selectSort(int a[],int n)
{
int i,j,min_index;
for(i=0;i<n;++i)//找到未排序数组中最小的那个元素,放在已排序部分的后面
{
min_index=i;
for(j=i+1;j<n;++j)
if(a[j]<a[min_index])
min_index=j;
swap(a[min_index],a[i]);
}
}
5:归并排序(mergeSort)
对于归并排序,记好一句话即可:递归的分解+合并。另外归并排序需要O(n)的辅助空间
归并排序程序如下所示:
void mergeSortCore(int a[],int first,int mid,int last,int pTemp[])
{
int i=first,j=mid;
int m=mid+1,n=last;
int k=0;
while(i<=j&&m<=n)
{
if(a[i]>a[m])
pTemp[k++]=a[m++];
else
pTemp[k++]=a[i++];
}
while(i<=j)
pTemp[k++]=a[i++];
while(m<=n)
pTemp[k++]=a[m++];
for(i=0;i<k;++i)//将辅助空间内的数据转移到原始数组
a[first+i]=pTemp[i];
}
void mergeSort(int a[],int first,int last,int pTemp[])
{
//递归的分解
while(first<last)
{
int mid=(first+last)/2;
mergeSort(a,first,mid,pTemp);//左边有序
mergeSort(a,mid+1,last,pTemp);//右边有序
mergeSortCore(a,first,mid,last,pTemp);//合并有序
}
}
void main()
{
int n;//数据长度
int *pTemp=new int[n];
if(pTemp==NULL)
cerr<<"No Space!";
mergeSort(a,0,n-1,pTemp);
delete[] pTemp;
}
提示:记好归并排序算法由3个程序组成及每个程序的作用
6:快速排序(quickSort)
快速排序一般是选定第一个数为基准数,然后分别从后向前找比基准数小的数,从前向后找比基准数大的数,然后交换前后找到的数的位置,并在最后为基准数找到一个合适的位置,使得基准数左侧的数据都比基准数小,基准数右侧的数据都比基准数大,然后以基准数为界将序列分为左右2个子序列,最后利用递归分解的方法完成排序过程。
提示:在遇到选择或者填空题时,在做某一趟的快速排序推算时,用“挖坑填数法”+“分治法”,而在写程序时,用“交换法”+“分治法”。
快速排序程序如下所示:
void quickSort(int a[],int first,int last)
{
int i=first,j=last;
if(i>j)
return;
while(i<j)
{
while(i<j&&a[j]>=a[first])//从后往前找小于基准数的位置
j--;
while(i<j&&a[i]<=a[first])//从前往后找大于基准数的位置
i++;
if(i<j)//注意,i,j不能相遇或交叉
swap(a[i],a[j]);
}
swap(a[first],a[j]);
quickSort(a,first,j-1);
quickSort(a,j+1,last);
}
7:堆排序
首先介绍一下最大堆和最小堆:
最大堆:父结点键值大于等于任一子结点,对最大堆排序后得到递增序列。
最小堆:父结点键值小于等于任一子结点,对最小堆排序后得到递减序列。
以下以最小堆为例讲解堆的插入,删除,创建及排序过程:
堆的插入:插入值最开始放在最后的子结点,将插入值与根结点逐级比较,将较大的根结点向下移动,替换其子结点。
堆插入程序代码如下:
void MinHeapInsert(int a[],int i)
{
int j=(i-1)/2;//找到i结点的根节点
int temp=a[i];//将待插入新值保存
while(j>=0&&i!=0)
{
if(a[j]<=temp)//说明若将值放在此位置,此时的堆已是最小堆
break;
a[i]=a[j];//根结点向下移动
i=j;
j=(i-1)/2;
}
a[i]=temp;//找到了合适的位置,对i点进行赋值
}
堆的删除:堆的删除就是删除根结点,然后再调整堆的过程。其调整过程为:每次拿2个子节点中最小的结点与根结点比较,并将较小的子结点向上移动,替换根结点。
void MinHeapDelete(int a[],int i,int n)//堆的删除
{
int j,temp=a[i];
j=2*i+1;//节点i的左孩子
while(j<n)
{
if(j+1<n&&a[j+1]<a[j])//在左右孩子中找到最小的
j++;
if(a[j]>=temp)//满足该条件时说明原始堆有序
break;
a[i]=a[j];//小数据上移,替换其父节点
i=j;
j=2*i+1;//继续处理
}
a[i]=temp;
}
堆的创建:有以下两种方法:
法1:
void MakeMinHeap(int a[],int n)
{
for(int i=0;i<n;++i)
MinHeapInsert(a,i);
}
法2:
void MakeMinHeap(int a[],int n)//建立最小堆
{
for(int i=n/2-1;i>=0;--i)//认为叶子节点都是符合最小堆的
MinHeapDelete(a,i,n);
}
堆排序:
void MinHeapSort(int a[],int n)//最小堆排序获得的是递减的数组
{
for(int i=n-1;i>=1;--i)
{
swap(a[i],a[0]);//每次“删除”最小堆的根节点
MinHeapDelete(a,0,i);//每次都是从0(根节点开始调整)
}
}
补充:几种排序算法的性能比较
1:复杂度
平均复杂度:
O(N^2)的有冒泡排序、插入排序、选择排序
O(N*logN)的有希尔排序、归并排序、快速排序、堆排序
复杂度最坏情况:冒泡排序、插入排序、选择排序、快速排序均为O(N^2),归并排序,堆排序均为O(N*logN)。
补充:对于快速排序:最坏的情况,待排序的序列为正序或者逆序,每次划分只得到一个比上一次划分少一个的子序列,另外一个为空。如果递归树画出来,就是一颗斜树。此时需要执行n-1次递归调用,且第i次划分需要经(n-i)次关键字比较才能找到才能找到第i个记录,因此比较的次数为(n-1)+(n-2)+…+1 = n*(n-1)/2,最终时间复杂度为O(n^2)。
复杂度最好情况:冒泡排序、插入排序均为O(N),选择排序仍为O(N^2),归并排序,快速排序,堆排序仍为O(N*logN)。
另可注意到:最好、最坏、平均三项复杂度全是一样的、就是与初始排序无关的排序方法为:选择排序、堆排序、归并排序
2:空间复杂度
除归并排序空间复杂度为O(N),快速排序空间复杂度为O(logN)外,其他几种排序方法空间复杂度均为O(1)。
3:稳定性
所谓排序过程中的稳定性是指:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法是稳定的;否则称为不稳定的。
为稳定排序的有:冒泡排序,插入排序,归并排序;其余几种均为非稳定排序。
补充:找出若干个数中最大/最小的前K个数(K远小于n),用什么排序方法最好?
答:用堆排序是最好的。建堆O(n),k个数据排序klogn,总的复杂度为n+klogn。不考虑桶排序,n+klogn小于n*logn只有在k趋近n时才不成立,所以堆排序在绝大多数情况下是最好的。