概述:
如果被排序的文件可以装载于内存中,则称该排序方法为内部排序(internal sort)。若排序文件来看磁带或磁盘,则称做外部排序(external sort)。两者的主要区别是,内部排序可以轻松访问任意项,而外部排序必须按顺序访问项,或至少要按大数据块来访问。
我们这里说说八大排序就是内部排序。
当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
1.插入排序
1.1直接插入排序
直接插入排序,就像我们扑克牌一样。当我们拿到一张牌时,我们习惯将这张牌插入到手上已有的牌当中。
示例:
我们知道,该方法是从前面排好了的序列是插入。那做法就是先搜索要排的数在前面的位置,然后将比它大的数往后移动,然后插入即可。我们可以从这一过程中,可以看到三个过程,即搜索-移动-插入。移动和插入很难有所改变,那可以在搜索上下功夫了。我们知道搜索有顺序搜索和二分搜索这两个基本的方法。
插入排序约平均使用N^2/4次比较,N^2/4次半交换(移动),在最坏的情况下,比较与交换次数加倍。从算法思想上很容易看出比较与移动的次数是相同,对于随机输入,每个元素平均移动一半路程就折回,因此,应当计算对角线之下一半的元素。
对于顺序搜索,我们可以将搜索和移动合并同时进行,即从后往前搜索,比较一次就移动一次,直到找到为止,然后插入,即代码为:
//插入排序
void InsertSort(int *d,int n)
{
int i,j;
int tmp;
for(i=1;i<n;i++){
j=i;tmp=d[i];//复制为哨兵,即存储待排序元素
while(j>0&&d[j-1]>tmp)//从后往前找,边找边移动
d[j--]=d[j-1];
d[j]=tmp;//找到后就插入
}
}
最坏时间复杂度:1+2...+n-1=n(n-1)/2
时间复杂度:O(n^2).
1.2二分插入排序
二分插入排序,实际上是将前面的搜索用二分搜索,其它是一样的,其代码为:
void insertDichotomySort(int *d,int n)
{
int i,m,a,b;//a,b为二分法中的头和尾,m为中间索引值
int tmp;//保存要插入的值
for(i=1;i<n;i++){
tmp=d[i];//保存要插入的值
a=0;b=i;//在[0,i]之内搜索,不是[0,i-1]的原因是,可能这个范围内找不到位置,
//但如果包含i,则必定会找到相应的位置,即本身
while(b>a){
m=(a+b)/2;
if(d[m]<d[i])
a=m+1;//由于要插入的值比中间值要大,那中间值必定不是要插入的地方,那至少在这点后面一个点
//同时也避免了最后a与b只相差1时的无穷循环了
else
b=m;//因为这里还包含等于
}
m=i;
while(m>=a)//移动
d[m--]=d[m-1];
d[a]=tmp;//插入
}
}
其实二分搜索插入并不占多少优势,主要是它依然要移动,同时在for循环中加入了if条件判断,使之性能下降。但如果量很大的时候,二分法的优势可能会体现出来
1.3 二路插入排序
void insert2Sort(int *d,int n)
{
int i,j;
int first=0,//只是记录左右端点的索引,而不是个数
last=n;
int *t=new int[n+1];//多申请一个空间,让t[0]和t[n],这样就不必用求余符号,减小计算量
memset(t,0,sizeof(int)*(n+1));
t[0]=d[0];t[n]=d[0];
for(i=1;i<n;i++){
if(d[i]>d[0]){//t[0]的右边插入排序
j=++first;//记录最右边的索引
while(j>0&&t[j-1]>d[i])//从后往前找,边找边移动
t[j--]=t[j-1];
t[j]=d[i];
}
else{
j=--last;
while(j<n&&t[j+1]<d[i])////从前往后找,边找边移动
t[j++]=t[j+1];
t[j]=d[i];
}
}
// for(i=0;i<n;i++)//由于从last到first本身就是由小到大的顺序,所以只需要将t中last到first平移到0到n-1即可
// d[i]=t[(i+last)%n];
memcpy(d,t+last,sizeof(int)*(n-last+1));//利用系统函数平移
memcpy(d+n-last,t,sizeof(int)*(first+1));
}
1.3希尔排序
选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;我们可以简单地设增量序列dk = {n/2 ,n/4, n/8 .....1},也可以是其它方式
按增量序列个数k0,对序列进行k0 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
void shellSort(int *d,int n)
{
//每次步长变化为前一次的一半
for(int dk=n/2;dk>0;dk/=2){
//起始索引每次要加1,但不能超过步长dk,
//即起始索引为0~dk-1,共移动dk次,即要排dk次直接插入排序
for(int k=0;k<dk;k++){
//每一组的直接插入
for(int i=k+dk;i<n;i+=dk){
int tmp=d[i];
int j=i;
while(j>k&&d[j-dk]>tmp){
d[j]=d[j-dk];
j-=dk;
}
d[j]=tmp;
}
}
}
}
2.交换排序
2.1冒泡法
2.1.1基本冒泡法
冒泡法是最简单的排序算法之一,但不是最有效的排序算法之一。从名字上看,就知道和冒泡有关。在水里,大的往下沉,小的往上冒。如果我们把一组数竖着看,小的数跑着前面,大的往后面跑。其实际做法就是,拿出数组中任意一个数,与下一个数比较,如果大,则往后移动;如果小,自身则不动,让下一个值进行比较。若假设a是所有中最大的数,索引到它后,由于它后面的所有数都要比a小,故会一起交换下去,这样一次的排序,就把最大值排到最后。当第二循环时,把第二大的数排到倒数第二位上。依此类推,就排好序了。同时也告诉我们怎么找第n大的数。
时间复杂度:O(n^2).冒泡排序在最坏情况下,约平均使用N^2/2次细瘦,N^2/2次交换。
具体流程如下:
其代码为:
void bubbleSort(int *d,int n)
{
for(int i=0;i<n-1;i++)//要进行n-1次循环比较
for(int j=0;j<n-i-1;j++)//每一次冒泡
if(d[j]>d[j+1])//前一个和后一个比较,如果大于后面那个数,则交换其值
swap(d[j],d[j+1]);
}
2.2标志冒泡法
void bubbleFlagSort(int *d,int n)
{
int pos;
int i=n-1;
while(i>0){//因为我们不能确定循环次数,所以我们用while
pos=0;
for(int j=0;j<i;j++){
if(d[j]>d[j+1]){//记录最后一次交换的位置
swap(d[j],d[j+1]);
pos=j;
}
}
i=pos;
}
}
2.3正反两趟标记冒泡法
前面的两种冒泡只是从一边开始,我们可以像二路插入排序一样,考虑两头冒泡。从左往右时,把大的往后移,然后从右往左移时,把小的往前移,同时采用标记减少循环次数。具体过程如下:
从上面的过程可以看到,首先是将最大值放入最右边,然后把最小值放入最左边,当然这个过程中还有别的交换。图中的R表示从左往右扫,L表示从右往左扫,其值代表最后一
void bubbleFlagTwoSideSort(int *d,int n)
{
int beforePos=0,
afterPos=n-1;//设置前后标志
int ib,ia;//用于前后标志的中间变量
int i;
while(beforePos<afterPos){//如果左标志大于右标志,说明循环已完
ib=afterPos;ia=beforePos;//左右标志的初始值的原则是,如果从头到尾都没有交换的话,那值是多少,很明显
for(i=beforePos;i<afterPos;i++){
if(d[i]>d[i+1]){
swap(d[i],d[i+1]);
ia=i;//其值代表交换中的前一个值,以致afterPos不需要减一了
}
}
afterPos=ia;
// cout<<afterPos<<"R: ";
// Print(d,n);
for(i=afterPos;i>beforePos;i--){
if(d[i-1]>d[i]){
swap(d[i-1],d[i]);
ib=i;//其值代表交换中的后一个值,以致beforePos不需要加一了
}
}
beforePos=ib;
// cout<<beforePos<<"L: ";
// Print(d,n);
}
}
次交换的位置。结合上面两种方法,可以写出如下程序:
2.2快速排序
3 选择排序
3.1 简单选择排序
3.2 堆排序
4 并归排序
5 基数排序
网络资源:
http://kb.cnblogs.com/page/210687/