排序算法总结

目录

一  插入排序

1.直接插入排序

2.希尔排序

二 选择排序

1.直接选择排序

2.堆排序

三 交换排序

1.冒泡排序

         2.快速排序

四 归并排序

五 基数排序

总结


排序算法总结_第1张图片

一  插入排序

1.直接插入排序

直接插入排序(Insertion Sort)的基础思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。

基本思路

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序大于新元素,将已排序元素到下一个位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 新元素插入到该位置后
  6. 重复步骤 2~5

排序算法总结_第2张图片

代码实现:

//直接插入排序
void Insertionsort(int a[], int n)
{
	for(int i=1;i=0&&a[j]>temp;j--)  //数据后移并向前逐个比较,直到需要插入的地方
			a[j+1]=a[j];
		a[j+1]=temp;               //插入temp
	}
}
//直接插入排序2
void Insertionsort(int a[], int n)
{
	for(int i=1;i=0&&a[j]>a[j+1];j--)
			swap(a[j],a[j+1]);
}

直接插入排序的效率分析
(1)时间复杂度
从时间分析,首先外层循环要进行n-1次插入,每次插入最少比较一次(正序),移动2次( int temp=a[i];a[j+1]=temp;)最多比较i次移动i+2次(逆序)(i=1,2,…,n-1)。

用Cmin ,Cmax 和Cave表示元素的总比较次数的最小值、最大值和平均值。

用Mmin ,Mmax 和Mave表示元素的总移动次数的最小值、最大值和平均值。

则上述直接插入算法对应的这些量为:

  • Cmin=n-1
  • Mmin=2(n-1)
  • Cmax=1+2+…+n-1=(n^{2}-n)/2
  • Mmax=3+4+…+n+1=(n^{2}+3n-4)/2
  • Cave=(n^{2}+n-2)/4
  • Mave=(n^{2}+7n-8)/4

因此,直接插入排序的时间复杂度为O(n^{2}

由上面对时间复杂度的分析可知,当待排序元素已从小到大排好序(正序)或接近排好序时,所用的比较次数和移动次数较少;当待排序元素已从大到小排好序(逆序)或接近排好序时,所用的比较次数和移动次数较多,所以插入排序更适合于原始数据基本有序(正序)的情况.

(2)空间复杂度
首先从空间来看,它只需要一个元素的辅助空间,用于元素的位置交换O(1).
(3)稳定性
插入排序是稳定的,因为具有同一值的元素必然插在具有同一值得前一个元素的后面,即相对次序不变.
 

 

2.希尔排序

希尔排序是希尔于 1959 提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破 O(n^2) 的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

希尔排序是把记录按一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组所包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。

在此我们选择希尔排序增量为 gap=length/2,缩小增量继续以 gap=gap/2的方式,这种增量选择我们可以用一个序列来表示,n/2,(n/2)/2,...,1 称为增量序列。
希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。

基本思路

  1. 首先选取一个整数d1各组内进行直接插入排序,直到各组排好序为止。
  2. 从上述的结果序列出发,再选择d2
  3. 依次取di+1直到dm=1(设一共需要m次分组),即所有数据放在一组中排序为止。此时,全部数据便按次序排好了


排序算法总结_第3张图片

注意看第二趟排序过程,更能直观的体现出各组内进行直接插入排序

 代码实现

//希尔排序
void Shellsort(int a[],int n)
{
	int gap=n/2;          //增量置初值
	while(gap>0)
	{
		for (int i=gap;i=0&&temp0;gap/=2)
		for (int i=gap;i=0&&a[j]>a[j+gap];j-=gap)
				swap(a[j],a[j+gap]);
}

希尔排序的效率分析

(1)时间复杂度

希尔排序的时间复杂度与增量(即,步长gap)的选取有关。例如,当增量为1时,希尔排序退化成了直接插入排序,此时的时间复杂度为O(n^2),而Hibbard增量的希尔排序的时间复杂度为O(n^{1.3})。

(2)空间复杂度

希尔排序属于原位排序,空间复杂度为O(1)

(3)稳定性

不稳定的排序算法,例如序列 [1,10,5,5∗] ,排序结果是 [1,5∗,5,10],显然 5和 5∗ 的相对位置发生了改变。
 

 

二 选择排序

1.直接选择排序

直接选择排序是一种简单直观的排序算法。它的工作原理:首先在未排序列中找到最小(大)的元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)的元素,然后放到已排序序列的末尾,(与待排的第一个元素交换)。以此类推,直到所有元素均排序完毕。

基本思路

  1.  从待排序序列中,找到关键字最小的元素;
  2.  如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换
  3.  从余下的 n- 1 个元素中,找出关键字最小的元素,重复1、2步,直到排序结束。

å¨è¿éæå¥å¾çæè¿°

排序算法总结_第4张图片

代码实现

//直接选择排序
void Selectsort(int a[],int n)
{
	for(int i=0;i

选择排序效率分析

(1)时间复杂度

  记录比较次数:

    无论待排序数组初始状态如何,都要进行n-1趟选择排序:
        第1趟:比较n-1次;
        第2趟:比较n-2次;
        ……
        第n-1趟:比较1次。

    从而,总共的比较次数为:1+2+……+(n-1) = n(n-1)/2
    记录移动次数:

    如果待排序数组为正序,则记录不需要交换,记录移动次数为0;
    如果当排序数组为逆序,则:
        第1趟:交换1次,移动3次;(int temp=a[i];a[i]=a[k];a[k]=temp;)
        第2趟:交换1次,移动3次;
        ……
        第n-1趟:交换1次,移动3次。

    从而,总共的移动次数为:3(n-1) = 3(n-1)。
    因此,时间复杂度为O(n^2{\color{Red} })

(2)空间复杂度

在选择排序的过程中,设置一个变量用来交换元素,所以空间复杂度为O(1)

(3)稳定性

不稳定的排序算法。例如序列 [5,5∗,3]第一趟就将第一个 5 与 3交换,导致第一个 5 挪动到第二个 5∗后面。
 

注:直接选择排序和直接插入排序类似,都将数据分为有序区和无序区,所不同的是直接插入排序是将无序区的第一个元素直接插入到有序区的相应位置以形成一个更大的有序区,而直接选择排序是从无序区选一个最小的元素直接放到有序区的最后。

 

 

2.堆排序

堆排序是指利用堆这种数据结构所涉及的一种排序算法。堆序列可看作完全二叉树的结构,并同时满足下面的性质:父节点的值总是大于等于(或小于等于)其左右孩子的值。

当每个节点都大于等于它的两个节点时,就称为大顶堆; 当每个父节点都小于等于它的两个子节点时,就称为小顶堆

(补充)二叉堆 二叉堆其实是一棵有着特殊性质的完全二叉树,父节点的值总是大于等于(或小于等于)其左右孩子的值;每个节点的左右子树都是一棵这样的二叉堆。

大顶堆
大顶堆

小顶堆
小顶堆

基本思路

  1. 构建初始堆:初始待排序序列 (R1,R2,...,Rn) 构建成大(小)顶堆(如上图),此堆为初始的无序区。
  2. 堆排序:堆顶元素 R[1]与最后一个元素 R[n]交换,此时得到新的无序区 (R1,R2,...,Rn)和新的有序区 (Rn) ,且满足R[1,2...,n−1]<=R[n]

  3. 调整堆:由于交换后新的堆顶 R[1]可能违反堆的性质,因此需要对当前无序区 (R1,R2,...,Rn−1)调整为新大(小)顶堆。
  4. 然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区 (R1,R2,...,Rn−2)和新的有序区 (Rn−1,Rn)。
  5. 不断重复此过程直到有序区的元素个数为 n−1 ,无序区只有一个元素时停止,则整个排序过程完成。

实现堆排序的三个详细步骤:(参考博客)

1、将一个无序序列建成初始堆

叶子节点(没有子节点的末尾节点)可以认为是一个堆(因为叶子结点没有子节点可以看作一定大于子节点满足堆的定义,所以所有的叶子节点一定是一个堆),然后从第一个非叶子位置序号为len/2-1,i从0开始节点向前直到第一个节点(从后向前)插入堆中并调整构成新大(小)顶堆(利用下面的调整堆实现)。

注意:由于数组从0开始计算序号,也就是二叉堆的根节点序号为0,因此序号为i的左右子节点的序号分别为2i+1和2i+2。

 以数组A为例,假设其数组内元素的原始顺序为:A[]=[6,1,3,9,5,4,2,7],那么在没有建成大顶堆前,元素在该完 全二叉树中的存放位置如下: 

 这里的后面四个元素均为叶子节点,很明显,这四个叶子可以认为是一个堆,而后我们便考虑将第一个非叶子节点9插入到这个堆中,再次构成一个堆,接着再将3插入到新的堆中,再次构成新堆,如此继续,直到该二叉树的根节点6也插入到了该堆中,此时构成的堆便是由该数组建成的大顶堆(利用到下面所写的HeapAdjust函数)。

建堆的代码可写成如下的形式:

	//把数组建成为大根堆,循环建立初始堆(从第一个非叶子节点向前直到第一个节点)
	//第一个非叶子节点的位置序号为n/2-1
	for (i=n/2-1;i>=0;i--)
		HeapAdjust(a,i,n-1);

 

2、建成大顶堆后,进行堆排序,堆顶元素(a[0])和最后一个元素(a[i])交换位置,最大元素归位

   1)我们排序的目标是序列从小到大,因此我们用大顶堆

   2)我们将堆中的元素以层序遍历后的顺序保存在一维数组A中,根节点在数组中的位置序号为A0。

    这样,如果某个节点在数组中的位置序号为i,那么它的左右孩子的位置序号分别为2i+1和2i+2

    注意下面的分析,我们并没有采用额外的数组来存储每次去掉的堆顶数据: 

        

    这里数组A中元素的个数为8,很明显最大值为A0,为了实现排序后的元素按照从小到大的顺序排列,我们可以将二叉堆中的最后一个元素A7与A0互换,这样A7中保存的就是数组中的最大值,而此时该二叉树变为了如下情况:

代码如下: 

	//进行堆排序
	for(int i=n-1;i>0;i--)			//从后往前,保存最大的数
	{
		//堆顶元素(a[0])和最后一个元素(a[i])交换位置,
		//这样最后的一个位置保存的是最大的数,最大元素归位
		//每次循环依次将次大的数值在放进其前面一个位置,
		//这样得到的顺序就是从小到大
		int temp =a[i];		
		a[i]=a[0];				
		a[0]=temp;				
		HeapAdjust(a,0,i-1);		//堆顶元素(a[0])保存最后一个位置的元素,导致不一定为堆,所以重新调整
	}

 

  3、由于交换后新的堆顶可能违反堆的性质,所以进行调整堆

    为了将其调整为大顶堆,我们需要寻找4应该插入的位置。为此,我们让4与它的孩子节点中最大的那个,也就是其左孩子7,进行比较,由于4<7,我们便把二者互换,这样二叉树便变成了如下的形式:

    接下来,继续让4与其左右孩子中的最大者,也就是6,进行比较,同样由于4<6,需要将二者互换,这样二叉树变成了如下的形式:

    这样便又构成了二叉堆,这时候A0为7,是所有元素中的最大元素。同样我们此时继续将二叉堆中的最后一个元素A6和A0互换,这样A6中保存的就是第二大的数值7,而A0就变为了3,形式如下:

    为了将其调整为二叉堆,一样将3与其孩子结点中的最大值比较,由于3<6,需要将二者互换,而后继续和其孩子节点比较,需要将3和4互换,最终再次调整好的二叉堆形式如下:

    一样将A0与此时堆中的最后一个元素A5互换,这样A5中保存的便是第三大的数值,再次调整剩余的节点,如此反复,直到最后堆中仅剩一个元素,这时整个数组便已经按照从小到大的顺序排列好了。

实现代码如下:

//堆排序——调整堆
/*
arr[low+1...high]满足大根堆的定义,
将arr[low]加入到最大堆arr[low+1...high]中,
调整arr[low]的位置,使arr[low...high]也成为大根堆
注:由于数组从0开始计算序号,也就是二叉堆的根节点序号为0,
因此序号为i的左右子节点的序号分别为2i+1和2i+2
*/
void HeapAdjust(int a[],int low,int high)
{
	int temp=a[low];		//保存堆中第一个节点(要调整)
	int i=2*low+1;			//a[i]是a[low]的左孩子,i为该节点的左孩子在数组中的位置序号
	while(i<=high)			
	{
		//比较左右孩子,找出左右孩子中最大的那个
		if (i+1<=high&&a[i+1]>a[i])
			i++;
		if (a[i]>temp)		//如果子节点大于父节点
		{
			a[low]=a[i];	//把最大的子节点赋值给父节点(比较后不用交换,子节点的值还是原来的值(可以理解为挖空))
			low=i;			//修改low(把i位置作为父节点),以便继续向下调整
			i=2*low+1;		//修改i(令i为low位置节点的左孩子序号),以便继续向下调整
		} 
		else				//如果符合堆的定义,则不用调整位置
			break;
	}
	a[low]=temp;			//把堆中刚开始要调整的第一个节点放入最终位置(最后挖空的位置),调整完毕。
}

 

完整代码实现

//堆排序——调整堆
/*
arr[low+1...high]满足大根堆的定义,
将arr[low]加入到最大堆arr[low+1...high]中,
调整arr[low]的位置,使arr[low...high]也成为大根堆
注:由于数组从0开始计算序号,也就是二叉堆的根节点序号为0,
因此序号为i的左右子节点的序号分别为2i+1和2i+2
*/
void HeapAdjust(int a[],int low,int high)
{
	int temp=a[low];		//保存堆中第一个节点(要调整)
	int i=2*low+1;			//a[i]是a[low]的左孩子,i为该节点的左孩子在数组中的位置序号
	while(i<=high)			
	{
		//比较左右孩子,找出左右孩子中最大的那个
		if (i+1<=high&&a[i+1]>a[i])
			i++;
		if (a[i]>temp)		//如果子节点大于父节点
		{
			a[low]=a[i];	//把最大的子节点赋值给父节点(比较后不用交换,子节点的值还是原来的值(可以理解为挖空))
			low=i;			//修改low(把i位置作为父节点),以便继续向下调整
			i=2*low+1;		//修改i(令i为low位置节点的左孩子序号),以便继续向下调整
		} 
		else				//如果符合堆的定义,则不用调整位置
			break;
	}
	a[low]=temp;			//把堆中第一个节点放入最终位置(最后挖空的位置),调整完毕。
}
/*
堆排序(从小到大)
需要建立大根堆
*/
void HeapSort(int a[],int n)
{
	int i;
	//把数组建成为大根堆,循环建立初始堆(从第一个非叶子节点向前直到第一个节点)
	//第一个非叶子节点的位置序号为len/2-1
	for (i=n/2-1;i>=0;i--)
		HeapAdjust(a,i,n-1);
	//进行堆排序
	for (i=n-1;i>0;i--)			//从后往前,保存最大的数
	{
		//堆顶元素(a[0])和最后一个元素(a[i])交换位置,
		//这样最后的一个位置保存的是最大的数,最大元素归位
		//每次循环依次将次大的数值在放进其前面一个位置,
		//这样得到的顺序就是从小到大
		int temp =a[i];		
		a[i]=a[0];				
		a[0]=temp;				
		HeapAdjust(a,0,i-1);		//堆顶元素(a[0])保存最后一个位置的元素,导致不一定为堆,所以重新调整
	}
}


堆排序效率分析

1.时间复杂度

我们在每次重新调整堆时,都要将父节点与孩子节点比较,这样,每次重新调整堆的时间复杂度变为O(logn),而堆排序时有n-1次重新调整堆的操作,建堆时有((len-1)/2+1)次重新调整堆的操作,因此堆排序的平均时间复杂度为O(n*logn)

2.空间复杂度

由于我们这里没有借用辅助存储空间,因此空间复杂度为O(1)

3.稳定性

不稳定的排序方法

(补充)由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。堆排序在排序元素较少时有点大才小用,待排序列元素较多时,堆排序还是很有效的。另外,堆排序在最坏情况下,时间复杂度也为O(n*logn)。相对于快速排序(平均时间复杂度为O(n*logn),最坏情况下为O(n*n)),这是堆排序的最大优点。

 

 

三 交换排序

1.冒泡排序

它重复地走访过要排序的数列,依次比较相邻的两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

基本思路

    1、依次比较序列中相邻的两个元素,将较小的放在前面,这样一趟比较后,最小的元素就放在了第一个位置;(冒泡)

    2、再依次比较相邻的两个元素,将第二小的元素最终放到第二个位置;

    3、依次循环,直到最大的元素放在了最后一个位置,排序完成。

排序算法总结_第5张图片

基础思路上的改进:无论原始序列的排序是怎样的(哪怕已经是从小到大排好的),它都要进行n-1趟比较,每趟比较又要进行n-i-1次相邻元素间的比较。若在某一趟排序中未发现相邻位置上的气泡发生交换,则说明待排序的无序区中所有气泡均满足小者在上,大者在下的原则,因此,冒泡排序过程可在此趟排序后终止。即很有可能还没有进行第n-1趟比较,已经完成了排序。基于此,我们可以做如下改进:设置一个标志位,在每趟排序开始前,先将其置为FALSE。若排序过程中发生了交换,则将其置为TRUE。各趟排序结束时检查标志位,若未曾发生过交换则终止算法,不再进行下一趟排序。代码实现如下:

代码实现

void BubbleSort(int *arr, int n)
 {
    int flag=1;// 用来标记是否进行过交换 
	for(int i=0;iarr[j+1])
            {
				int temp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=temp;    
				flag=1;
			}
        }
	}
} 

冒泡排序效率分析

(1)时间复杂度 

算法的最好时间复杂度:若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值: Cmin=n-1,Mmin=0。冒泡排序最好的时间复杂度为O(n)。

算法的最坏时间复杂度:若初始文件是反序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
        Cmax=n(n-1)/2=O(n^2)
        Mmax=3n(n-1)/2=O(n^2)
冒泡排序的最坏时间复杂度为O(n^2)。

算法的平均时间复杂度为O(n^2):虽然冒泡排序不一定要进行n-1趟,但由于它的记录移动次数较多,故平均时间性能比直接插入排序要差得多。

(2)空间复杂度

空间复杂度为 O(1)

(3)稳定性

由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。

 

2.快速排序

快速排序的基本思想:挖坑填数+分区操作

在待排序的n个记录中任取一个记录(通常取第一个记录),把该记录放入适当位置后,数据序列被此记录划分成两部分。所有关键字比该记录关键字的记录放置在一部分,所有比它的记录放置在一部分,并把该记录排在这两部分的中间(称为该记录归位),这个过程称作一趟快速排序。

之后对所有划分出来的两部分分别重复上述过程,直至每部分内只有一个记录或为空为止。

  简而言之,每趟使表的第一个元素放入适当位置,将表一分为二,对子表按递归方式继续这种划分,直至划分的子表长为1或0。


补充:

在C++的STL中,sort采用的就是优化的快速排序

快速排序是在原序列越有序时越慢,越无序则越快

基本思路:

  1. 每次从当前数列中挑出一个元素(通常取第一个元素),称为基准
  2. 重新排序当前数列,所有元素比基准的摆放在基准前面,所有元素比基准的摆在基准的后面。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区操作
  3. 递归将把 小于基准值元素的子数列 和 大于基准值元素的子数列 分别重复上述过程进行排序。

分区操作

一趟快速排序(从小到大)的划分过程是采用从两头向中间扫描的办法:

 附设两个元素指针low和high,初值分别为该序列的第一个元素的序号R[s]和最后一个元素的序号R[t],设枢轴元素的值为val=R[s];

首先从high所指位置起向前搜索到第一个值小于val的元素,并将其和val互换位置,然后从low所指位置起向后搜索到第一个值大于val的元素,并将其和val交换位置,如此反复 ,直到low=high为止。

   注意:我们上面说交换位置,只是为了便于理解,应尽量避免比较多的元素交换操作,因此下面的分析和代码的实现中,我们并不是采取交换操作,而是先将枢轴元素(R[s])保存在val变量中(将R[s]挖空),然后每次遇到需要交换的元素R[x]时,先将该元素R[x]赋给val(R[s])所在的位置,于是元素R[x]所在位置“挖空”,之后的每一次比较,就用需要交换的元素来填充上次“挖空”的位置,同时交换过来的元素之前所在的位置也就被“挖空”,以等待下次填充。

挖坑填数

  1. i =s; j = t; 将基准数挖出形成第一个坑a[i]。
  2. j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
  3. i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
  4. 再重复执行2,3二步,直到i==j,将基准数填入a[i]中。

分区一次的过程

代码实现

void QuickSort(int R[],int s,int t) 
	//对R[s]至R[t]的元素进行快速排序
{   
	int i=s,j=t,temp;
	if (si && R[j]>temp) 
				j--;  
			R[i]=R[j];
			while (i

快速排序效率分析

(1)时间复杂度

最坏时间复杂度
     最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。
     因此,快速排序必须做n-1次划分,第i次划分开始时区间长度为n-i+1,所需的比较次数为n-i(1≤i≤n-1),故总的比较次数达到最大值:Cmax = n(n-1)/2=O(n^2)
     如果按上面给出的划分算法,每次取当前无序区的第1个记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多,快速排序就完全成了冒泡排序,这便是最坏的情况,时间复杂度为O(n^2)。

 最好时间复杂度
     在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:O(nlogn)
     因为快速排序的记录移动次数不大于比较的次数,所以快速排序的最坏时间复杂度应为O(n^2),最好时间复杂度为O(nlogn)

平均时间复杂度
     尽管快速排序的最坏时间为O(n2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。它的平均时间复杂度为O(nlogn)。

(2)空间复杂度

 快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为O(logn),故递归后需栈空间为O(logn)。最坏情况下,递归树的高度为O(n),所需的栈空间为O(n)

(3)稳定性

快速排序是不稳定的。

基准关键字选取的改进
     在当前无序区中选取划分的基准关键字是决定算法性能的关键。
  ①"三者取中"的规则
     "三者取中"规则,即在当前区间里,将该区间首、尾和中间位置上的关键字比较,取三者之中值所对应的记录作为基准。

  ②取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准
     选取基准最好的方法是用一个随机函数产生一个取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准,这相当于强迫R[low..high]中的记录是随机分布的。用此方法所得到的快速排序一般称为随机的快速排序。

四 归并排序

归并排序其实要做两件事:

分解:将序列每次折半拆分
合并:将划分后的序列段两两排序合并
因此,归并排序实际上就是两个操作,拆分+合并

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

归并排序是建立在归并操作上的一种有效的排序算法。该算法是分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使得每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

2-路归并基本思路

  1. 将含有n个记录的序列看成是n个长度为1有序的子序列。
  2. 对相邻的两个子序列进行归并,并使得到n/2个长度为2或1的有序子序列;
  3. 再进行两两合并,直到最后由两个有序的子序列合并成为一个长度为n的有序序列。

å¨è¿éæå¥å¾çæè¿°

2-路归并的核心操作是将以分解后的前后相邻的两个有序序列归并为一个有序序列:

  • 设两个有序表存放在同一数组中相邻位置a[start,mid],a[mid+1,end]。合并过程中,设置i,j标记指针,其初值分别指向这两个记录区的起始位置。
  • 合并时依次比较a[i]和a[j]的关键字,取关键字较小的记录复制到b[k]中,然后将被复制记录的标记指针i或j加1,以及指向复制位置的标记指针k加1。
  • 重复这一过程直至两个输入的子文件有一个已全部复制完毕(不妨称其为空),此时将另一非空的子文件中剩余记录依次复制到b中即可。最后复制回a数组中。
/*
将有序的a[start...mid]和有序的a[mid+1...end]归并为有序的b[0...end-start+1],
而后再将b[0...end-start+1]复制到a[start...end]中,使a[start...end]有序
*/
void Merge(int a[],int b[],int start,int mid,int end)
{
	int i = start;			//i、j分别为第1、2段的下标
	int j = mid+1;
	int k = 0;				//k是b的下标
 
	//比较两个有序序列中的元素,将较小的元素插入到b中
	while(i<=mid && j<=end)
	{	
		if(a[i]<=a[j])				//将第1段中的记录放入b中
		{
			b[k++] = a[i++];
		}	
		else						//将第2段中的记录放入b中
		{
			b[k++] = a[j++];
		}
 
	}
	//将a序列中剩余的元素复制到b中,这两个语句只可能执行其中一个
	while(i<=mid)					//将第1段余下部分复制到b
	{
		b[k++] = a[i++];
	}	
	while(j<=end)					//将第2段余下部分复制到b
	{
		b[k++] = a[j++];
	}
 //将b中的元素复制回到a中
	for(i=0;i

 

时间效率分析

(1)时间复杂度

对长度为n的文件,需进行 logn趟2路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlogn)

(2)空间复杂度

需要一个辅助数组空间来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。

(3)稳定性

 归并排序是一种稳定的排序。
 

五 基数排序

基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine), 排序器每次只能看到一个列。它是基于元素值的每个位上的字符来排序的。 对于数字而言就是分别基于个位,十位, 百位或千位等等数字来排序。

基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

基本思路
它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

基数排序按照优先从高位或低位来排序有两种实现方案:

MSD(Most significant digital) 从最左侧高位开始进行排序。先按k1排序分组, 同一组中记录, 关键码k1相等, 再对各组按k2排序分成子组, 之后, 对后面的关键码继续这样的排序分组, 直到按最次位关键码kd对各子组排序后. 再将各组连接起来, 便得到一个有序序列。MSD方式适用于位数多的序列。

LSD (Least significant digital)从最右侧低位开始进行排序。先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。LSD方式适用于位数少的序列。

详细步骤
我们以LSD为例,从最低位开始,具体算法描述如下:

①. 取得数组中的最大数,并取得位数;
②. arr为原始数组,从最低位开始取每个位组成radix数组;
③. 对radix进行计数排序(利用计数排序适用于小范围数的特点);

排序算法总结_第6张图片

代码实现
基数排序:通过序列中各个元素的值,对排序的N个元素进行若干趟的“分配”与“收集”来实现排序。

分配:我们将L[i]中的元素取出,首先确定其个位上的数字,根据该数字分配到与之序号相同的桶中

收集:当序列中所有的元素都分配到对应的桶中,再按照顺序依次将桶中的元素收集形成新的一个待排序列L[]。对新形成的序列L[]重复执行分配和收集元素中的十位、百位…直到分配完该序列中的最高位,则排序结束
 

//取一个数的个位,或者十位,或者百位……
//d=1,表示取个位,d=2取十位
int get_digit(int num, int d)
{
	int val;
	while(d--)
	{
		val = num % 10;
		num = num / 10;
	}
	return val;
}
 
void radix_sort(int A[], int n)
{
	int radix = 10;//十进制
	int *count = new int[radix];//存放对应数位是0,1,2,3...的数的个数
	int *bucket = new int[n];//一个桶来存放A数组的数,用于中转
 
	for(int d = 1; d <= 3; ++d)//最大的数有多少位,就要循环多少次,假设最大的数在这里是3位数,所以外层循环3次
	{
		//初始化为0
		for(int i = 0; i < radix; ++i)
			count[i] = 0;
		//统计数位是0,1,2,3...的数分别有多少个
		for(int i = 0; i < n; ++i)
		{
			int j = get_digit(A[i], d);
			count[j]++;
		}
		//意思是,数位为i的数,在它的前面最多有多少个数
		for(int i = 1; i < radix; ++i)
			count[i] = count[i] + count[i - 1];
 
		//将数组中的数从后往前装入桶中,保证了稳定性
		for(int i = n - 1; i >= 0; --i)
		{
			int j = get_digit(A[i], d);
			bucket[count[j] - 1] = A[i];//根据这个数的数位上是多少,就放到桶的对应的位置
			count[j]--;
		}
 
		//把桶中的数放回A中,完成一趟排序,下一趟就是比较A数组各个数的十位/百位
		for(int i = 0; i < n; ++i)
		{
			A[i] = bucket[i];
		}
	}
	delete[] count;
	delete[] bucket;
}
 

 基数排序效率分析

排序算法总结_第7张图片

 

总结

 1.平方阶O(n²)排序:各类简单排序:

         直接插入、直接选择、冒泡排序;

 2.线性对数阶O(nlog₂n)排序:

         快速排序、堆排序和归并排序;

3.O(n1+§))排序,§是介于0和1之间的常数:

         希尔排序;

4.线性阶O(n)排序:

         基数排序,此外还有桶、箱排序。

 

排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序,这些记录的相对顺序保持不变,则称该算法是稳定的;若经排序后,记录的相对顺序发生了改变,则称该算法是不稳定的。

稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。另外,如果排序算法稳定,可以避免多余的比较;

1.稳定
   冒泡排序,插入排序,归并排序,基数排序
2.不稳定
   选择排序,希尔排序, 快速排序,堆排序
 


说明:
–当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次-数,时间复杂度可降至O(n);
–而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
–原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
 

附:本文参考博客1博客2整理而来

你可能感兴趣的:(数据结构)