排序

概述:

如果被排序的文件可以装载于内存中,则称该排序方法为内部排序(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希尔排序

         插入排序速度慢,因为它进行的唯一交换涉及到邻接项,因此,在数组中,项只能一次移动一个位置。例如,如果最小键刚好就在数组的末端,则需要N步将它移动位。希尔排序是插入排序的简单扩展,它允许离得很远的元素进行交换,所以提高了速度。
       希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。 该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。


操作方法:
选择一个增量序列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标志冒泡法

        我们先来看看普通冒泡法的具体过程:

我们从上面可以看到最后4行是相同的,也就是说最后四次我们可以不要了。那为什么最后四行会是一样的呢?我们来分析下冒泡法的原理的过程。冒泡法是每次把剩下数中的最大值给排到最后,但又不仅仅是将最大排到最后,中间过程会有很多排序,所以到最后可能会提前排好。知道了这原因,那我们怎么确定最后停止的位置呢?每次都会把大的往后移,既然最后没有移动了,说明没有交换了,所以我们只要把最后交换的地方给记录下来即可。这就是标记法。程序如下:
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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值