写基础排序算法的原因
一是因为这是基础,然后再就是我有时候闲的没事会默想,然后就有的细节不确定了,我又是一个比较追求完美的人,我就又会去看。然后过一段时间有的细节又记不住了,周而复始。思路当然晓得,但是不能一次把细节都想对,就感觉很不完美。
所以我要写在我的博客上面,顺便把经常记不住的“不完美”细节标注(就直接标注成注释了)一下。
说明
- 引用了一些第四版《算法》的观点。(比如一些算法优缺点)
- 以从小到大排序为例。
- 算法稳定性:算法能够保存数组中重复元素的相对位置,称为稳定。
- 基础的交换操作被这几个算法都调用了,代码如下:
//基础交换操作
void exchange(int *a, int i, int j)
{
a[i] ^= a[j];
a[j] ^= a[i];
a[i] ^= a[j];
}
一、选择排序
主要思路就是再乱序部分选择一个最小的放到有序的部分。第一次找一个最小的放第一个,第二次除了第一个(有序)再找一个最小的放在第二个,第三次除了第一第二个(有序)再找一个最小的放在第三个,……
通过两个循环实现,第一个循环代表第几次找,第二个循环用来找到乱序部分的最小数。
不稳定,时间复杂度N方,空间复杂度1(常数)。
代码如下:
void selectSort(int *a, int n)
{
int sub = 0;
for(int i=0;i<n;++i)
{
sub = i;
for(int j=i; j<n; ++j)
{
if(a[sub]>a[j]) //这里是sub和j比较,经常写错成i
{
sub = j;
}
}
if(sub != i) //交换之前,最好自检,否则sub等于i时,出错
exchange(a,sub,i);
}
}
二、冒泡排序
主要思路从头开始,向尾部比较、交换,把较大的数值交换到尾部去,以此实现从小到大的排列顺序。
通过两个循环实现。第一个循环可以看作是比较遍数,n个值比较n-1遍;第二个循环用来把较大的数值“浮动”到尾部去,注意第二个循环中不需要与有序部分作比较,所以判断条件为j<n-1-i,i在这里是指尾部的有序个数。
冒泡排序慢,但稳定,空间复杂度N方。
void bubbleSort(int *a, int n)
{
for(int i=0; i<n-1; ++i)
{
for(int j=0; j<n-1-i; ++j)
{
if(a[j]>a[j+1])
{
exchange(a,j,j+1);
}
}
}
}
三、插入排序
主要思路是把一个数值插入到有序的部分,从有序部分的尾部开始比较,如果小于就交换,然后往前比较交换,一直到这个数值大于有序数值,就是它正确的位置了。
依旧是两个循环。第一个循环是确定要插入的数值,这个数值前面是有序数列,后面是乱序数列。第二个循环用来比较和交换。
适合对已经部分有序的数列进行排序;稳定,时间复杂度介于N和N方之间,空间复杂度为常数。
代码如下:
void insertSort(int *a, int n)
{
for(int i=0; i<n; ++i)
{
for(int j=i; j>0; --j)
{
if(a[j]<a[j-1])
{
exchange(a,j,j-1);
}
}
}
}
四、希尔排序
希尔排序就是进行了很多遍插入排序,不过存在一个增量,跳跃式的分组进行插入排序。虽然同样是“插入排序”的原理,但当数据量相当大的时候,希尔排序的效率应该是大于插入排序的,数据量小的时候不确定。另外增量的计算方法一般通过经验确定,这里用了第四版《算法》里的增量计算方式。
适合对已经部分有序的数列进行排序,且数据量相当大。不稳定,时间复杂度为NlogN,空间复杂度为常数。
代码如下:
void hillSort(int *a, int n)
{
int h=1;
while(h<n/3)
h = 3*h+1;
while(h>=1)
{
for(int i=h; i<n; i++) //++i
{
for(int j=i; j>=h; j-=h) //j>=h,注意等号,因为每一遍要与a[0]比较
{
if(a[j]<a[j-h])
{
exchange(a,j,j-h);
}
}
}
h /= 3;
}
qDebug()<<count;
}
五、归并排序
归并排序是分而治之的思想,这里写从上至下的递归算法,从下之上的虽然写起来简约但是感觉不直观。主要思路是每次将数列对半分,分到不可分为止,然后按对半分的顺序依次合并,在合并的过程中排序。在合并过程中用到了额外的一些空间,用来辅助。
稳定,时间复杂度NlogN,空间复杂度为N。如果稳定性很重要而空间又不是问题,归并排序很好,可以考虑。
void merge(int *a, int low, int mid, int hi)
{
int i=low;
int j=mid+1;
int *auk;
auk = (int*)malloc(sizeof(int)*ARRAYNUM);//辅助数组这里是局部变量,其实声明成全局变量更方便。
for(int k=low; k<=hi; ++k)
{
auk[k] = a[k];
}
//这里low和hi数组长度无关,所以注意k<=hi,要有等号
for(int k=low; k<=hi; ++k)
{
if(i>mid)
a[k] = auk[j++];
else if(j>hi)
a[k] = auk[i++];
else if(auk[i]>auk[j]) //辅助数组比较,不是原数组
a[k] = auk[j++];
else
a[k] = auk[i++];
}
free(auk);
}
void lumpSort(int *a, int low, int hi)
{
if(low >= hi)
return;
int mid = low+(hi-low)/2;
lumpSort(a, low, mid);
lumpSort(a, mid+1, hi);
merge(a, low, mid, hi);
}
六、快速排序
大名鼎鼎的快速排序,相当通用的算法。快速排序也是分治的思想。主要思路:在乱序部分中,选择一个基准值(这里选的是第一个),将比基准值小的数值放到左边,比基准值大的数值放到右边。然后分别以左边(较小的部分)和右边(较大的部分)作为上面描述中的乱序部分,继续选择基准值调整位置。
这里写的是递归的快速排序,因为感觉递归比较直观。
不稳定,时间复杂度为NlongN,空间复杂度为lgN。快速排序比较快的原因是内循环中指令比较少,是很多情况下的最佳选择。
int partition2(int *a, int low, int hi)
{
int key = a[low];
int i=low;
int j=hi+2;
while(1)
{
while(key>a[++i])
if(i>hi)
break;
while(key<a[--j])
if(j<low)
break;
if(i>=j)
break;
exchange(a,i,j);
}
exchange(a,low,j); //交换low和j,我经常写成low和i。
return j;
}
void quickSort(int *a, int low, int hi)
{
if(low >= hi)
return;
int j = partition2(a, low, hi);
quickSort(a, low, j-1); //注意这里两边继续递归都不再包括已经排序好的基准值a[j]了
quickSort(a, j+1, hi);
}
小结
我觉得了解主要思路,然后写出来算法,有些许细节不对应该没关系,再改就是了,但是我可能是有点轻微强迫症。