排序算法详解及代码

排序算法介绍

排序算法一般是指将有数字大小关系的序列,对其进行操作,得到一次有序的序列关系。

  1. 内部排序:内部排序算法(只访问内存),外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
  2. 算法的稳定性:在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的;若具有相同关键字的记录之间的相对次序发生改变,则称这种排序方法是不稳定的。

稳定与否其实与算法的实现有很大的关系,一个不稳定的算法如果多加一个域进行比较又可以变稳定,因此本文稳定性分析只针对最朴素算法而言,并非改良算法。当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。

快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;以下,依次介绍各自排序算法及其c++代码实现。

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

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

算法复杂度:时间复杂度O(N2),空间复杂度O(1)。性能分析:最佳效率O(N),最糟效率O(N2)与冒泡、选择相同,适用于排序小列表,若列表基本有序,则插入排序比冒泡、选择更有效率。

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

/*
排序算法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;
 }
}

int main(){ 	
int num[] = { 11, 33, 55, 22, 77, 88, 66, 22 };	
int numLen = sizeof(num) / sizeof(int);	
cout << "数组长度:" << numLen << endl;	
cout << "排序前:" << endl;	
for (int i = 0; i < numLen; i++)	
{		
	cout << num[i] << " ";	
}	
cout << endl;	
insertion_sort(num, numLen);	
cout << "排序后:" << endl;	
for (int i = 0; i < numLen; i++)	
{		
cout << num[i] << " ";	
}	
cout << endl;	
system("pause");	
return 0; 
}

此处的main函数内容,同样适合后续算法,只需替换对应算法名字即可。

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

在这里插入图片描述

图中增量取5,3,1。
算法过程:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。一般的初次取序列的一半为增量,以后每次减半,直到增量为1。

算法复杂度:比较次数依赖于增量的选取,大约为O(NlogN)~ O(N1.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(N2),因此平均时间复杂度O(N2)。

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

/*
排序算法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(N2),空间复杂度O(1)。

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

稳定性分析:由于有交换的存在(而不是插入),例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开始的情况,非叶子节点和它的左右孩子有关系:k(i)左孩子为k(2i)右孩子k(2i+1),可以利用数组下标关系方便的更新数组元素,用来存储堆元素。最后一个非叶子节点是多少呢?画一棵满二叉树可知为n/2向下取整。

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

在这里插入图片描述
建立堆:通过数组建立堆,由于子树的根节点非常重要,因此可以从最后一个非叶子节点自底向上更新堆,见下图:
在这里插入图片描述
算法过程:堆排序正是利用小根堆(或大根堆)来选取当前无序区中关键字小(或最大)的记录实现排序的。我们不妨利用大根堆来排序。每一趟排序的基本操作是:将当前无序区调整为一个大根堆,选取关键字最大的堆顶记录,将它和无序区中的最后一个记录交换。这样,正好和直接选择排序相反,有序区是在原记录区的尾部形成并逐步向前扩大到整个记录区,其实本质上就是选择排序的思想。
在这里插入图片描述
在这里插入图片描述
算法复杂度:时间复杂度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(N2),平均时间复杂度为O(NlogN),空间复杂度:操作上只需要O(1)。

算法性能分析:最好情况每次划分为两个等长的部分,需要logN次划分,每次划分后形成的小组内依旧需要对组内元素比较进行下一次划分,整体而言,每次划分都需要比较N次,因此最佳情况为O(NlogN);最坏情况(主元为当前组内最大或最小数字)每次划分只能减少一个元素,此时退化为冒泡(O(N2))。

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

/*排序算法6:快速排序依主元划分数组,找到主元所在位置,要求主元左边元素均小于主元,右边元素均大于主元本质上是交换排序。*/ //快速排序一次划分
int Partition(int r[], int first, int end)
{	int i = first;                        
	//初始化	
	int j = end;	int temp; 	
	while (i<j)	
	{		
		while (i<j && r[i] <= r[j])			
			j--;                       
		//右侧扫描		
		if (i<j)		
		{			
			temp = r[i];                 
			//将较小记录交换到前面			
			r[i] = r[j];			
			r[j] = temp;			
			i++;		
		}		
		while (i<j && r[i] <= r[j])			
			i++;                         
		//左侧扫描		
		if (i<j)		
		{			
			temp = r[j];			
			r[j] = r[i];			
			r[i] = temp;                
			//将较大记录交换到后面			
			j--;		
		}	
	}	
	return i;                           
	//i为轴值记录的最终位置
} 

//快速排序
void QuickSort(int r[], int first, int end)
{	
	if (first<end)	
	{                                  
 		//递归结束		
 		int pivot = Partition(r, first, end);  
 		//一次划分		
 		QuickSort(r, first, pivot - 1);
 		//递归地对左侧子序列进行快速排序		
 		QuickSort(r, pivot + 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;
}

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

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

无论是冒泡、插入、选择、希尔、堆排、快排、归并,都是基于比较的,这一节的三种排序均不是基于比较的。

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个记录分布到各个桶中去。如果有多于一个记录分到同一个桶中,需要进行桶内排序。最后依次把各个桶中的记录列出来即得到有序序列。

算法复杂度:期望时间复杂度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;
}

以上是总结的多种排序算法,及对应分析,下面列表总结对比。
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值