排序
文章目录
1.排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
2.常见的排序算法
- 插入排序:直接插入排序、希尔排序
- 选择排序:选择排序、堆排序
- 交换排序:冒泡排序,快速排序
- 归并排序:归并排序
- 非比较排序:计数排序
写排序的时候先想一趟排序的写法,然后再写上多趟的逻辑。
2.1插入排序
2.1.1直接插入排序
基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。【可以想象玩扑克牌的时候,把新拿的牌插入到已经排好的手牌中】
直接插入排序过程:
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移
- 单趟排序:将x插入到[0,end]的有序区间
void InsertSort(int* a, int n){
int end;
int x;
while(end>=0){
if(a[end]>x) a[end+1]=a[end],end--;
else break;
}
a[end+1]=x;
}
- 将前面的部分变成有序数组
void InsertSort(int *a,int n){
assert(a);
for(int i=0;i<n-1;i++){
int end=i;
int x=a[end+1];
while(end>=0){
if(a[end]>x) a[end+1]=a[end],end--;
else break;
}
a[end+1]=x;
}
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
2.1.2希尔排序
希尔排序是在直接插入排序的思想上优化。
因为直接插入排序在接近有序的情况下效率很高,所以希尔的做法就是先尝试把数组排成有序。
希尔排序法又称缩小增量法。
希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
- 分组预排序——数组接近有序
- 按gap分组,对分组值进行插入排序
- 对每组进行直接插入排序后
- 特别的,对于逆序数据预排后更接近有序。
- 按gap分组,对分组值进行插入排序
- 直接插入排序
- 一组内的单趟
void ShellSort(int* a,int n){
//按gap分组数据进行预处理
int gap=3;
int end=0;
int x=a[end+gap];
while(end>=0){
if(a[end]>x){
a[end+gap]=a[end];
end-=gap;
}
else break;
}
a[end+gap]=x;
}
void ShellSort(int* a,int n){
//按gap分组数据进行预处理
int gap=3;
for(int i=0;i<n-gap;i+=gap){
int end=i;
int x=a[end+gap];
while(end>=0){
if(a[end]>x){
a[end+gap]=a[end];
end-=gap;
}
else break;
}
a[end+gap]=x;
}
}
- 提升到多组
void ShellSort(int* a,int n){
//按gap分组数据进行预处理
int gap=3;
for(int j=0;j<gap;j++){
for(int i=j;i<n-gap;i+=gap){
int end=i;
int x=a[end+gap];
while(end>=0){
if(a[end]>x){
a[end+gap]=a[end];
end-=gap;
}
else break;
}
a[end+gap]=x;
}
}
}
时间复杂度:最好O(N),最坏:F(N,gap)=(1+2+3…N/gap)*gap。代入可得gap越大,预排越快,越不接近有序。gap越小,预排越慢,越接近有序。由于慢慢变小接近有序,整个处理接近O(n),所以从下面的代码来看,可以估计成NlogN。官方说法是O( N 1.3 {N^{1.3}} N1.3)
实际上我们可以多组一起进行预排处理。不过是每组按顺序排过来。
void ShellSort(int *a,int n){
//按gap分组数据进行预处理
int gap=3;
//多组一起搞,每次轮流一组
for(int i=0;i<n-gap;i++){
int end=i;
int x=a[end+gap];
while(end>=0){
if(a[end]>x){
a[end+gap]=a[end];
end-=gap;
}
else break;
}
a[end+gap]=x;
}
}
}
同时预排序不是只能排一次,采用的是多次预排序(gap>1)+直接插入(gap==1)。
所以多次预处理到gap=1即可。每次/2是能到1,但是比如/3就不能到1。所以如果/3需要每次在后面+1。
void ShellSort(int *a,int n){
//按gap分组数据进行预处理
int gap=n;
while(gap>1){
//多组一起搞,每次轮流一组
gap/=2;
//gap=gap/3+1;
for(int i=0;i<n-gap;i++){
int end=i;
int x=a[end+gap];
while(end>=0){
if(a[end]>x){
a[end+gap]=a[end];
end-=gap;
}
else break;
}
a[end+gap]=x;
}
}
}
}
- 希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此给出的希尔排序的时间复杂度都不固定,按照 O ( n 1.25 ) O(n^{1.25}) O(n1.25)或者 O ( 1.6 ∗ n 1.25 ) O(1.6*n^{1.25}) O(1.6∗n1.25)
2.2选择排序
2.2.1直接选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。
选择排序步骤:
- 在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
对于常数优化就是在一次选一个的基础上同时把最大值和最小值选出来。不过要注意一个特例的判断。
- 先写单趟
void SelectSort(int* a,int n){
int begin=0;int end=n-1;
while(begin<end){
int mini=begin,maxx=begin;
for(int i=begin;i<=end;i++){
if(a[i]<a[maxx]) maxx=i;
if(a[i]>a[mini]) mini=i;
}
swap(&a[mini],&a[begin]);
//maxx和begin的位置相等
if(maxx==begin){
maxx=mini;//修正maxx的位置
}
swap(&a[maxx],&a[end]);
begin++;end--;
}
}
性质:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
2.2.2堆排序
向上调整建堆是O(N*logN)。向下是O(N)。
void HeapSort(int* a,int n){
//O(N)
for(int i=(n-1-1)/2;i>=0;i--) adjustdown(a,n,i);
int ed=n-1;
//O(NlogN)
while(ed>=0){
swap(&a[0],&a[ed]);
adjustdown(a,n-1,0);
ed--;
}
}
- 直接选择排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
2.3交换排序
2.3.1冒泡排序
void BubbleSort(int *a,int n){
for(int i=0;i<n;i++){
for(int j=0;j<n-i-1;j++){
if(a[j]>a[j+1]) swap(&a[j],&a[j+1]);
}
}
}
- 冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
2.3.2快速排序
- hoare法
int Partion1(int* a, int left, int right)
{
// 三数取中 -- 面对有序最坏情况,变成选中位数做key,变成最好情况
int mini = GetMidIndex(a, left, right);
Swap(&a[mini], &a[left]);
//左边取key右边先走;反之右边取key左边先走。是为了保证能和keyi交换的数一定是对的
int keyi = left;
while (left < right)
{
// 右边先走,找小
while (left < right && a[right] >= a[keyi])
--right;
//左边再走,找大
while (left < right && a[left] <= a[keyi])
++left;
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
return left;
}
- 挖坑法
// 挖坑法
int Partion2(int* a, int left, int right)
{
// 三数取中 -- 面对有序最坏情况,变成选中位数做key,变成最好情况
int mini = GetMidIndex(a, left, right);
Swap(&a[mini], &a[left]);
int key = a[left];
int pivot = left;
while (left < right)
{
// 右边找小, 放到左边的坑里面
while (left < right && a[right] >= key)
{
--right;
}
a[pivot] = a[right];
pivot = right;
// 左边找大,放到右边的坑里面
while (left < right && a[left] <= key)
{
++left;
}
a[pivot] = a[left];
pivot = left;
}
a[pivot] = key;
return pivot;
}
- 前后指针做法
该做法的思路就是通过前后指针来找出小于keyivalue的一段连续序列。
cur找小,把小的往左边翻;prev把大的序列往右边推。
同时单链表/双向链表也能用这个去排序。但是链表是没法三数取中的。
同时快排有一个极端的情况就是所有数据都一样或者两个数据反复跳跃,快排是被严格卡成 O ( n 2 ) O(n^2) O(n2)的。
这种做法不容易出边界并且好用,建议理解记这个板子。
int Partion3(int *a,int left,int right){
int mini = GetMidIndex(a, left, right);
Swap(&a[mini], &a[left]);
int keyi=left;
int prev=left;
int cur=prev+1;
while(cur<=right){
if(a[cur]<a[keyi]&&++prev!=cur){
swap(&a[cur],&a[prev]);
}
++cur;
}
swap(&a[prev],&a[keyi]);
return prev;
}
void QuickSort(int *a,int left,int right){
if(left>=right) return;
int keyi=Partion3(a,left,right);
QuickSort(a,left,keyi-1);
QuickSort(a,keyi+1,right);
}
- 快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
2.3.1快速排序的两个优化
- 三数取中法选key
int GetMidIndex(int* a, int left, int right)
{
//int mid = (left + right) / 2;
int mid = left + ((right - left) >> 1);
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
- 递归到小的子区间时,可以考虑使用插入排序
int Partion3(int *a,int left,int right){
int keyi=left;
int prev=left;
int cur=prev+1;
while(cur<=right){
if(a[cur]<a[keyi]&&++prev!=cur){
swap(&a[cur],&a[prev]);
}
++cur;
}
swap(&a[prev],&a[keyi]);
return prev;
}
由于到递归深层的时候,小区间的递归总层数就占据了整个递归区间层数的一半,所以我们当小区间递归的时候减少递归次数。而此时接近有序,用直接插入排序的效果更好。
void QuickSort(int *a,int left,int right){
if(left>=right) return;
//小区间优化
if(right-left+1<10){
Insert(a+left,right-left+1);
}
else{
int keyi=Partion3(a,left,right);
QuickSort(a,left,keyi-1);
QuickSort(a,keyi+1,right);
}
}
2.3.2快排的非递归版本
有些时候单纯的快排是特意给你爆栈的。而且现在面试的主流是非递归用栈进行模拟写的。
这里的优势就是堆比栈要大。
//递归深度太深的程序,我们只能考虑非递归。
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
while (StackEmpty(&st) != 0)
{
right = StackTop(&st);
StackPop(&st);
left = StackTop(&st);
StackPop(&st);
if(right - left <= 1) continue;
int div = PartSort1(a, left, right);
// 以基准值为分割点,形成左右两部分:[left, div) 和 [div+1, right)
StackPush(&st, div+1);
StackPush(&st, right);
StackPush(&st, left);
StackPush(&st, div);
}
StackDestroy(&s);
}
2.4归并排序
2.4.1归并排序
思想:假设数组的左边有序,右边也有序,O(N)归并成一个有序数组。同时要借助一个第三方数组。也就是说归并排序是有空间复杂度的消耗的。
10 6 7 1 3 9 4 2
1 6 7 10 2 3 4 9
tmp:1 2 3 4 6 7 9 10
做法的前提是左区间有序并且右区间也有序。就类似快排一样,处理更小的区间。
void _MergeSort(int* a,int left,int right,int* tmp){
if(left>=right) return;
int mid=(left+right)/2;
//如果[left,mid]有序并且[mid+1,right]有序,就可以有序
_MergeSort(a,left,mid,tmp);
_MergeSort(a,mid+1,right,tmp);
int begin1=left,end1=mid;
int begin2=mid+1,end2=right;
int idx=left;
while(begin1<=end1&&begin2<=end2){
if(a[begin1]<a[begin2]) tmp[idx++]=a[begin1++];
else tmp[idx++]=a[begin2++];
}
while(begin1<=end1){
tmp[idx++]=a[begin1++];
}
while(begin2<=end2){
tmp[idx++]=a[begin2++];
}
//tmp数组拷贝回a
for(int i=left;i<=right;i++) a[i]=tmp[i];
}
void MergeSort(int *a ,int n){
int * tmp=(int*)malloc(n*sizeof(int));
if(tmp==NULL){
perror("malloc fail\n");exit(-1);
}
_MergeSort(a,0,n-1,tmp);
free(tmp);
tmp=NULL;
}
- 归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
2.4.2非递归的归并排序
非递归可以利用队列bfs来处理。不过用栈模拟就是和dfs是一样的过程。
考虑单趟做法,先考虑一一元素归并的区间,然后再把区间距离加大。
void MergeSortNonR(int *a,int n){
int *tmp=(int*)malloc(n*sizeof(int));
if(tmp==NULL){
printf("Error!\n");
exit(-1);
}
int gap=1;
while(gap<n){
for(int i=0;i<n;i+=2*gap){
//[i,i+gap-1],[i+gap,i+2*gap-1]
int begin1=i;int end1=i+gap-1;
int begin2=i+gap;int end2=i+2*gap-1;
int idx=i;
while(begin1<=end1&&begin2<=end2){
if(a[begin1]<a[begin2]){
tmp[idx++]=a[begin1++];
}else tmp[idx++]=a[begin2++];
}
while(begin1<end1){
tmp[idx++]=a[begin1++];
}
while(begin2<end2){
tmp[idx++]=a[begin2++];
}
}
for(int i=0;i<n:i++){
a[i]=tmp[i];
}
gap*=2;
}
}
但是上面的代码只对 2 i 2{^i} 2i长度的数据成立。非递归版本要考虑边界。
越界情况:
- [begin1,end1],[begin2,end2]。end1,begin2,end2越界了。
- [begin1,end1],[begin2,end2]。begin2,end2越界了。
- [begin1,end1],[begin2,end2]。end2越界了。
void MergeSortNonR(int *a,int n){
int *tmp=(int*)malloc(n*sizeof(int));
if(tmp==NULL){
printf("Error!\n");
exit(-1);
}
int gap=1;
while(gap<n){
for(int i=0;i<n;i+=2*gap){
//[i,i+gap-1],[i+gap,i+2*gap-1]
int begin1=i;int end1=i+gap-1;
int begin2=i+gap;int end2=i+2*gap-1;
//end1越界,begin2和end2不存在
if(end1>=n){
end1=n-1;
}
if(begin2>=n){
begin2=n; end2=n-1;///这里要修正成不存在的区间 不然会有[8,8][8,8]两次访问导致index下标越界的问题。
}
if(end2>=n){
end2=n-1;
}
int idx=i;
while(begin1<=end1&&begin2<=end2){
if(a[begin1]<a[begin2]){
tmp[idx++]=a[begin1++];
}else tmp[idx++]=a[begin2++];
}
while(begin1<=end1){
tmp[idx++]=a[begin1++];
}
while(begin2<=end2){
tmp[idx++]=a[begin2++];
}
}
for(int i=0;i<n:i++){
a[i]=tmp[i];
}
gap*=2;
}
free(tmp);
tmp=NULL;
}
上面这份代码由于最后统一处理放回原数组就存在一些边界要考虑一下。我们可以像递归一下边递归边返回去。
void MergeSortNonR(int *a,int n){
int *tmp=(int*)malloc(n*sizeof(int));
if(tmp==NULL){
printf("Error!\n");
exit(-1);
}
int gap=1;
while(gap<n){
for(int i=0;i<n;i+=2*gap){
//[i,i+gap-1],[i+gap,i+2*gap-1]
int begin1=i;int end1=i+gap-1;
int begin2=i+gap;int end2=i+2*gap-1;
//end1越界||begin2>=n,该区间的不用归并直接使用原数组即可。
if(end1>=n||begin2>=n){
break;
}
//end2越界,需要归并,修正end2
if(end2>=n){
end2=n-1;
}
int idx=i;
while(begin1<=end1&&begin2<=end2){
if(a[begin1]<a[begin2]){
tmp[idx++]=a[begin1++];
}else tmp[idx++]=a[begin2++];
}
while(begin1<=end1){
tmp[idx++]=a[begin1++];
}
while(begin2<=end2){
tmp[idx++]=a[begin2++];
}
for(int j=i;j<=end2:j++){
a[j]=tmp[j];
}
}
gap*=2;
}
free(tmp);
tmp=NULL;
}
2.4.3外部排序
如果数据量不大,可以使用scanf/fscanf/sscanf(读取字符串)对打开的文件标识符读取后排序再写回文件。
假如是海量数据,内存没有那么大,为了避免频繁调页,直接需要在外存上进行排序。
文件不支持快排和堆排的随机访问,fseek对文件的定位也是很慢的。
但是归并排的思想可以用于快排,把多个有序文件的数据依次往一个新的文件里写。
将大文件平均分割成N份,保证每份的大小可以加载到内存,那么就可以把每个小文件加载到内存中,使用快排排成有序再写回小文件,这时归并要求的每个段有序已经完成了。
printf/fprintf/sprintf(写到字符串里面去)
#include<iostream>
using namespace std;
int main(){
FILE* fout=fopen("xxx","r");
char subfile[100]="";
/*将每个小文件处理成有序*/
while(fscanf(fout,"%d\n",&num)!=EOF)
{
if(i<n-1) a[i++]=num;
else{
a[i]=num;
QuickSort(a,0,n-1);
sprintf(subfile,"test\\%d.txt",filei++);
FILE* fin=fropen(subfile,"w");
if(fin==NULL)
{
perror("file open error");
exit(-1);
}
for(int i=0;i<n;i++)
{
fprintf(fin,"%d\n",a[i]);
}
fclose(fin);
i=0;
}
}
/*归并到一个文件里*/
char mfile[100]="01";
char file0[100]="test\\0";
char file1[100];
for(int i=1;i<n;i++)
{
sprintf(file1,"test\\%d",i);
//读取file0和file1,进行归并出mfile
_MergeFile(file0,file1,mfile);
strcpy(file0,mfile);
sprintf(mfile,"%s%d",mfile,i+1);
}
fclose(fout);
}
void _MergeFile(char* f0,char* f1,char* f2)
{
FILE* F0=fopen(f0,"r");
if(F0==NULL)
{
perror("error");exit(-1);
}
FILE* F1=fopen(f1,"r");
if(F1==NULL)
{
perror("error");exit(-1);
}
FILE* F2=fopen(f2,"w");
if(F2==NULL)
{
perror("error");exit(-1);
}
int num1,num2;
int res1=fscanf(F0,"%d\n",&num1);
int res2=fscanf(F1,"%d\n",&num1);
while(res1!=EOF&&res2!=EOF)
{
if(num1<num2)
{
fprintf(F2,"%d\n",num1);
res1=fscanf(F0,"%d\n",&num1);
}
else{
fprintf(F2,"%d\n",num2);
res2=fscanf(F1,"%d\n",&num2);
}
}
while(ret1!=EOF){
fprintf(F2,"%d\n",num1);
fscanf(F0,"%d\n",&num1);
}
while(ret2!=EOF){
fprintf(F2,"%d\n",num2);
fscanf(F0,"%d\n",&num1);
}
fclose(F0);
fclose(F1);
fclose(F2);
}
归并各个小文件可以两个两个归并后搞,但是这样会有很多的临时文件。
我们可以都归到一个文件里。归并的树是类似哈夫曼树。
2.5非比较排序
2.5.1计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤:
- 统计相同元素出现次数 O(N)
- 根据统计的结果将序列回收到原来的序列中 O(range)
或者复杂度为O(max(N,range))
但是比如数据为:1000,1200,1001,1500,1300,1301难道要开1501个大小吗。我们可以简单优化一下开MAX-MIN+1个空间。
映射位置:x-min
相对映射。
可以看出计数排序比较适合数据比较紧密的情况。
void CountSort(int* a,int n){
int max=a[0],min=a[0];
for(int i=0;i<n;i++){
if(a[i]>max) max=a[i];
if(a[i]<min) min=a[i];
}
int range=max-min+1;
int* count=(int*)malloc(range*sizeof(int));
memset(count,0,sizeof(int)*range);
if(count==NULL) perror("malloc error\n");
//统计次数
for(int i=0;i<n;i++){
count[a[i]-min]++;
}
//根据次数进行排序
int idx=0;
for(int i=0;i<range;i++){
while(count[i]--){
a[idx++]=i+min;
}
}
}
对于负数,可以用unsigned转成正数,但是时间复杂度就炸了。
- 计数排序的特性总结
-
计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。 范围较大,浮点数等就不适合。
-
时间复杂度:O(MAX(N,范围))
-
空间复杂度:O(范围)
-
稳定性:稳定
3.排序总结
O ( N 2 ) O(N^{2}) O(N2):直接插入,选择排序,冒泡排序。
O ( N ∗ l o g ( N ) ) O(N*log(N)) O(N∗log(N)):希尔排序,堆排序,快速排序,归并排序。
面试中最容易写快排,归并,堆排。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。 ——数组中相同的值在排序后相对位置是否变化,可能会变的就是不稳定。能保证不变就是稳定的。
稳定性的意义体现在哪里?比如有一个结构体数据,那么对于第一维value相同的可以看第二维的。
首先所有的排序都可以不稳定。我们看哪些排序经过控制能稳定。
排序方法 | 稳定性 | 原因 |
---|---|---|
直接插入排序 | 稳定 | 可以通过控制不让同value的交换 |
希尔排序 | 不稳定 | 不能保证同value的分派到一个组 |
选择排序 | 不稳定 | 5 , 4 , 1 , 5 , 0 , 9 {5,4,1,5,0,9} 5,4,1,5,0,9会把第一个5交换到后面导致不稳定 |
堆排序 | 不稳定 | 5 , 5 , 5 , 2 , 4 {5,5,5,2,4} 5,5,5,2,4调整的时候变成 4 , 5 , 5 , 2 , 5 {4,5,5,2,5} 4,5,5,2,5第一个5到最后一个位置了 |
冒泡排序 | 稳定 | 相等不换 |
快速排序 | 不稳定 | 5....5...5 {5....5...5} 5....5...5快排左边找严格大的数,右边找严格小的数。第一个5就会到中间的一些位置。之后递归的区间不会改变现在的位置 |
归并排序 | 稳定 | 1 , 2 , 2 , 4 {1,2,2,4} 1,2,2,4和 1 , 2 , 3 {1,2,3} 1,2,3。相等的时候先出左边的。 |
计数排序 | 不稳定 | 计数完之后什么数都不等了 |
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O ( n 2 ) O(n^{2}) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^{2}) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
直接选择排序 | O ( n 2 ) O(n^{2}) O(n2) | O ( n 2 ) O(n^{2}) O(n2) | O ( n 2 ) O(n^{2}) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
直接插入排序 | O ( n 2 ) O(n^{2}) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^{2}) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
希尔排序 | O ( n l o g n ) O(nlogn) O(nlogn)~ O ( n 2 ) O(n^{2}) O(n2) | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( n 2 ) O(n^{2}) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
堆排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( 1 ) O(1) O(1) | 不稳定 |
归并排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( N ) O(N) O(N) | 稳定 |
快速排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n 2 ) O(n^{2}) O(n2) | O ( l o g N ) O(logN) O(logN)~ O ( N ) O(N) O(N) | 不稳定 |
1. 快速排序算法是基于( )的一个排序算法。
A分治法
B贪心法
C递归法
D动态规划法
2.对记录(54,38,96,23,15,72,60,45,83)进行从小到大的直接插入排序时,当把第8个记录45插入到有序
表时,为找到插入位置需比较( )次?(采用从后往前比较)
A 3
B 4
C 5
D 6
3.以下排序方式中占用O(n)辅助存储空间的是
A 简单排序
B 快速排序
C 堆排序
D 归并排序
4.下列排序算法中稳定且时间复杂度为O(n2)的是( )
A 快速排序
B 冒泡排序
C 直接选择排序
D 归并排序
5.关于排序,下面说法不正确的是
A 快排时间复杂度为O(N*logN),空间复杂度为O(logN)
B 归并排序是一种稳定的排序,堆排序和快排均不稳定
C 序列基本有序时,快排退化成冒泡排序,直接插入排序最快
D 归并排序空间复杂度为O(N), 堆排序空间复杂度的为O(logN)
6.下列排序法中,最坏情况下时间复杂度最小的是( )
A 堆排序
B 快速排序
C 希尔排序
D 冒泡排序
7.设一组初始记录关键字序列为(65,56,72,99,86,25,34,66),则以第一个关键字65为基准而得到的一趟快
速排序结果是()
A 34,56,25,65,86,99,72,66
B 25,34,56,65,99,86,72,66
C 34,56,25,65,66,99,86,72
D 34,56,25,65,99,86,72,66
答案:
1.A
2.C
3.D
4.B
5.D
6.A
7.A