一张各种排序算法的对比:
1、选择排序(Selection Sort)
选择排序的基本思想是对待排序的记录序列进行n-1遍的处理,第i遍处理是将L[i..n]中最小者与L[i]交换位置。这样,经过i遍处理之后,前i个记录的位置已经是正确的了。
2、 插入排序 (Insertion Sort)
插入排序的基本思想是,经过i-1遍处理后,L[1..i-1]己排好序。第i遍处理仅将L[i]插入L[1..i-1]的适当位置,使得L[1..i] 又是排好序的序列。要达到这个目的,我们可以用顺序比较的方法。首先比较L[i]和L[i-1],如果L[i-1]≤ L[i],则L[1..i]已排好序,第i遍处理就结束了;否则交换L[i]与L[i-1]的位置,继续比较L[i-1]和L[i-2],直到找到某一个位置j(1≤j≤i-1),使得L[j] ≤L[j+1]时为止。
3、 堆排序堆排序是一种树形选择排序,在排序过程中,将A[n]看成是完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点 之间的内在关系来选择最小的元素。
4、 归并排序
设有两个有序(升序)序列存储在同一数组中相邻的位置上,不妨设为A[l..m],A[m+1..h],将它们归并为一个有序数列,并存储在A[l..h]。
5、快速排序
快速排序是对冒泡排序的一种本质改进。它的基本思想是通过一趟扫描后,使得排序序列的长度能大幅度地减少。在冒泡排序中,一次扫描只能确保最大数值的数移到正确位置,而待排序序列的长度可能只减少1。快速排序通过一趟扫描,就能确保某个数(以它为基准点吧)的左边各数都比它小,右边各数都比它大。然后又用同样的方法处理它左右两边的数,直到基准点的左右只有一个元素为止。
6、希尔排序
在直接插入排序算法中,每次插入一个数,使有序序列只增加1个节点,并且对插入下一个数没有提供任何帮助。如果比较相隔较远距离(称为 增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。D.L.shell于1959年在以他名字命名的排序算法中实现了这一思想。算法先将要排序的一组数按某个增量d分成若干组,每组中记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量对它进行,在每组中再进行排序。当增量减到1时,整个要排序的数被分成一组,排序完成。一、直接插入排序(稳定):
思想:“一趟一个“地将待排序记录插入到已经排好序的部分记录的适当位置中,使其成为一个新的有序序列,直到所有待排序记录全部插入完毕。
流程:
分析:
- 空间:仅需使用一个辅助单元。故空间复杂度为O(1);
- 最好情况:待排序序列已经有序,每趟操作只需比较1次和移动0次,此时,总的比较次数为n-1,总移动次数为0,故最好情况算法复杂度为O(n);
- 最坏情况:待排序序列按逆序排序,这时在第j趟操作中,为插入元素需要同前面的j个元素比较,移动元素的次数为j+1.此时有:总的比较次数=1+2+...+(n-1)=n(n-1)/2;总移动次数=2+3+4+...+n=(n+2)(n-1)/2,故最坏情况算法复杂度为O(n^2);
- 平均情况:在第j趟操作中,插入记录大约需要同前面j/2个元素比较,移动记录的次数为j/2+1次,此时总的比较次数约为n^2/4;总移动次数为n^2/4;故时间复杂度为O(n^2);
代码:
void insertSort(int *a,int low,int high)//插入排序
{
for(int i=low+1;i<high;i++)
{
if(a[i]<a[i-1]) //判断:需要移动位置吗?
{
int tmp=a[i];
a[i]=a[i-1]; //设置哨兵为a[i-1].
int j=i-2;
for(;j>=low&&tmp<a[j];j--) //那就开始找到应该放的位置吧!
a[j+1]=a[j]; 记录后移
a[j+1]=tmp;
}
}
}
二、二分插入排序(稳定):
思想:与直接插入类似,在找寻插入位置时采用二分查找方式;
分析:与直接插入相同,仅仅减少了元素的比较次数,并没减少元素的移动次数,因此仍为O(n^2);
代码:
void binaryInsertSort(int *a,int low,int high)//二分插入排序
{
for(int i=low+1;i<high;i++)
{
int tmp=a[i];
int hi=i-1;
int lo=low;
while(lo<=hi)
{
int mid=(lo+hi)/2;
if(a[mid]>tmp) hi=mid-1;
else lo=mid+1;
}
for(int j=i-1;j>hi;j--)
{
a[j+1]=a[j];
}
a[hi+1]=tmp;
}
}
三、希尔排序
思想:”基本有序化“可以提升插入排序的效率,将记录序列分成若干子序列,每个子序列分别进行插入排序。需要注意的是,这种子序列是由间隔为某个增量的一组数据组成,即跳跃式选择数据组。(这个解释起来比较抽象,面试中也很少问,但编程贼简单)
流程:
分析:时间复杂度:开始时增量较大,分组较多,每组的记录数目较少,当排序规模n值较小时,n和n^2的差距也较小,但随着增量d逐渐缩小,分组逐渐减小,而各组的记录数目逐渐增多,但由于已经基本有序,所以新的一趟也较快。时间复杂度与增量的设置有关,约在O(n^1.25)和O(n^1.6)之间;
代码:
void shellInsert(int *a,int low,int high,int deltaK)//希尔排序
{
for(int i=low+deltaK;i<high;i++) //deltak是增量
{
if(a[i]<a[i-deltaK])
{
int tmp=a[i];
int j=i-deltaK;
for(;j>=low&&tmp<a[j];j=j-deltaK)
a[j+deltaK]=a[j]; //记录后移,找到插入位置
a[j+deltaK]=tmp; //插入
}
}
}
void shellSort(int *a,int low,int high,int *delta,int m)//希尔排序
{
for(int k=0;k<m;k++)
{
shellInsert(a,low,high,delta[k]);
}
}
四、冒泡排序(稳定):
1、思想:对n个数进行n-1次扫描,每次扫描比较相邻两个数字,大的移到后面。这样每趟排序会把最大的数字扔到最后面;
规则:两两比较相邻的关键字,如果反序则交换,直到没有反序的记录为止。
2、流程:
3、分析:
- 空间:需要一个辅助单元,故空间复杂度为O(1);
- 最好情况:数据全部排好序,这时循环n-1次,时间复杂度为O(n),但是程序中可以设置flag,某一次未发生位置交换,程序退出;
- 最坏情况:数据全部逆序存放,这时循环n-1次,比较次数为n-1+n-2+...+1=n(n-1)/2;移动次数为3*(n-1+n-2+...+1)=3n(n-1)/2,时间复杂度为O(n^2);
- 平均情况:与直接插入分析一样,时间复杂度为O(n^2);
4、代码:
void bubbleSort(int *a,int low,int high)//冒泡排序普通实现
{
int n=high-low;
for(int i=0;i<n-1;i++)
{
int flag=0;
for(int j=0;j<n-i-1;j++)
{
if(a[j]>a[j+1])
{
flag=1;
int tmp=a[j];
a[j]=a[j+1];
a[j+1]=tmp;
}
}
if(flag==0)
break;
}
}
腾讯面试——递归实现冒泡。
代码(冒泡排序的递归实现):
void bubbleSort2(int *a,int low,int high)//冒泡排序递归实现
{
if(high==1)
return;
for(int i=0;i<high-1;i++)
{
if(a[i]>a[i+1])
{
int tmp=a[i];
a[i]=a[i+1];
a[i+1]=tmp;
}
}
bubbleSort2(a,low,--high);
}
五、快速排序(不稳定):
思想:每次选择一个元素放置到它应该在的位置,划分原来序列为前面的数都不大于它,后面的数都不小于它;
流程:
分析(消耗的时间空间主要在递归上):
- 空间:最好情况,每次取到的元素刚好平分整个数组,此时空间复杂度为O(log n);最坏情况,每次取到的就是数组的最大或最小的,这种情况就是冒泡排序,此时空间复杂度为O(n),所以空间复杂度为O(log n)~O(n);
- 最好情况:相当于一个完全二叉树结构,即每次标准元素都把当前数组分成两个大小相等的子数组,这是分解次数等于完全二叉树的深度lb n;每次快排过程无论把数组怎样划分,全部的比较次数都接近于n-1次,所以时间复杂度为O(n lb n);
- 最坏情况:退化成冒泡排序,时间复杂度为O(n^2);
- 平均情况:时间复杂度为O(n lb n);
代码:
void quickSort(int *a,int low,int high)//快速排序
{
int i=low;
int j=high-1;
int tmp=a[low];
while(i<j)
{
while(i<j&&tmp<=a[j]) j--;
if(i<j)
{
a[i]=a[j];
i++;
}
while(i<j&&tmp>=a[i]) i++;
if(i<j)
{
a[j]=a[i];
j--;
}
}
a[i]=tmp;
if(low<i) quickSort(a,low,i-1);
if(i<high) quickSort(a,i+1,high);
}
六、直接选择排序(不稳定):
思想:在每趟过程中将待排序数组最小数放到最前面。
规则:
分析:
- 空间:需要一个辅助单元,故O(1);
- 最好最坏情况:无论初始状态如何,都需要进行n-1+n-1+...+1=n(n-1)/2次比较,所以时间复杂度为O(n^2);
代码:
void selectSort(int *a,int low,int high)//直接选择排序
{
int n=high-low; //获得长度
for(int i=0;i<n-1;i++)
{
int min=i;
for(int j=i+1;j<n;j++)
{
if(a[min]>a[j])
min=j;
}
if(min!=i)
{
int tmp=a[min];
a[min]=a[i];
a[i]=tmp;
}
}
}
七、堆排序
思想:把待排序的数据元素构造成一个完全二叉树结构,则每次选择出一个最大的数据元素只需比较完全二叉树的高度次,即logn次,则排序算法的最好最坏时间复杂度均为O(n log n);
空间:O(1);
这个比较复杂,具体的讲解可以参考博文:
http://www.cnblogs.com/skywang12345/p/3602162.html
代码:
void heapAdjust(int *a,int low,int high)//堆排序,调整堆
{
int tmp=a[low];
for(int j=2*low+1;j<high;j=j*2+1)
{
if(j<high-1&&a[j]<a[j+1]) j++;
if(tmp>=a[j]) break;
a[low]=a[j];
a[j]=tmp;
low=j;
}
//a[low]=tmp;
}
void heapSort(int *a,int high)//堆排序
{
for(int i=(high)/2-1;i>=0;i--)
heapAdjust(a,i,high);
for(int i=high-1;i>0;i--)
{
int tmp=a[0];
a[0]=a[i];
a[i]=tmp;
heapAdjust(a,0,i); //将a[0...i]重新调整成最
}
}
八、归并排序(稳定)
思想:将若干个已经排好序的子序列合并成一个有序的序列。
分析:
- 时间:归并的次数约为lb n,任何一次的归并比较次数约为n-1,所以最好最坏时间复杂度均为O(n log n);
- 空间:需要一个与原数组大小一样的辅助数组,故空间复杂度为O(n).
代码:
void merge(int *a,int p,int q,int r)//一次归并算法
{
int n1=q-p+1;
int n2=r-q;
int *L=new int[n1+1];
int *R=new int[n2+1];
for(int i=0;i<n1;i++)
{
L[i]=a[p+i];
}
for(int j=0;j<n2;j++)
{
R[j]=a[q+j+1];
}
L[n1]=1000000;
R[n2]=1000000;
for(int i=0,j=0,k=p;k<=r;k++)
{
if(L[i]<=R[j])
{
a[k]=L[i];
i++;
}
else
{
a[k]=R[j];
j++;
}
}
delete []L;
delete []R;
}
void mergepass(int *a,int p,int r)//二路归并
{
if(p<r)
{
int q=(p+r)/2;
mergepass(a,p,q);
mergepass(a,q+1,r);
merge(a,p,q,r);
}
}