浅谈排序

浅谈排序

说到排序,那必然得先确定一种排序依据,或为数字大小,或为事物发生先后之顺序,或为父子等辈分关系。我们将所有要排序的元素放在一个集合里面,若这个集合是自反的,反对称,传递的,我们就称在这个集合上具有某种偏序R关系。也就是说只有满足这个条件,我们才能对这个集合做关于R关系的排序。

现实世界中,很多待排序的对象不是单一的数而是一个记录,其中的某个关键字域key,它是排序的依据。记录的其他数据称为卫星数据,它们以关键字为中心,当关键字移动时,卫星数据也要一起移动。

评价排序算法好坏的标准主要有两条:

1、执行时间和所需的辅助空间。执行时间主要与关键字的比较和移动次数有关。

2、算法本身的复杂程度。

算法

1.1简单插入排序

1.2希尔排序

2.1 冒泡排序

2.2快速排序

3.1 选择排序

3.2堆排序

4.1 归并排序

5.1 计数排序

5.2 桶排序

5.3基数排序

1.1简单插入排序

r[1...n],将其分为两个部分r[1....i-1]r[i...n],其中r[1....i-1]排好序,依次选取r[i...n]中的数据,插入到r[1....i]中,是其继续保持有序。

void insertsort(record r[],int n)

{

for(i=2;i<=n;++i)

{

if(r[i-1]>r[i])

/*如果新纪录比最后一个小,插入排好的序列*/

{

r[0]=r[i];

for(j=i-1;r[0]<r[j];--j)

r[j+1]=r[j];/*记录后移*/

r[j]=r[0];

}

}

}

若此序列是排好的序列,显然需要n次比较。若是最差情况,第i个需要比较i次, =n(n+1)/2,平均为,所以其复杂度为O(n^2),辅助空间复杂度为O(1)。

可以进行改进,因为r[1..i-1]为有序,可对其进行二分查找,使第i个查找的时间变为log(i),其最差情况为。

void BIinsertsort(record r[],int n)

{

for(i=2;i<=n;++i)

{

if(r[i-1]>r[i])

/*如果新纪录比最后一个下小,插入排好的序列*/

{

r[0]=r[i];

low=1,high=i-1;

while(low<=high)/*二分查找插入位置*/

{

m=low+(high-low)/2

if(a[m]>a[0])

high=m-1;

else

low=m+1;

}

for(j=i-1;j>=low;--j)

r[low+1]=r[low];/*往后移动*/

r[low]=r[o];

}

}

}

1.2希尔排序

希尔排序又称“缩小增量法排序”,算法思想是将序列不断的按间隔分成若干小组,然后对每一组进行排序。是由插入排序改变来的。

先取间隔d1,然后去d2(d2<d1),直至di<1为止。

希尔提出的取法是d1=n/2di=dm/2BYm=i-1);克努特则提出di=dm/3BYm=i-1);

void shellsort(record r[],int n)

{

int bool;

int i,j,d;

int x;

d=n;

do

{

d=d/2;

do

{

bool=1;

for(i=1;i<=n-d;i++)

{

j=i+d;

if(a[i]>a[j])

{

x=a[i];

a[i]=a[j];

a[j]=x;

bool=0;

}

}

}while(!bool);

}while(d>1)

}

希尔排序平均比较次数与移动次数都是n^1.3,希尔排序的复杂度依赖于所选取的di序列,一般认为是Onlogn),希尔排序不稳定。

2.1 冒泡排序

void bubblesort(record r[],int n)

{

int i,j,flag;

flag=1;

for(i=1;i<n&&flag==1;i++)

{

flag=0;

for(j=0;j<n-i;j++)

{

if(r[i]>r[i+1])

{

swap(r[i],r[i+1]);

flag=1;

}

}

}

}

冒泡排序的第一个循环n是控制冒泡次数为n-1,若某次冒泡过程没有发生数字移动,则排序成功。第二重循环,冒泡原理,使大的数一直往右冒泡,第一次冒泡最大,依次类推。简单看出来,若排好序,则只需要n-1次比较。若是逆序,则比较次数为n*n-1/2,移动次数3*n*n-1/2(每次比较后交换需要移动三次)。所以复杂度为On^2)。

2.2快速排序

快速排序是一种基于分治策略的一种算法,先选取序列中的一个数r[i]为基准,将数组分为大于等于r[i],小于等于r[i]r[i],然后对左右两个区间继续进行递归选取基准分组。

由算法可以看出,实现快速排序最自然的方法是进行递归实现。

void quicksort(record r[i],int l,int r)

{

int i;

if(r<=1) return ;

i=partition(a,l,r);

quicksort(a,l,i-1);

quicksort(a,i+1,r);

}

int partition(record r[],int l,int r)

{

int i=l,j=r;

record v=a[r];

while(i<j)

{

while(a[i]<v&&i<j) i++;/*小于a[i],须得有i<j这一判定条件,否则当有序时会一直往后跑*/

while(a[j]>v&&i<j) j--;/*大于a[i]*/

swap(a[i],a[j]);

}

swap(a[i],a[r]);

return i;

}

在最坏情况下,每次分化都不对称划分

Tn={O1),n<=1;T(n-1)+O(n),n>1},可以解得Tn=n^2

当处于最好情况下时,每次划分都是对称均匀的,

Tn={O1),n<=1;2T(n/2)+O(n),n>1}

可得T(n)=O(n logn);

也可以理解快速排序是递归实现的,在系统内部需要用一个栈来实现。若每次都分布均匀,则递归树的高度为logn,则复杂度为O(nlogn)。最坏情况下,即刚好有序的情况下,则递归树的高度为n,则复杂度为O(n^n)。所以可以改进,取头、中、尾,取中间值,来做基准。

一般而言,快速排序的复杂度为O(nlogn),优于大部分算法,当递归进行比较慢时,可以转换成非递归形式;当n还很小是,用快速排序不合算,一般n>20,才使用快速排序。快速排序不是一种稳定的排序方法。

3.1 选择排序

选择排序是将序列中最小的数选出来,然后和第一个数交换,然后划去第一个数所在位置,再在剩下数种选最小,放在第二个数的位置,依次进行。

不难看出复杂度为 n*n-1),不给代码,可以自己实现。

选择排序还有一种树形选择,将所给数组依次排列,作为叶节点,每顺序两个为一组,选出较小者,变成此两个的根节点,依次推导,树的高度易得为logn,所以复杂度为Onlogn),但实际上所需要的辅助空间和存放中间结果的空间较复杂,难实现。故一般不用来排序,只用来做某些证明。

3.2堆排序

堆是利用完全二叉树的性质,使其具有鲜明的结构特点,r[i]的子节点为r[2*i]r[2*i+1]。成为一个堆的充要条件是,r[i]是其与两个子节点中的最小值(此时成为小根堆),或者最大值(大根堆)。很显然,我们要建堆,首先得把数据按完全二叉树的的结构特点进行树形排列。接下来就是对其调整,使其满足堆的性质,若是自顶向下进行调整,由于无法保证其子树是堆,还得继续往下寻找。由此,我们不难推断出,堆的调整要从其序号最大的其子树接满足堆性质的节点开始,也就是从最后一个子节点是叶子节点的根节点开始进行调整,也就是n/2的那个节点,很容易理解,总共有n个节点,也就是最后一个叶子节点序号为n,便必须从其父亲节点开始,也就是n/2

void heapadjust(record r[],int i,int n)

{

if(i<=n/2)

{

int min=i;

//比较找出根结点与两个子节点中最小值,记录位置

if(r[i]>r[2*i]&&2*i<=n)

min=2*i;

if(r[min]>r[2*i+1]&&2*i+1<=n)

min=2*i+1;

if(min!=i)

{

swap(r[i],r[min]);

heapadjust(r,min,n);//有交换,继续深入,使子树依旧为堆

}

    }

}

void buildheap(record r[],int n)//建堆

{

int i;

for(i=n/2;i>0;i--)//in/2的原因使使其子树都是堆

{

heapadjust(r,i,n);

}

}

void heapsort(record r[],int n)//输出逆向序列

{

int i;

for(i=n;i>=1;i--)

{

swap(r[1],r[i]);

heapadjust(r,1,i-1);

}

}

很显然,这是一种不稳定的排序,因为若是中间有与最后一个数相同的数,我们一次取堆首元素后将最后一个元素调到堆顶,无法使其稳定。

分析其复杂度,其树高度是logn,所以为Onlogn)。也可由递归方程

Tn={O1),n<=1;2T(n/2)+O(n),n>1}求解得到,求解方法是由下往上递推。

4.1 归并排序

何为归并,简单说就是把两个有序集合合并成一个有序集合,很显然,只需要遍历两个集合就可以实现这种归并。给我们一组无序的数据。我们这样想,当我们把这组数据一分为二,二分为四,依次进行,直到最后的子集只存在一个数,此时,我们可以两两集合进行归并,这就是我理解的归并排序。很显然,按照对算法的理解,最自然的方法是递归实现。

void merge(record r[],int l,int m,int h)

{

int i,j,k;

//使b[]r[]中的lm的正序和m+1h的倒叙

for(i=m;i<=l;i--) b[i]=r[i];

for(j=m+1;j<=h;j++) b(h-(j-(m+1)))=r[j];

for(k=1;k<=h;k++)

{

if(b[j]>=b[i]) r[k]=b[i++];

else           r[k]=b[j--];

}

}

void mergesort(record r[],int l,int h)

{

int m=(l+h)/2;

if(h<=1);

mergesort(r,l,m);

mergesort(r,m+1,h);

merge(r,l,m,h);

}

按照我们对算法的描述与实现,我们发现和堆排序很类似的递归,

Tn={O1),n<=1;2T(n/2)+O(n),n>1},由此也可解出,其复杂度为O(nlogn),按照算法实现的过程我们可以看出,这是一个稳定的排序。

5.1 计数排序

我们开一个很大的数组c[],其范围大于待排序数组a的最大值,将待排序数组的值映射到c数组中,即c[a[i]],然后统计映射到同一个位置的个数,c[i]=c[i]+c[i-1],求得c[i]表示a中小于等于i的元素的个数,再通过一次映射取出a[i]所在位置,即有几个小于等于它的数,它就该排在第几个。每次映射一个a[i]出来,就使c[a[i]]--,即剩下待排数组里小于等于a[i]的数少一个了。而对于剩余待排数组里大于a[i]的数,取出小于它的数,不会影响它排序的最终位置,故不做修改。

#define m 10000

void countsort(int a[],int b[],int n)

{

int c[m+1],i;

for(i=0;i<=m;i++) c[i]=0;

for(i=0;i<n;i++)  c[a[i]]++;

//计算出c[i]表示小于等于i的数的总数

for(i=1;i<=m;i++) c[i]=c[i]+c[i-1];

for(i=n-1;i>=0;i--)

{

b[c[a[i]]-1]=a[i];

c[a[i]]--;

}

}

从代码我们可以看出,这是一种稳定的排序方法。其复杂度,为Om+n)。

5.2 桶排序

m个桶排成一列,将关键字等于i的放入第i个桶,然后从第一个桶开始,顺序输出。

初始化桶和输出都需要O(m),将元素装入桶需要O(n),故复杂度为O(m+n)。在此就不实现了。

5.3基数排序

基数排序,若以三位数为例子,我们先将个位的数排列,再十位,再百位。这样称作LSDLeast  Significant Digital)。若是从百位开始,则称为MSDMost Significant Digital),一般对数字我们采用LSD,因为若采用MSD,则需要将百位的先分割成不同片,然后再每一片区域内再次分割。而采用LSD,则可以分别取出个位,十位,百位的数字,然后进行计数排序。其复杂度为Od*(r+n)),其中d为位数,n为由几个数,而r为基数,比如对于数字,其只有0-9十种可能,则其基数为10。由于时间仓促,就先不给代码了,等寒假有时间再敲一下。

 

总结:

对于排序算法,我们根据几种基础的算法,选择排序,冒泡排序,插入排序,计数排序而衍生出其他的排序算法。我们根据不同的要求,不同的限制条件下,尽量选择更优的算法,这就要求我们对算法有一个全面的认知。熟悉每一种算法的原理与实现过程。比如,一个排序算法是不是稳定的,靠死记硬背我觉得意义不大,这个时刻,你需要回想一下原理,实现步骤。而分析复杂度的时候,也不要去背,毫无意义。介绍的排序算法也就这几种,每一种回想一下过程,便能想出其复杂度。算法浩瀚如烟,我们无法了解全面,这就要求我们对基本的思想,实现烂熟于胸,解问题,自然有其新意。赵括言:半部论语可治天下。虽不全对,但也有其深意。

诸君共勉。

猜你喜欢

转载自blog.csdn.net/waaa_fool/article/details/79015007