八大排序算法详解

说明:本文所用算法示意图并非原创,多来自网络。如原创者不同意使用,请与我联系。

特别的,多张图例引用于http://blog.csdn.net/ggxxkkll/article/details/8667429#comments ,非常感谢!

排序算法:

       1.本文专门总结内部排序算法(只访问内存),外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

       2.在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的;若具有相同关键字的记录之间的相对次序发生改变,则称这种排序方法是不稳定的。
要注意的是,排序算法的稳定性是针对所有输入实例而言的。即在所有可能的输入实例中,只要有一个实例使得算法不满足稳定性要求,则该排序算法就是不稳定的。

       3.本文所述稳定与否其实与算法的实现有很大的关系,一个不稳定的算法如果多加一个域进行比较又可以变稳定,因此本文稳定性分析只针对最朴素算法而言,并非改良算法。

      当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
      快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;

一、插入排序---直接插入排序(Insertion Sort)


       算法过程:每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。

       算法复杂度:时间复杂度O(N^2),空间复杂度O(1)。

       性能分析:最佳效率O(N),最糟效率O(N^2)与冒泡、选择相同,适用于排序小列表,若列表基本有序,则插入排序比冒泡、选择更有效率。

       稳定性分析:如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

       代码如下:

/*
排序算法1:直接插入排序
将一个待排数据按大小插到已排序数据的适当位置,直到全部插入完毕。
算法复杂度:O(N^2)
*/
void insertion_sort(int num[],int numLen)
{
	for(int j=1 ; j<numLen ; j++){
		int key = num[j];//取未排序的数
		int i=j-1;//排好序数组的末位
		while(num[i]>key && i>=0){
			//---数组移位
			num[i+1] = num[i];//向右移
			i--;
		}
		//---第一个比key小(或相等)的数位置为i,将key插在i+1处
		num[i+1]=key;
	}
}

二、插入排序---希尔排序(Shell`s Sort)


       本图中增量取5,3,1.

       算法过程:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。一般的初次取序列的一半为增量,以后每次减半,直到增量为1。子序列不是逐段分割的,而是相隔特定的增量的子序列,对各个子序列进行插入排序;然后再选择一个更小的增量,再将数组分割为更多个子序列进行排序......

       算法复杂度:比较次数依赖于增量的选取,大约为O(NlogN)~ O(N^1.5),空间复杂度O(1)。

       性能分析:插入排序的改进。比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。没有快排快,中等规模下表现好,但是它好在最坏情况和平均情况差不多,快排最坏情况效率很低。建议任何排序都可以先希尔排序,如果事实证明不够快再改成快排。

       ①当数据基本有序时直接插入排序所需的比较和移动次数均较少。
       ②在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。

       稳定性分析:因为排序过程中元素可能会前后跳跃,所以不稳定。

       代码如下:

/*
排序算法2:希尔排序
将相隔一定增量元素按照小组对待,然后组内直接插入排序。当增量逐渐递减到1的时候
说明排序结束,此时所有数组元素为一个小组。
*/
void shellInsert(int num[] , int numLen , int increment)//按照分组插入排序
{
	for(int i=increment ; i<numLen ; i++){//遍历分组的无序数组
		int key = num[i];
		int j=i-increment;//分组的有序数组末位
		while(num[j]>key && j>=0){
			num[j+increment] = num[j];
			j=j-increment;//组内偏移量
		}
		//---小于等于待插数key的有序数组中第一位为j位
		num[j+increment] = key;
	}
}
void shellSort(int num[] , int numLen)//增量折半递减
{
	int d = numLen/2;
	while(d>=1){//直到增量为1排序结束
		shellInsert(num,numLen,d);//分组插入排序
		d=d/2;//每次增量折半
	}
}

三、交换排序---冒泡排序(Bubble Sort)


        算法过程:比较相邻的前后二个数据,如果前面数据大于后面的数据,就将二个数据交换;这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1个位置。重新令N=N-1,重复前面二步直到N变为0。

        算法复杂度:O(N^2),空间复杂度O(1)

        性能分析:最好情况下(正序)只需要遍历一次,不做交换,复杂度O(N)。最坏情况(逆序),需要遍历n(n-1)/2个元素,每个元素需要交换一次(访问3次),故最坏情况下复杂度3n(n-1)/2=O(N^2),因此平均时间复杂度O(N^2)。

        稳定性分析:由于算法只在前面元素大于后面时候才交换,相等情况下不交换,因此算法稳定。

        代码如下:

/*
排序算法3:冒泡排序
从[0,N-1]遍历,如果前面的数比后面的数大则交换位置(让大数下沉),经过一轮后最大数沉到N-1处。
下一次迭代从[0.N-2]遍历···直到排序结束
*/
void bubbleSort(int num[],int numLen)
{
	for(int i=numLen-1 ; i>=0 ; i--)//遍历的下界不断缩小至0结束排序
	{
		for(int j=0 ; j<i ; j++){
			if(num[j]>num[j+1]){//交换次序
				int temp = num[j];
				num[j] = num[j+1];
				num[j+1] = temp;
			}
		}
	}
}

四、选择排序---简单选择排序(Simple Selection Sort)


       算法过程:在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。

       算法复杂度:时间复杂度O(N^2),空间复杂度O(1)。

       性能分析:冒泡法的改进,找到最合适的那个位置再交换,只需交换一次就能找到一个数的合适位置,而冒泡排序对一个数下沉或上浮到合适位置需要交换若干次,即数据的移动次数少,但是遍历次数多了。对最好情况(正序),还是需要遍历数据寻找最小值、作比较等等···,因此无论最坏还是最佳情况下,时间复杂度都是O(N^2)。

       稳定性分析:由于有交换的存在(而不是插入),例2,4(1),4(2)排序后由于交换会变成2,4(2),4(1),因此不稳定。

       代码如下:

/*
排序算法4:简单选择排序
从[1,n-1]找最小的数与num[0]比较,如果更小就交换。下一次迭代从[2,n-1]中找最小数与num[1]相比,如果更小就交换···
直到排序结束
*/
void simpleSelectSort(int num[] , int numLen)
{
	int index = -1;//最小值下标
	for(int i=0 ; i<numLen ; i++)//有序数组末位
	{
		index = i;
		for(int j=i+1 ; j<numLen ; j++){//遍历一遍无序数组
			if(num[j] < num[index])
				index = j;//找最小值下标
		}
		if(index != i){//最小值不是有序数组末位,则把它交换无序数组最小值
			int temp = num[index];
			num[index] = num[i];
			num[i] = temp;
		}
	}
}

五、选择排序---堆排序(Heap Sort)

        预备知识:

        1.什么是堆? 对一棵完全二叉树,树中任一非叶子结点的关键字均不大于其左右孩子(若存在)结点的关键字称为最小堆,最大堆同理,见下图:

        2.如何存储堆? 可以用数组存储,对于下标从1开始的情况,非叶子节点和它的左右孩子有关系:k(i)左孩子为k(2i)右孩子k(2i+1),可以利用数组下标关系方便的更新数组元素,用来存储堆元素。最后一个非叶子节点是多少呢?画一棵满二叉树可知为n/2向下取整。

        3.如何调整最大堆? 将堆底元素(数组最后一个元素)与堆顶元素交换,然后将堆顶与左右孩子中最小的那个交换,一旦与左孩子交换,可能会破坏左子树的最小堆性质,依次迭代直到没有子树违背这个性质。见下图:


        4.如何通过一个数组建立堆? 由于子树的根节点非常重要,因此可以从最后一个非叶子节点自底向上更新堆,见下图:



        算法过程:堆排序正是利用小根堆(或大根堆)来选取当前无序区中关键字小(或最大)的记录实现排序的。我们不妨利用大根堆来排序。每一趟排序的基本操作是:将当前无序区调整为一个大根堆,选取关键字最大的堆顶记录,将它和无序区中的最后一个记录交换。这样,正好和直接选择排序相反,有序区是在原记录区的尾部形成并逐步向前扩大到整个记录区,其实本质上就是选择排序的思想。

        算法复杂度:时间复杂度O(NlogN),空间复杂度O(1)(直接用数组原地排序)。

        性能分析:每一次筛选过程都需要调用一次updateHeap函数,而每次调用都会导致节点下沉一级,所以updateHeap()开销O(logN),对于createHeap从第一个非叶节点逆推到根节点,每次都调用updateHeap()函数,因此createHeap()函数开销O(NlogN)。对于堆排序,需要一次createHeap时间加上N-1次调整堆updateHeap时间,加在一起还是O(NlogN),由堆排序过程可以知道,本算法不存在最佳或最坏情况,都需要做建堆和调整堆的操作,都是O(NlogN)。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。

        稳定性分析:不稳定,举例3,27(1),36,27(2)如果先输出堆顶3,然后27(2)放到堆顶,满足性质则会继续输出堆顶27(2),则27(2)先于27(1)输出。

        代码如下:

/*
排序算法5:堆排序
维护一个最大堆,用数组作为数据结构
*/
//维护以下标为index非叶节点子树的堆性质
void updateHeap(int num[] , int numLen , int index)
{
	int nonLeaf = numLen/2-1;
	if(index <= nonLeaf && index>=0){//如果是非叶节点一定存在左孩子
		int LChild = 2*index+1;//左孩子下标,注意数组下标从0开始
		int RChild = 2*index+2;//右孩子下标
		int largest = index;//最小值下标
		if(num[LChild] > num[largest])
			largest = LChild;
		if(RChild <= numLen-1 && num[RChild]>num[largest])//右孩子也存在
			largest = RChild;
		if(largest != index){//如果非叶节点不是最大的,互换该节点与最大孩子节点
			int temp = num[largest];
			num[largest] = num[index];
			num[index] = temp;
			updateHeap(num,numLen,largest);//维护被破坏堆性质的子树
		}
	}
}
//由数组建立堆
void createHeap(int num[] , int numLen)
{
	int nonLeaf = numLen/2-1;//最后一个非叶节点下标
	for (int i=nonLeaf; i>=0; i--)
	{
		updateHeap(num,numLen,i);//自底向上维护堆
	}
}
//堆排序
void heapSort(int num[] , int numLen)
{
	createHeap(num,numLen);
	//---将堆顶元素与堆底元素互换,然后重新按照[0,numLen-2]建立最小堆
	for(int i=numLen-1 ; i>=1 ; i--){//最后无序区只剩一个元素排序完成
		int temp = num[0];//堆底堆顶互换
		num[0] = num[i];
		num[i] = temp;
		updateHeap(num,i,0);//每次堆节点数量减一,从根维护最大堆
	}
}

六、交换排序---快速排序(Quick Sort)



        算法过程:是冒泡法的改进,本质上也是交换排序。思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

        算法复杂度:最佳情况时间复杂度O(NlogN),最坏情况O(N^2),平均时间复杂度为O(NlogN),空间复杂度:操作上只需要O(1),但递归栈上需要 logN(最佳:完全二叉树叶子节点个数为logN左右)~N(最坏情况退化成单枝二叉树,每次只能划分1个元素,叶子节点个数为N)。

        性能分析:最好情况每次划分为两个等长的部分,需要logN次划分,每次划分后形成的小组内依旧需要对组内元素比较进行下一次划分,整体而言,每次划分都需要比较N次,因此最佳情况为O(NlogN);最坏情况(主元为当前组内最大或最小数字)每次划分只能减少一个元素,此时退化为冒泡(O(N^2))。快速排序是通常被认为在同数量级O(NlogN)的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快速排序反而蜕化为冒泡排序。因此在使用过程中最好使用随机化主元的方法,得到理论最坏情况的可能性仅为1/(2^n)。

        稳定性分析:不稳定,发生在中枢元素和a[j] 交换的时刻,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱。

        代码如下:

/*
排序算法6:快速排序
依主元划分数组,找到主元所在位置,要求主元左边元素均小于主元,右边元素均大于主元
本质上是交换排序。
*/
int partitionByPivot(int num[] , int begin , int end)
{
	//int pivot = num[begin];//普通主元:选取每组的第一个元素作为主元
	srand((time(0)));
	int pivot = num[begin + rand()%(end-begin+1)];//随机化主元
	while (begin < end)
	{
		while(begin<end && pivot<=num[end])
			end--;
		num[begin] = num[end];
		while(begin<end && pivot>=num[begin])
			begin++;
		num[end] = num[begin];
	}
	num[begin] = pivot;
	return begin;
}
void quickSort(int num[] ,int begin , int end)
{
	if(end - begin <= 0)
		return;
	int pivotPos = partitionByPivot(num,begin,end);
	quickSort(num , begin , pivotPos-1);
	quickSort(num , pivotPos+1,end);	
}

七、分治法---归并排序(Merge Sort)


        算法过程:归并排序是采用分治法(Divide and Conquer)的一个非常典型的应用,分而治之,不断的将原序列划分,直至两两一组,此时进行排序后再将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

        算法复杂度:时间复杂度O(NlogN),空间复杂度O(N)( 辅助数组大小)。

        性能分析:分治法:总时间=分解时间+解决问题时间+合并时间,分解时间就是把一个待排序序列分解成两序列,时间为一常数,时间复杂度O(1)。解决问题时间是两个递归式,把一个规模为N的问题分成两个规模分别为N/2的子问题,时间为2T(N/2)。合并时间复杂度为O(N)。总时间T(N)=2T(N/2)+O(N).这个递归式可以用递归树来解,其解是O(NlogN)。此外在最坏、最佳、平均情况下归并排序时间复杂度均为O(NlogN),因为无论序列如何都需要分治。速度仅次于快速排序,适用于总体无序,但各子项相对有序的数列。

        稳定性分析:稳定,因为分治的缘故需要分解为两个一组或一个一组,对于一个一组不可能调换顺序,两个一组的情况如果两数相同也不需要调换顺序,因此分解后合并时算法稳定。

        代码如下:

/*
排序算法7:归并排序
分治法,将数组划分至两两一组,排序后归并即可
*/
//对数组num[begin,mid]和数组num[mid+1,end]进行归并,结果存放temp中
void mergeArray(int num[],int begin , int mid ,int end , int temp[])
{
	int i=begin ,j=mid , m=mid+1 , n=end;
	int k=0;//temp起始位从0开始
	while(i<=j && m<=n){//只要有一个序列合并完跳出循环
		if(num[i] <= num[m])//一定要加等号,相等的元素不会相邻
			temp[k++] = num[i++];
		else
			temp[k++] = num[m++];
	}
	while(i<=j)//此时m>n即序列num[mid+1,end]合并结束,将num[begin,mid]直接复制到temp中
		temp[k++] = num[i++];
	while(m<=n)
		temp[k++] = num[m++];
	//---将temp的数据复制回num
	for(int i=0 ; i<k ; i++)//k是temp的有效数据个数
		num[begin+i] = temp[i];
}
//分治与归并
void divideAndMerge(int num[] , int begin , int end , int temp[])
{
	if(begin >= end)//只留下一个数的时候返回
		return;
	int mid = begin+(end-begin)/2;
	divideAndMerge(num , begin ,mid, temp);
	divideAndMerge(num , mid+1 ,end, temp);
	mergeArray(num , begin , mid , end , temp);
} 
void mergeSort(int num[] , int numLen)
{
	int  *temp = new int[numLen];//分配临时数组用于存放原数组排序后的数
	//共用一个临时数组,此暂存数组也可以在mergeArray中创建
	divideAndMerge(num , 0 , numLen-1 , temp);
	delete[]temp;
}

八、线性时间排序(非基于比较排序)---计数排序(Counting Sort)、基数排序(Radix Sort)、桶排序(Bucket Sort)

         第八节提出的排序与前七节的排序有着本质的区别,前七节无论是冒泡、插入、选择、希尔、堆排、快排、归并,都是基于比较的,这一节的三种排序均不是基于比较的。

比较排序算法的下界

        最坏情况下界为Ω(nlogn)。假设有一n个元素组成的数组(假设每个元素都不相等),那么一共有n!排列组合,而且这n!排列组合结果都应该在决策树的叶子节点上,见下图:

image

         在图中n = 3,所以有3! = 6种组合全都在决策树的叶子节点,对于高度为h的二叉树,叶子节点的个数最多为2^h (当为满二叉树时为2^h,这里根节点为第0层)。所以N! <= 2^h ,从而h >= log(N!) = Ω(NlogN)。即在最差情况下,任何一种比较排序至少需要O(nlogn)比较操作,这是由于比较操作所获的信息有限所导致的。

        快速排序在平均情况下复杂性为O(NlogN),最坏情况下复杂性为O(N^2);堆排序和合并排序在最坏情况下复杂性为O(NlogN),因此堆排序和归并排序是渐进最优的比较排序算法。

8.1 计数排序(Counting Sort)

        算法过程:对每一个元素x,确定小于x的元素个数N,就可以把x放置在数组的第N+1位上(数组下标从0开始,也就是下标为N的位置),对于重复元素,对计数数组逆向扫描,输出一次计数器减一直到完全输出。

        算法复杂度:时间复杂度O(N+maxVal),空间复杂度O(N+maxVal)。

        性能分析:从代码来看,计数排序有5个for循环(memset算是一个),其中三个时间是N,两个时间是maxVal。所以时间复杂度为3N+2maxVal,即O(N+maxVal);空间复杂度为N+maxVal(两个辅助数组)。不管是在最坏还是最佳情况下,都需要作出统计与复制,因此时间复杂度不变。辅助空间在maxVal比较大的时候是非常浪费的,且有限制条件,因为计数数组的下标与数组元素关联,因此数组元素要求非负且在一定范围内。

        稳定性分析:稳定,对于重复元素x,由前向后扫描得到小于等于x的元素个数Pos,如果重复元素有2个x1,x2,则按稳定性需求将分别要在num[]中位于Pos-1,Pos二个位置,在“对号入座”时逆向扫描保证第一次访问到x的元素为x2,位置是Pos,并在输出一次后让计数器减一可以保证后访问x的元素x1紧挨着x2放置在Pos-1位置。

        运用约束:记录非负且在一定范围内。

        代码如下:

/*
排序算法8:计数排序
对于数组中的每个数统计小于它的元素个数n,那么可以直接将它放置在n+1的位置上了。
当然,当有相同元素时需要做适当调整。
*/
void countingSort(int num[] , int numLen ,int maxVal)
{

	int *countArray = new int [maxVal+1];
	int *temp = new int[numLen];
	//---初始化计数数组
	for(int i=0 ; i<maxVal+1 ; i++)
		countArray[i] = 0;
	//---计算数组中num[i]出现的次数
	for(int i=0 ; i<numLen ; i++)
		(countArray[num[i]])++;
	//---累计数组中小于num[i]的数出现次数
	for(int i=1 ; i<maxVal+1 ; i++)
		countArray[i] += countArray[i-1];
	//---对应小于num[i]的数出现次数将数字放在指定位置
	for(int i=numLen-1 ; i>=0 ; i--){//从后向前遍历让排序稳定
		int pos = countArray[num[i]]--;//小于等于num[i]的数字个数,元素相等时,如果输出一次,计数器减一。
		temp[pos-1] = num[i];
	}
	//--将temp的数据复制回num
	for(int i=0 ; i<numLen ; i++)
		num[i] = temp[i];
	delete []temp;
	delete []countArray;
}

8.2 基数排序(Radix Sort)

        算法过程:计数排序中当maxVal很大时,时间和空间的开销都会增大(想象序列{8888,1234,9999}计数排序得花多少空间和遍历的时间?)。于是可以把待排序记录由低位到高位分解(如果递归的先比较高位再比较低位会产生许多要保存的临时数据),自低位到高位分别进行计数排序。这样的话分解出来的每一位最大值为9。

        算法复杂度:时间复杂度O(N),空间复杂度O(K+N)(由于K<<maxVal,因此所占空间一般比计数排序小得多)

        性能分析:基数排序时间T(n)=d*(2K+3N),其中d是记录值的位数,(2K+3N)是每一趟计数排序时间,由于一位数最大值K不超过9,d的值一般也很小,k、d都可以看成是一个很小的常数,所以时间复杂度O(n)。最坏最佳情况并不改变时间复杂度。

        稳定性分析:稳定,由于是基于计数排序,因此若实现的计数排序稳定则本算法也稳定。

        运用约束:非负数。

        代码如下:

/*
排序算法9:基数排序
将一个d位数字分割成d次计数排序,由低位到高位进行计数排序。只需要进行d轮就能排序完成
对一个数(无论高位还是低位)最大值为9,因此计数排序maxVal = 9
*/
//对第d位进行计数排序
void countingSort(int num[] , int numLen , int maxVal , int d)
{
	int *countArray = new int[maxVal+1];
	int *temp = new int[numLen];
	//---初始化
	for(int i=0 ; i<maxVal+1 ; i++)
		countArray[i] = 0;
	//---统计数组每个num[i]第d位出现的次数
	for(int i=0 ; i<numLen ; i++){
		int dNum =num[i]/(int)pow(10,d-1)%10;
		countArray[dNum]++;
	}
	//---累计数组中小于等于num[i]第d位出现的次数
	for(int i=1 ; i<maxVal+1 ; i++){
		countArray[i] += countArray[i-1];
	}
	//---逆向遍历第d位的累计计数数组寻找相应位置
	for(int i=numLen-1 ; i>=0 ; i--){
		int dNum = num[i]/(int)pow(10,d-1)%10;
		int pos = countArray[dNum]--;
		temp[pos-1] = num[i];
	}
	//---将排序完的数组复制回num
	for(int i=0 ; i<numLen ; i++)
		num[i] = temp[i];
	delete []temp;
	delete[]countArray;
}
void radixSort(int num[] , int numLen , int dMax)
{
	for(int i=1 ; i<=dMax ; i++)//由低位到高位计数排序
		countingSort(num , numLen , 9 , i);
}

8.3 桶排序(Bucket Sort)

        算法过程:假设输入均匀、独立的分布在区间[0,1),将之划分成M个(本图选10个)相同大小的子区间,称为桶。将N个记录分布到各个桶中去。如果有多于一个记录分到同一个桶中,需要进行桶内排序。最后依次把各个桶中的记录列出来即得到有序序列。如上图所示,第i个桶内存放的是区间(i/10,(i+1)/10)中的值,排好序的输出是链表B[0],B[1]···B[9]依次连接而成。

        算法复杂度:期望时间复杂度O(N),空间复杂度O(N+M)。

        性能分析:当元素越均匀、独立的分布在[0,1)空间上,该算法效率越高,一般不会出现很多个数落在同一个桶的情况。桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。当然桶排序的空间复杂度为O(N+M)(见上图),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。

        稳定性分析:稳定,对于两个相同元素,如果掉在同一个桶不会毁坏稳定性,如果掉在不同的桶,则由桶内排序和桶间链表连接决定是稳定的。

        运用约束:均匀而独立的分布在各个桶中。或所有桶的大小的平方与总的元素数呈线性关系。

        代码如下:

/*
排序算法10:桶排序
假定有N个数据输入均匀、独立的分布在[0,1)之间,只需要建立一个大小为M的链表作为桶
将N个数据均匀的扔进桶内,对于桶内不为空的链表进行插入排序,最终输出链接好的节点
*/
void bucketSort(double num[] , int numLen)
{
	//---定义链表节点
	struct Node{
		Node():pNext(NULL){}
		Node *pNext;
		double data;
	};
	//---定义一个长为numLen的指针数组,数组内每个元素为Node*类型
	Node **bucket = new Node*[numLen];
	//---初始化桶标号
	for(int i=0 ; i<numLen ; i++){
		bucket[i] = new Node;
		bucket[i]->data = i;
		bucket[i]->pNext = NULL;
	}
	//---将数组中元素均匀的插入到桶中相应位置。
	for(int i=0 ; i<numLen ; i++){
		int bucketIndex = (int)num[i]*numLen;//欲扔入的桶下标
		Node *newNode = new Node;
		newNode->data = num[i];
		//---寻找相应位置,将temp插进去
		Node *pTemp = bucket[bucketIndex];
		while(pTemp->pNext && pTemp->pNext->data<=num[i])//找到下一个元素大于num[i]的位置,在该位置前面插入
			pTemp = pTemp->pNext;
		newNode->pNext = pTemp->pNext;
		pTemp->pNext = newNode;
	}
	//---按桶标号顺序取出排好序的数据
	int k=0;
	for(int i=0 ; i<numLen ; i++){
		Node *pTemp = bucket[i]->pNext;//非空桶第一个节点
		if(pTemp == NULL)//空桶
			continue;
		while (pTemp)
		{
			num[k++] = pTemp->data;
			pTemp = pTemp->pNext;
		}
	}
	//---释放内存空间
	for(int i=0 ; i<numLen ; i++){
		Node *pTemp = bucket[i];//表头结点
		Node *pHead = pTemp ;
		while(pTemp){
			pHead = pHead->pNext;//表头后移
			delete pTemp;
			pTemp = pHead;//再指向表头
		}
	}
	delete []bucket;//最终释放指针数组
}
int main()
{
	double bucketTest[]={0.78,0.17,0.39,0.26,0.72,0.94,0.21,0.12,0.23,0.68};
	int numLen = sizeof(bucketTest)/sizeof(double);
	bucketSort(bucketTest ,numLen);
	for(int i=0 ; i<numLen ; i++)
		cout<<bucketTest[i]<<" ";
	cout<<endl;
	return 0;
}

总结:

            至此,经典的八种排序方法全部介绍完毕,以后有时间的话可以研究一下经典排序方法的改良版本。



  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值