常见排序算法总结及C语言实现

一直没有好好的扎扎实实的算法的基础,要找工作了,临时抱下佛脚,顺便把学的东西整理下,以应对比较健忘的大脑。。。

废话不说,直接主题,其实整理这个,借鉴了不少这个blog,http://www.cppblog.com/shongbee2/archive/2009/04/25/81058.html 在此再次感谢这个博主,但愿有一天,自己也能请博主喝杯咖啡 哈哈~ 

先从最熟悉的冒泡排序开始吧:

冒泡排序(BubbleSort)

冒泡排序也是我接触到的最早的排序,其基本思想是把数组中每个元素都看成是有质量的气泡,每个气泡的质量就是数组对应位置的元素的大小。排序的过程就是不停的把质量较轻(元素较小)的气泡上浮的过程。第一次循环最小的元素起泡到第一个位置,第二次循环,次小的元素被放到正确的位置,这样n-1次循环之后,数组就是有序的了。值得说明的是,冒泡排序是原地排序的过程,即不需要额外的内存空间,时间复杂度为O(N^2),且是稳定的排序。

源代码:

int BubbleSort(int *pData,int len)
{//冒泡排序,pData 是待排序的数组,len是数组的长度
	bool isOk=false;//这是一个优化,标志数组是否有序
	for(int i=0;i<len-1&&!isOk;i++)//外层循环控制起泡循环的次数
	{
		isOk=true;//初始化为有序的
		for(int j=len-1;j>i;j--)//从最后一个元素开始,一次循环的结果是使第i小的元素出现在第i个位置上
		{
			if(pData[j]<pData[j-1])//如果第j-1个元素比第j个元素小,则交换.
			{
				int temp=pData[j];
				pData[j]=pData[j-1];
				pData[j-1]=temp;
				isOk =false;//有交换发生,说明数组是无序的,即排序没有完成.
			}
		}
	}
	return 1;
}

快速排序(QuickSort)

快速排序和冒泡排序一样,都是基于交换的排序。但是值得注意的是快速排序用到了分治的思想。

利用分治的思想可以这样描述快排:对于待排序的无序的数组pData[1,2,...n],分解:任选其中一个元素做为键值进行划分,找到划分位置i,使得i左边的元素都小于等于找到的键值,i右边的元素都大于等于键值。 求解:递归的对i左边的元素和i右边的元素进行快速排序;组合:由于求解过程中递归步骤结束后,左右两部分都已经有序,这样对于排序来说,组合的过程就不需要操作了。可以看成是空操作。同样说明的是快速排序也是原地排序,且时间复杂度比冒泡排序低最好的情况是O(nlog(n)),最坏情况是O(N^2),是不稳定的排序。

源代码

int Partition(int *pData,int begining,int end)
{//划分的过程 pData是待排序的无序数组,begining是开始位置,end是结束位置,函数返回以起始位置元素做为键值的划分的位置
	int i = end,j;//i初始化为最后一个位置+1
	int nd=pData[begining-1];//选定起始位置为键值
	int tmp;
	for (j=end-1;j>begining-1;j--)//j从最后一个元素开始,到第二个元素结束
	{
		if(pData[j]>=nd)//如果pData[j]比选的键值大,那么i-1,同时交换pData[j]和pData[i],这样保证i右边的元素永远比键值大,同时i-1位置元素比键值小
		{
			--i;
			tmp=pData[i];
			pData[i]=pData[j];
			pData[j]=tmp;
		}
	}
	--i;//循环结束后i左边的元素都比键值小,而且i右边的元素(包括i位置元素)都比键值大,所以i值减一,同时和键值交换
	pData[begining-1]=pData[i];
	pData[i]=nd;
	return i+1;//考虑到数组下标是从零开始的
}
int QuickSortRecursion(int *pData,int begining,int end)
{
	{
		if (begining>=end)//终止条件
			return 1;
	}
	int i=Partition(pData,begining,end);//划分
	QuickSortRecursion(pData,begining,i);//递归的排序左半部分
	QuickSortRecursion(pData,i+1,end);//递归的排序右半部分
	return 1;
}
 int QuickSort(int *pData,int len)
 {
	 QuickSortRecursion(pData,1,len);
	 return 1;
 }

选择排序(SelectSort)

选择排序的基本思想和冒泡排序有一点像,都是依次选择最小元素,次小元素…… ,但是和冒泡排序不同的是,冒泡排序发现小的就进行互换,但是选择排序是直接把小的放到应该出现的位置。选择排序的过程如下:

n个元素的数组可以经过n-1趟选择排序后得到有序的结果:初始状态:无序区pData[1,2,...n],有序区间为空。第一趟排序:在无序区pData[1,2,...n]中选择最小的元素pData[k],将他与第一个元素pData[1]交换,使得pData[1...1]和pData[2,3,...n],分别表示有序区和无序区。每一次循环,有序区的长度加一,同时无序区长度减一。这样n个元素的序列可以通过n-1趟排序得到有序的序列。选择排序的时间复杂度为O(N^2),是原地排序,且不稳定排序。

int SelectSort(int *pData,int num)
{//选择排序,pData是待排序数组,num是数组的长度

	for (int i=0;i<num-1;i++)//控制循环次数 num-1次循环即可得到有序序列
	{
		int index=i;//初始化最小元素的下标
		for(int j=i+1;j<num;j++)//从i+1个位置开始
		{
			if(pData[j]<pData[index])//如果有比当前最小值更小的元素,更新指向最小元素的索引
				index=j;
		}//循环结束后,index指向的是无序区最小元素的索引
		if(index!=i)//如果第i个元素不是当前循环最小元素,则交换
		{
			int temp=pData[i];
			pData[i]=pData[index];
			pData[index]=temp;
		}
	}
	return 1;
}

插入排序(InsertionSort)

1、基本思想
     假设待排序的记录存放在数组R[1..n]中。初始时,R[1]自成1个有序区,无序区为R[2..n]。从i=2起直至i=n为止,依次将R[i]插入当前的有序区R[1..i-1]中,生成含n个记录的有序区。
2、第i-1趟直接插入排序:
     通常将一个记录R[i](i=2,3,…,n-1)插入到当前的有序区,使得插入后仍保证该区间里的记录是按关键字有序的操作称第i-1趟直接插入排序。
     排序过程的某一中间时刻,R被划分成两个子区间R[1..i-1](已排好序的有序区)和R[i..n](当前未排序的部分,可称无序区)。
     直接插入排序的基本操作是将当前无序区的第1个记录R[i]插人到有序区R[1..i-1]中适当的位置上,使R[1..i]变为新的有序区。因为这种方法每次使有序区增加1个记录,通常称增量法。
     插入排序与打扑克时整理手上的牌非常类似。摸来的第1张牌无须整理,此后每次从桌上的牌(无序区)中摸最上面的1张并插入左手的牌(有序区)中正确的位置上。为了找到这个正确的位置,须自左向右(或自右向左)将摸来的牌与左手中已有的牌逐一比较。

插入排序的时间复杂度是O(N^2),是原地排序,且是稳定的排序。

int InsertionSort(int *pData,int num)
{//插入排序
	int i,j,key;
	for(j=1;j<num;j++)//循环次数
	{
		key=pData[j];//把第j个元素插入到前j-1个有序的序列中
		i=j-1;//循环的初始
		while(i>=0&&pData[i]>key)
		{
			pData[i+1]=pData[i];//从后往前遍历,若比key值大,则元素后移(为后面的插入做准备)
			i--;
		}//找到第一个比key值小的位置
		pData[i+1]=key;//key插入到第i一个比key小的元素的位置之后
	}
	return 1;
}
插入排序的一个变种是希尔排序,用到了分治的思想,其实这个我也不是很懂,附上源代码,供大家参考

int SortGroup(int *pnData,int nlen,int nBegin,int nStep)
{
	for(int i=nBegin+nStep;i<nlen;i+=nStep)
	{
		for (int j=nBegin;j<i;j+=nStep)
		{
			if(pnData[i]<pnData[j])
			{
				int nTemp=pnData[i];
				for(int k=i;k>j;k-=nStep)
				{
					pnData[k]=pnData[k-nStep];
				}
				pnData[j]=nStep;
			}
		}
	}
	return 1;
}

int ShellSort(int *pnData,int nLen)
{
	for(int nStep=nLen/2;nStep>0;nStep/=2)
	{
		for(int i=0;i<nStep;++i)
		{
			SortGroup(pnData,nLen,i,nStep);
		}
	}
	return 1;
}

归并排序(MergeSort)
归并排序的主要思想是分治

1、算法基本思路
     设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。
(1)合并过程
     合并过程中,设置i,j和p三个指针,其初值分别指向这三个记录区的起始位置。合并时依次比较R[i]和R[j]的关键字,取关键字较小的记录复制到R1[p]中,然后将被复制记录的指针i或j加1,以及指向复制位置的指针p加1。
     重复这一过程直至两个输入的子文件有一个已全部复制完毕(不妨称其为空),此时将另一非空的子文件中剩余记录依次复制到R1中即可。
(2)动态申请R1

     实现时,R1是动态申请的,因为申请的空间可能很大,故须加入申请空间是否成功的处理。

归并排序的时间复杂度是O(nlog(n))的,但是是需要额外的空间的O(N),同时是稳定的排序

int Merge(int *pData,int nP,int nM,int nR)
{//归并pData中nP到nM和nM到nR的元素
	int nLen1=nM-nP;
	int nLen2=nR-nM;
	int *pnD1=new int[nLen1];
	int *pnD2=new int[nLen2];
	int i,j;
	for(i=0;i<nLen1;++i)
	{
		pnD1[i]=pData[nP+i];//复制pData中nP到nM的元素

	}
	for (j=0;j<nLen2;j++)
	{
		pnD2[j]=pData[nM+j];//复制pData中nM到nR的元素

	}
	i=j=0;
	while(i<nLen1&&j<nLen2)//归并过程
	{
		if (pnD1[i]<pnD2[j])
		{
			pData[nP]=pnD1[i];
			++i;
		}
		else
		{
			pData[nP]=pnD2[j];
			j++;
		}
		nP++;
	}
	if (i<nLen1)//如果j先结束,复制i剩下的元素
	{
		while(nP<nR)
		{
			pData[nP++]=pnD1[i++];
		}
	}
	else//复制j剩下的元素
	{
		while(nP<nR)
		{
			pData[nP++]=pnD2[j++];
		}
	}
delete pnD1;
delete pnD2;
return 1;
}

int MergeRecursion(int *pData,int nBegin,int nEnd)
{//递归调用
	if (nBegin>=nEnd-1)
	{
		return 0;
	}
	int nMid=(nBegin+nEnd)/2;//中间位置
	MergeRecursion(pData,nBegin,nMid);//递归求解前半部分
	MergeRecursion(pData,nMid,nEnd);//递归求解后banbuf
	Merge(pData,nBegin,nMid,nEnd);//组合,归并求解
	return 1;
}
int MergeSort(int *pData,int Len)
{
	return MergeRecursion(pData,0,Len);
}
堆排序(HeapSort)

 堆的定义

     n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质):(1) ki≤K2i且ki≤K2i+1 或(2)Ki≥K2i且ki≥K2i+1(1≤i≤(n/2) )满足(1)的叫做最小堆,满足(2)的称作最大堆。

 堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
(1)用大根堆排序的基本思想
先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。

    ……
直到无序区只有一个元素为止。

(2)大根堆排序算法的基本操作:

① 初始化操作:将R[1..n]构造为初始堆;

② 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
  注意:
①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。
②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻,堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。

堆排序是原地进行的,时间复杂度为O(nlog(n)),是不稳定的排序。

void swap(int &i,int &j)
{//交换两个元素的值,纯属装X,从别地看到的不用第三个变量就能互换的方法
	if (i!=j)
	{
		i^=j;
		j^=i;
		i^=j;
	}
}

int MaxHeapIfy(int *pData,int nPose,int nLen)
{//保证以nPose 为根的子树为最大堆,算法的前提是以Left[nPose]和Right[nPose]为根的子树都是最大堆
	int nMax=-1;//最大位置的索引
	int lChild=2*nPose+1;//考虑到数组下标从零开始,左子树的根的位置
	int rChild=2*nPose+2;//右子树根的位置
	if(lChild<nLen&&pData[lChild]>pData[nPose])
		nMax=lChild;//如果左孩子比根大,违反最大堆的定义,设定nMax为左孩子的索引
	else
		nMax=nPose;
	if (rChild<nLen&&pData[rChild]>pData[nMax])
		nMax=rChild;//同理 若右孩子比根大,设定nMax为右孩子的索引
	if(nMax!=nPose)//如果nPose不是最大
	{
		swap(pData[nPose],pData[nMax]);//交换nPose和nMax的值
		MaxHeapIfy(pData,nMax,nLen);//交换后,可能以你年Max位置为根的子树不满足最大堆条件,递归的调用
	}
	else
	{
		return 1;
	}
	return 1;
}
int BuildMaxHeap(int *pData,int nLen)
{//建立最大堆
	for(int i=nLen/2;i>=0;i--)//由完全二叉树的性质知,后n/2的元素都是叶子节点,自成一个最大堆
		MaxHeapIfy(pData,i,nLen);//自底向上的建堆
	return 1;
}

int HeapSort(int *pData,int nLen)
{
	BuildMaxHeap(pData,nLen);//建堆
	while(nLen>=1)
	{
		swap(pData[0],pData[nLen-1]);//当堆建好后,第一个元素肯定是最大的元素,交换第一个元素与最后一个元素,这样最大的元素就放在了最后
		--nLen;//nLen-1.对前nLen个元素重复上述过程
		MaxHeapIfy(pData,0,nLen);//交换后,可能第一个元素不满足最大堆条件,调整
	}
	return 1;
}


posted @ 2012-07-19 20:17  JWMNEU  阅读(239)  评论(0编辑  收藏  举报