数据结构:排序(Sort)【详解】

十大排序算法(冒泡排序、插入排序、选择排序、希尔排序、堆排序、快排、归并排序、桶排序、计数排序、基数排序) 阅读详情
AI摘要

本文详细介绍了包括冒泡排序、选择排序、插入排序等在内的多种经典排序算法,对比了它们的时间和空间复杂度,并分析了各自的稳定性特点。

排序

【知识框架】

在这里插入图片描述

排序概述

一、排序的相关定义

  1. 排序,就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。为了查找方便,通常希望计算机中的表是按关键字有序的。排序的确切定义如下:
    输入: n n n个记录 R 1 , R 2 , . . . , R n R_1,R_2,...,R_n R1​,R2​,...,Rn​,对应的关键字为 k 1 , k 2 , . . . , k n k_1,k_2,...,k_n k1​,k2​,...,kn​。
    输出:输入序列的一个重排 R 1 ′ , R 2 ′ , . . . , R n ′ {R_1}^{'},{R_2}^{'},...,{R_n}^{'} R1​′,R2​′,...,Rn​′,使得 k 1 ′ ≤ k 2 ′ ≤ . . . ≤ k n ′ {k_1}^{'}≤{k_2}^{'}≤...≤{k_n}^{'} k1​′≤k2​′≤...≤kn​′(其中“ ≤ ≤ ≤”可以换成其他比较大小的符号)

  2. 排序的稳定性。假设 k i = k j ( 1 ≤ i ≤ n , 1 ≤ j ≤ n , i ! = j ) k_i=k_j(1≤i≤n,1≤j≤n,i!=j) ki​=kj​(1≤i≤n,1≤j≤n,i!=j),且在排序前的序列中 R i R_i Ri​领先于 R j R_j Rj​(即 i < j i<j i<j)。如果排序后 R i R_i Ri​仍领先于 R j R_j Rj​,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中 R j R_j Rj​领先于 R i R_i Ri​,则称所用的排序方法是不稳定的。
    需要注意的是,算法是否具有稳定性并不能衡量一个算法的优劣,它主要是对算法的性质进行描述。如果待排序表中的关键字不允许重复,则排序结果是唯一的,那么选择排序算法时的稳定与否就无关紧要。

  3. 内排序和外排序。内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。

通常可以将排序算法分为插入排序、交换排序、选择排序、归并排序和基数排序五大类。每种排序算法都有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为 是最好的算法。内部排序算法的性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动的次数决定的。

二、排序用到的结构与函数

以下是一个用于排序用的顺序表结构。

#define MAXSIZE 10	//用于要排序数组个数最大值,可根据需要修改
typedef struct{
	int R[MAXSIZE];	//用于存储要排序的数组
	int length;	//用于记录顺序变的长度
}SqList;

另外,由于排序最常用到的操作是数组两元素的交换,所以我们将它写成函数。

/*交换L中数组r的下标为i和j的值*/
void swap(SqList *L, int i, int j){
	int temp = L->R[i];
	L->R[i] = L->R[j];
	L->R[j] = temp;
}

常见的排序算法

一、冒泡排序(交换排序)

1、算法

冒泡排序的基本思想是:从后往前(或从前往后)两两比较相邻元素的值,若为逆序,则交换它们,直到序列比较完。第一趟冒泡,结果是将最小的元素交换到待排序列的第一个位置(或将最大的元素交换到待排序列的最后一个位置),关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”(或关键字最大的元素如石头一般下沉至水底)。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素(或最大元素)放到了序列的最终位置…这样最多做 n − 1 n-1 n−1趟冒泡就能把所有元素排好序。
如下图所示,我们对序列 { 49 , 38 , 65 , 97 , 76 , 13 , 27 , 49 } \{49,38,65,97,76,13,27,49\} {49,38,65,97,76,13,27,49}进行冒泡排序:
在这里插入图片描述
冒泡排序算法的代码如下:

void BubbleSort(SqList *L){
	int i, j;
	bool flag = true;	//表示本趟冒泡是否发生交换的标志
	for(i=0; i< L->length-1; i++){	
		flag = false;	
		for(j=n-1; j>i; j--){	//一趟冒泡过程
			if(L->R[j-1] > L->R[j]){	//若为逆序
				swap(&L, j-1, j);	//交换
				flag = true;
			}
		}
		if(flag == false){
			return;	//本趟遍历后没有发生交换,说明表已经有序
		}
	}
}

2、性能分析

空间效率:仅使用了常数个辅助单元,因而空间复杂度为 O ( 1 ) O(1) O(1)。
时间效率:当初始序列有序时,显然第一趟冒泡后flag依然为false (本趟冒泡没有元素交换),从而直接跳出循环,比较次数为 n − 1 n-1 n−1,移动次数为 0 0 0,从而最好情况下的时间复杂度为 O ( n ) O(n) O(n);当初始序列为逆序时,需要进行 n − 1 n- 1 n−1趟排序,第 i i i趟排序要进行 n − i n -i n−i次关键字的比较,而且每次比较后都必须移动元素 3 3 3次来交换元素位置。这种情况下, 比较次数 = ∑ i = 1 n ( n − i ) = n ( n − 1 ) / 2 比较次数=\displaystyle\sum_{i=1}^{n}(n-i)=n(n-1)/2 比较次数=i=1∑n​(n−i)=n(n−1)/2 移动次数 = ∑ i = 1 n 3 ( n − i ) = 3 n ( n − 1 ) / 2 移动次数=\displaystyle\sum_{i=1}^{n}3(n-i)=3n(n-1)/2 移动次数=i=1∑n​3(n−i)=3n(n−1)/2从而,最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2),其平均时间复杂度也为 O ( n 2 ) O(n^2) O(n2)。


二、简单选择排序

简单选择排序法(Simple Selection Sort) 就是通过 n − i n-i n−i次关键字间的比较,从 n − i + 1 n-i+1 n−i+1个记录中选出关键字最小的记录,并和第 i ( 1 < i < n ) i (1<i<n) i(1<i<n)个记录交换之。

1、算法

void SelectSort(SqList *L){
	int i,j,min;
	for(i=0; i<L->length-1;i++){	//一共进行n-1趟
		min = i;	//记录最小元素位置
		for(j=i+i; j<L->length; j++){
			if(L->R[j] < L->R[min]){	//在R[i...n-1]中选择最小的元素
				min = j;	//更新最小元素位置
			}
		}
		if(min !=i){
			swap(L->R[i], L->R[min]);	//swap函数移动元素3次
		}
	}
}

2、性能分析

空间效率:仅使用常数个辅助单元,故空间效率为 O ( 1 ) O(1) O(1)。
时间效率:从上述代码中不难看出,在简单选择排序过程中,元素移动的操作次数很少,不会超过 3 ( n − 1 ) 3(n-1) 3(n−1)次,最好的情况是移动 0 0 0次,此时对应的表已经有序;但元素间比较的次数与序列的初始状态无关,始终是 n ( n − 1 ) / 2 n(n- 1)/2 n(n−1)/2次,因此时间复杂度始终是 O ( n 2 ) O(n^2) O(n2)。
稳定性:在第 i i i趟找到最小元素后,和第 i i i个元素交换,可能会导致第 i i i个元素与其含有相同关键字元素的相对位置发生改变。例如,表 L = { 2 , 2 , 1 } L= \{2,2, 1\} L={2,2,1},经过一趟排序后 L = { 1 , 2 , 2 } L= \{1, 2,2\} L={1,2,2},最终排序序列也是 L = { 1 , 2 , 2 } L=\{1,2,2\} L={1,2,2},显然, 2 2 2与 2 2 2的相对次序已发生变化。因此,简单选择排序是一种不稳定的排序方法。


三、直接插入排序

直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。

1、算法

void InsertSort(SqList *L){
	int i,j;
	//依次将R[2]~R[n]插入到前面已排序序列,R[1]为默认排好序的序列,R[0]作为哨兵不存放元素
	for(i=2; i<=L->length; i++){
		//若R[i]关键码小于其前驱,将A[i]插入有序表
		if(L->R[i] < L->R[i-1]){
			L->R[0] = L->R[i];	//复制为哨兵,R[0]不存放元素
			//从后往前查找待插入位置
			for(j=i-1; L->R[0]<L->R[j]; --j){
				L->R[j+1] = L->R[j];	//向后挪位
			}
			L->[j+1] = A[0];	//复制到插入位置
		}
	}
}

假定初始序列为 { 49 , 38 , 65 , 97 , 76 , 13 , 27 , 49 } \{49, 38, 65, 97 ,76, 13, 27, 49\} {49,38,65,97,76,13,27,49},初始时 49 49 49可以视为一个已排好序的子序列,按照上述算法进行直接插入排序的过程如下图所示,括号内是已排好序的子序列。
在这里插入图片描述

2、性能分析

空间效率:仅使用了常数个辅助单元,因而空间复杂度为 O ( 1 ) O(1) O(1)。
时间效率:在排序过程中,向有序子表中逐个地插入元素的操作进行了 n − 1 n-1 n−1趟,每趟操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。
在最好情况下,表中元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因而时间复杂度为 O ( n ) O(n) O(n)。
在最坏情况下,表中元素顺序刚好与排序结果中的元素顺序相反(逆序),总的比较次数达到最大,为 ∑ i = 2 n i \displaystyle\sum_{i=2}^{n}i i=2∑n​i,总的移动次数也达到最大,为 ∑ i = 2 n ( i + 1 ) 。 \displaystyle\sum_{i=2}^{n}(i+1)。 i=2∑n​(i+1)。
平均情况下,考虑待排序表中元素是随机的,此时可以取上述最好与最坏情况的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数均约为 n 2 / 4 n^2/4 n2/4。
因此,直接插入排序算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
稳定性:由于每次插入元素时总是从后向前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序方法。
适用性:直接插入排序算法适用于顺序存储和链式存储的线性表。为链式存储时,可以从前往后查找指定元素的位置。


四、折半插入排序

从直接插入排序算法中,每趟插入的过程中都进行了两项工作:①从前面的有序子表中查找出待插入元素应该被插入的位置;②给插入位置腾出空间,将待插入元素复制到表中的插入位置。注意到在该算法中,总是边比较边移动元素。下面将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。

1、算法

void HalfInsertSort(SqList *L){
	int i,j,low,high,mid;
	//依次将R[2]~R[n]插入前面的已排序序列
	for(i=2; i<=L->length;i++){
		L->R[0] = L->R[i];	//将R[i]暂存到R[0]
		low=1; high=i-1;	//设置折半查找的范围
		//折半查找(默认递增有序)
		while(low <= high){
			mid = (low + high) / 2;	//取中间点
			if(L->R[mid] > L->R[0]){
				high = mid-1;	//查找左半子表
			}else{
				low = mid+1;	//查找右半子表
			}
		}
		for(j = i-1; j>=high+1; --j){
			L->R[j+1] = L->R[j];	//统一后移元素,
		}
		L->R[high+1] = L->R[0];	//插入操作
	}
}

2、性能分析

从上述算法中,不难看出折半插入排序仅减少了比较元素的次数,约为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2​n),该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数 n n n;而元素的移动次数并未改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为 O ( n 2 ) O(n^2) O(n2),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。折半插入排序是一种稳定的排序方法。


五、希尔排序

希尔排序是对直接插入排序进行改进而得来的,又称缩小增量排序。
希尔排序的基本思想是:先将待排序表分割成若干形如 L [ i , i + d , i + 2 d , . . . , i + k d ] L[i,i+d,i+ 2d,...,i+ kd] L[i,i+d,i+2d,...,i+kd]的“特殊”子表,即把相隔某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
希尔排序的过程如下:先取一个小于 n n n的步长 d 1 d_1 d1​,把表中的全部记录分成 d 1 d_1 d1​组,所有距离为 d 1 d_1 d1​的倍数的记录放在同一组,在各组内进行直接插入排序;然后取第二个步长 d 2 < d 1 d_2<d_1 d2​<d1​,重复上述过程,直到所取到的 d t = 1 d_t= 1 dt​=1,即所有记录已放在同一组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,故可以很快得到最终结果。

1、算法

/*对顺序表L作希尔排序*/
void ShellSold(SqList *L){
	int i,j;
	int increment = L->length;	
	do{
		increment = increment/3 + 1;	//增量序列
		for(i = increment+1; i <= L->length; i++){
			if(L-R[i] < L->R[i-increment]){
				/*需将L->R[i]插入有序增量子表*/
				L->R[0] = L->R[i];	//暂存R[0]
				for(j = i-increment; j>0 && L->R[0]<L->R[j]; j-=increment){
					L->R[j+increment] = L->R[j];	//记录后移,查找插入位置
				}
				L->R[j+increment] = L->R[0];	//插入
			}
		}
	}while(increment > 1);
}

例如,当传入的SqList的的 l e n g t h = 9 length=9 length=9, R [ 10 ] = { 0 , 9 , 1 , 5 , 8 , 3 , 7 , 4 , 6 , 2 } R[10]=\{0,9,1,5,8,3,7,4,6,2\} R[10]={0,9,1,5,8,3,7,4,6,2}。排序的大致过程下图所示。

第一轮increment=4:
在这里插入图片描述
第二轮increment=2:
在这里插入图片描述
第三轮increment=1:
在这里插入图片描述
最终结果:
在这里插入图片描述

2、性能分析

空间效率:仅使用了常数个辅助单元,因而空间复杂度为 O ( 1 ) O(1) O(1)。
时间效率:由于希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当 n n n在某个特定范围时,希尔排序的时间复杂度约为 O ( n 3 / 2 ) O(n^{3/2}) O(n3/2)。要好于直接排序的 O ( n 2 ) O(n^2) O(n2)。
稳定性:当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序,因此希尔排序是一种不稳定的排序方法。
适用性:希尔排序算法仅适用于线性表为顺序存储的情况。


六、堆排序

堆排序(Heap Sort)是对简单选择排序进行的一种改进。

1、堆的定义

堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大根堆(如下图所示);或者每个结点的值都小于或等于其左右孩子结点的值,称为小根堆。
在这里插入图片描述

2、堆排序

堆排序的思路很简单:首先将存放在 L [ 1... n ] L[1...n] L[1...n]中的 n n n个元素建成初始堆,由于堆本身的特点(以大顶堆为例),堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大顶堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。可见堆排序需要解决两个问题:①如何将无序序列构造成初始堆?②输出堆顶元素后,如何将剩余元素调整成新的堆?

堆排序的关键是构造初始堆。 n n n个结点的完全二叉树,最后一个结点是第 ⌊ n / 2 ⌋ ⌊n/2⌋ ⌊n/2⌋个结点的孩子。对第 ⌊ n / 2 ⌋ ⌊n/2⌋ ⌊n/2⌋个结点为根的子树筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大者,则交换),使该子树成为堆。之后向前依次对各结点( ⌊ n / 2 ⌋ − 1 ⌊n/2⌋ -1 ⌊n/2⌋−1~ 1 1 1)为根的子树进行筛选,看该结点值是否大于其左右子结点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根结点。
举例自上往下逐步调整为大根堆,如下图所示。
在这里插入图片描述
继续已上图为例:
输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选。将 09 09 09和左右孩子的较大者 78 78 78交换,交换后破坏了子树的堆,继续对子树向下筛选,将 09 09 09和左右孩子的较大者 65 65 65交换,交换后得到了新堆,调整过程如下图所示。
在这里插入图片描述

3、算法

下面是建立大根堆的算法:

void BuildMaxHeap(ElemType A[], int len){
	for(int i=len/2; i>0; i--){	//从i=[n/2]~1,反复调整堆
		HeadAdjust(A, i, len);
	}
}

函数HeadAdjust将元素k为根的子树进行调整。

/*函数HeadAdjust将元素k为根的子树进行调整*/
void HeadAdjust(ElemType A[], int k, int len){
	A[0] = A[k];	//A[0]暂存子树的根节点
	for(i=2*k; i<=len; i*=2){	//沿key较大的子结点向下筛选
		if(i<len && A[i]<A[i+1]){
			i++;	//取key较大的子节点的下标
		}
		if(A[0] >= A[i]){
			break;	//筛选结束
		}else{
			A[k] = A[i];	//将A[i]调整到双亲结点上
			k = i;	//修改k值,以便继续向下筛选
		}
	}
	A[k] = A[0];	//被筛选结点的值放入最终位置
}

调整的时间与树高有关,为 O ( h ) O(h) O(h)。在建含 n n n个元素的堆时,关键字的比较总次数不超过 4 n 4n 4n,时间复杂度为 O ( n ) O(n) O(n),这说明可以在线性时间内将一个无序数组建成一个堆。
下面是堆排序算法:

void HeapSord(ElemType A[], int len){
	BuildMaxHeap(A, len);	//初始建堆
	for(i = len; i>1; i--){	//n-1趟的交换和建堆过程
		Swap(A, i, 1);	//输出堆顶元素(和堆底元素交换)
		HeapAdjust(A, 1, i-1);	//调整,把剩余的i-1个元素整理成堆
	}
}

同时,堆也支持插入操作。对堆进行插入操作时,先将新结点放在堆的末端,再对这个新结点向上执行调整操作。大根堆的插入操作示例如下图所示。
在这里插入图片描述

堆排序适合关键字较多的情况(如n>1000)。例如,在1亿个数中选出前100个最大值?首先使用一个大小为100的数组,读入前100个数,建立小顶堆,而后依次读入余下的数,若小于堆顶则舍弃,否则用该数取代堆顶并重新调整堆,待数据读取完毕,堆中100个数即为所求。

4、性能分析

空间效率:仅使用了常数个辅助单元,所以空间复杂度为 O ( 1 ) O(1) O(1)。
时间效率:建堆时间为 O ( n ) O(n) O(n),之后有 n − 1 n-1 n−1次向下调整操作,每次调整的时间复杂度为 O ( h ) O(h) O(h),故在最好、最坏和平均情况下,堆排序的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2​n)。
稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法。


七、归并排序

1、算法

归并排序与上述基于交换、选择等排序的思想不一样,“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表。假定待排序表含有 n n n个记录,则可将其视为 n n n个有序的子表,每个子表的长度为 1 1 1,然后两两归并,得到 ⌈ n / 2 ⌉ ⌈n/2⌉ ⌈n/2⌉个长度为 2 2 2或 1 1 1的有序表;继续两两…如此重复,直到合并成一个长度为 n n n的有序表为止,这种排序方法称为 2 2 2路归并排序。
下图所示为 2 2 2路归并排序的一个例子,经过三趟归并后合并成了有序序列。
在这里插入图片描述
Merge()的功能是将前后相邻的两个有序表归并为一个有序表。设两段有序表 A [ l o w . . . m i d ] A[low...mid] A[low...mid]、 A [ m i d + 1... h i g h ] A [mid+1...high] A[mid+1...high]存放在同一顺序表中的相邻位置,先将它们复制到辅助数组 B B B中。每次从对应 B B B中的两个段取出一个记录进行关键字的比较,将较小者放入 A A A中,当数组 B B B中有一段的下标超出其对应的表长(即该段的所有元素都已复制到 A A A中)时,将另一段中的剩余部分直接复制到 A A A中。算法如下:

ElemType *B = (ElemType *)malloc((n+1)*sizeof(ElemType));	//辅助数组B
void Merge(ElemType A[], int low, int mid, int high){
	//表A的两段A[low...mid]和A[mid+1...high]各自有序,将它们合并成一个有序表
	for(int k=low; k<=high; k++){
		B[k] = A[k];	//将A中所有元素复制到B中
	}
	for(i=low, j=mid+1, k=i; i<=mid && j<=high; k++){
		//从low到mid,j从mid+1到high,k是最终排序数组的下标
		if(B[i] <= B[j]){	//比较B左右两段的元素
			A[k] = B[i++];	//将较小值赋值给A,B左段下标加1,右段不动
		}else{
			A[k] = B[j++];	//将较小值赋值给A,B右段下标加1,左段不动
		}
	}
	while(i <= mid){	
		//若第一个表(左段)未检测完,复制
		A[k++] = B[i++];
	}
	while(j <= high){	
		//若第二个表(右段)未检测完,复制
		A[k++] = B[j++];
	}
}

一趟归并排序的操作是,调用 ⌈ n / 2 h ⌉ ⌈n/2h⌉ ⌈n/2h⌉次算法 m e r g e ( ) merge() merge(),将 L [ 1... n ] L[1...n] L[1...n]中前后相邻且长度为 h h h的有序段进行两两归并,得到前后相邻、长度为 2 h 2h 2h的有序段,整个归并排序需要进行 ⌈ l o g 2 n ⌉ ⌈log_2n⌉ ⌈log2​n⌉趟。递归形式的 2 2 2路归并排序算法是基于分治的,其过程如下。
分解:将含有 n n n个元素的待排序表分成各含 n / 2 n/2 n/2个元素的子表,采用 2 2 2路归并排序算法对两个子表递归地进行排序。
合并:合并两个已排序的子表得到排序结果。

viod MergeSort(ElemType A[], int low, int high){
	if(low < high){
		int mid = (low + high)/2;	//从中间划分两个子序列
		MergeSort(A, low, mid);	//对左侧子序列进行递归排序
		MergeSort(A, mid+1, high);	//对右侧子序列进行递归排序
		Merge(A, low, mid, high);	//归并
	}
}

用图像描述,整个的递归拆分加归并排序的过程大概如下:
在这里插入图片描述

2、性能分析

空间效率: Merge()操作中,辅助空间刚好为 n n n个单元,所以算法的空间复杂度为 O ( n ) O(n) O(n)。
时间效率:每趟归并的时间复杂度为 O ( n ) O(n) O(n),共需进行 ⌈ l o g 2 n ⌉ ⌈log_2n⌉ ⌈log2​n⌉趟归并,所以算法的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2​n)。
稳定性:由于Merge()操作不会改变相同关键字记录的相对次序,所以2路归并排序算法是一种稳定的排序方法。


八、快速排序

快速排序算法,被列为20世纪十大算法之一。快速排序是所有内部排序算法中平均性能最优的排序算法。

前面介绍及几种算法中,希尔排序相当于直接插入排序的升级,它们同属于插入排序类,堆排序相当于简单选择排序的升级,它们同属于选择排序类。而快速排序是冒泡排序的升级,它们都属于交换排序类。即它也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动交换次数。

快速排序(Quick Sort) 的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小, 则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

1、算法

假设关键字数组为 { 49 , 38 , 65 , 97 , 76 , 13 , 27 } \{49,38,65,97,76,13,27\} {49,38,65,97,76,13,27}。快速排序算法的核心逻辑就是先选取当中的一个关键字,比如选择第一个关键字49,然后想尽办法将它放到一个位置,使得它左边的值都比它小,右边的值比它大,我们将这样的关键字称为枢轴(pivot)。

快速排序的基本思想是基于分治法的:在待排序表 L [ 1... n ] L[1...n] L[1...n]中任取一个元素 p i v o t pivot pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分 L [ 1... k − 1 ] L[1...k-1] L[1...k−1]和 L [ k + 1... n ] L[k+1...n] L[k+1...n],使得 L [ 1... k − 1 ] L[1...k-1] L[1...k−1]中的所有元素小于 p i v o t pivot pivot, L [ k + 1... n ] L[k+1...n] L[k+1...n]中的所有元素大于等于 p i v o t pivot pivot,则 p i v o t pivot pivot放在了其最终位置 L ( k ) L(k) L(k)上,这个过程称为一趟快速排序(或一次划分)。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
一趟快速排序的过程是一个交替搜索和交换的过程,下面通过实例来介绍,附设两个指针 i i i和 j j j,初值分别为 l o w low low和 h i g h high high,取第一个元素 49 49 49为枢轴赋值到变量 p i v o t pivot pivot。
指针j从 h i g h high high往前搜索找到第一个小于枢轴的元素 27 27 27,将 27 27 27交换到 i i i所指位置。
在这里插入图片描述
指针 i i i从 l o w low low往后搜索找到第一个大于枢轴的元素 65 65 65,将 65 65 65交换到 i i i所指位置。
在这里插入图片描述
指针 j j j继续往前搜索找到小于枢轴的元素 13 13 13,将 13 13 13交换到i所指位置。
在这里插入图片描述
指针 i i i继续往后搜索找到大于枢轴的元素 97 97 97,将 97 97 97交换到j所指位置。
在这里插入图片描述
指针j继续往前搜索小于枢轴的元素,直至 i = = j i==j i==j。
在这里插入图片描述
此时 i = = j i==j i==j,指针 i i i之前的元素均小于 49 49 49,指针 i i i之后的元素均大于等于 49 49 49,将 49 49 49放在 i i i所指位置即其最终位置,经过一趟划分, 将原序列分割成了前后两个子序列。
在这里插入图片描述
按照同样的方法对各子序列进行快速排序,若待排序列中只有一个元素,显然已有序。
在这里插入图片描述
代码实现大致如下:

void QuickSort(ElemType A[], int low, int high){
	if(low < high){
		//Partition()就是划分操作,将表A[low...high]划分为满足上述条件的两个子表
		int pivotpos = Partition(A, low, high);	//划分
		QuickSort(A, low, pivotpos-1);	//依次对两个子表进行递归操作
		QuickSort(A, pivotpos+1, high);
	}
}

假设每次总以当前表中第一个元素作为枢轴来对表进行划分,则将表中比枢轴大的元素向右移动,将比枢轴小的元素向左移动,使得一趟Partition ()操作后,表中的元素被枢轴值一分为二。代码如下:

int Partition(ElemType A[], int low, int high){
	ElemType pivot = A[low];	//将当前表中第一个元素设为枢轴,对表进行划分
	while(low < high){
		while(low < high && A[high] >= pivot){
			--high;	
		}
		A[low] = A[high]	//将比枢轴小的元素移动到左端
		while(low < high && A[low] <= pivot){
			++low;	
		}
		A[high] = A[low];	//将比枢轴大的元素移动到右端
	}
	A[low] = pivot;	//枢轴元素存放到最终位置
	return low;	//返回存放枢轴的最终位置
}

2、性能分析

空间效率:由于快速排序是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量应与递归调用的最大深度一致。最好情况下为
O ( l o g 2 n ) O(log_2n) O(log2​n);最坏情况下,因为要进行 n − 1 n-1 n−1次递归调用,所以栈的深度为 O ( n ) O(n) O(n);平均情况下,栈的深度为 O ( l o g 2 n ) O(log_2n) O(log2​n)。
时间效率:快速排序的运行时间与划分是否对称有关,快速排序的最坏情况发生在两个区域分别包含 n − 1 n-1 n−1个元素和 0 0 0个元素时,这种最大程度的不对称性若发生在每层递归上,即对应于初始排序表基本有序或基本逆序时,就得到最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
有很多方法可以提高算法的效率:一种方法是尽量选取一个可以将数据中分的枢轴元素,如从序列的头尾及中间选取三个元素,再取这三个元素的中间值作为最终的枢轴元素;或者随机地从当前表中选取枢轴元素,这样做可使得最坏情况在实际排序中几乎不会发生。
在最理想的状态下,即Partition ()可能做到最平衡的划分,得到的两个子问题的大小都不可能大于 n / 2 n/2 n/2,在这种情况下,快速排序的运行速度将大大提升,此时,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2​n)。 好在快速排序平均情况下的运行时间与其最佳情况下的运行时间很接近,而不是接近其最坏情况下的运行时间。快速排序是所有内部排序算法中平均性能最优的排序算法。
稳定性:在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它们的相对位置会发生变化,即快速排序是一种不稳定的排序方法。

各种排序算法的比较

在这里插入图片描述

附录

上文链接

数据结构:查找

专栏

数据结构专栏

参考资料

1、严蔚敏、吴伟民:《数据结构(C语言版)》
2、程杰:《大话数据结构》
3、王道论坛:《数据结构考研复习指导》
4、托马斯·科尔曼等人:《算法导论》

【数据结构】七大排序算法(超详细) 本文详细介绍了数据结构中的七大排序算法 阅读详情

相关推荐

在elementUI中sort-orders排序,默认为三种,怎么改成两种

在 table表单中添加sort-change事件, :sort-orders="[‘ascending’, ‘descending’]" <el-table-column prop="pubDate" label="出版日期" width="150px" sortable='custom' :sort-orders="['ascending', 'descending']" show-overflow-tooltip></el-table-column> 这里 我们这样写 但是有

milijiangjun的博客 6277

三分钟学会sort排序详解(新手版)

C++新生教程

2301_80284843的博客 2万+

UE5入门第二周总结:基于蓝图的国际象棋的实现

在C++的学习中,这是一个入门级别的项目。我们的目标是灵活运用蓝图的特性和强大的可视化功能,做出这一入门级项目的同时,思考如何优化并改进算法,并尽可能的用面向对象的方法实现,用蓝图中的继承和多态简化编码过程。

YoungeYang的博客 1259

排序(7种常见排序方法)

常见的排序算法:按照分:1.直接插入排序 2.希尔排序按照分:1.选择排序 2.堆排序按照分:1.冒泡排序 2.快速排序按照分:1.归并排序常见的排序思想:是使用双指针或者三指针,在同一个数组上遍历、比较、交换。

Kai的学习笔记 4432

华为手机连接Win11失败?内核隔离与驱动签名冲突的终极解决指南

在Windows操作系统中,驱动程序是硬件与系统通信的桥梁,其数字签名机制是保障系统内核安全的核心防线。基于虚拟化的安全技术通过强制验证驱动签名,能有效隔离恶意代码,提升系统抵御高级威胁的能力。然而,严格的签名策略有时会与部分硬件驱动产生兼容性问题,导致设备无法识别,直接影响文件传输、应用调试等核心工作流。本文聚焦于华为手机在开启Windows 11内核隔离功能时出现的连接故障,深入剖析驱动签名验证与Hypervisor保护的代码完整性原理,并提供从驱动更新、策略调整到无线替代方案的完整解决路径,帮助用户在

weixin_31309419的博客 351

数据标注的下一代SOP,可能不是Word,而是一段Prompt

再给几个反例:“这个只是正常纹理,不算。真正用于数据生产的Prompt,可能至少包含:任务定义、正例、反例、边界案例、优先级、输出格式、验收逻辑。一个项目100个人,如果100个人对这句话有100种理解,最后返工的就不是一个框,而可能是几十万帧数据。一个项目启动以后,项目经理先研究SOP,培训组长,组长再培训标注员,最后几百个人按照同一套规则干活。实际上一个成熟的项目经理,最重要的能力之一,就是:把客户的业务语言翻译成生产规则。真正让项目难交付的,从来不是80%的普通数据,而是那20%的长尾数据。

AI数据标注猿 295

sort排序详解

sort函数默认的排序方式是升序排序,即从小到大。 1.对简单的数组排序 简单来说就是sort(begin,end,cmp); sort函数中参数有三个(第三个可以省略) 其中begin是排序数组的起始地址 end是排序数组的结束地址(最后一位要排序元素的地址)这两个参数都是地址。 #include <iostream> #include<algorithm> using...

qq_43811879的博客 2万+

Synopsys的CoreConsultant使用简析

CoreConsultant使用简析

weixin_58165606的博客 1万+

数据结构 堆排序(Heap Sort) 详解 附C++代码实现:

目录 简介: 算法描述: 代码实现: 总结: 简介: 堆排序是利用堆这种数据结构所设计的排序算法,是一种近似于完全二叉树。分为大顶堆和小顶堆。时间复杂度为:o(nlog2(n)),不稳定。 算法描述: 知识储备:一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1...

小渣渣的博客 6119

python——体育收入排行2012-2019

请分析文件并读取数据,完成以下功能: 1. 某年的收入排名前若干名选手 如果输入n是2012-19之间的年份,则可以输入整数k,输出n年的前k名排名信息。如果k大于当年排名运动员人数,则输出当年所有的信息。每行一名运动员信息,以' | '间隔该行各项信息。 2. 某年属于某个运动项目的所有运动员信息,和收入总和 如果输入的是'sport'(不区分大小写),则可以继续输入一个年份201*,输出一个当年排行中的运动项目菜单,按照字符串升序排列,格式见示例,再输入运动项目对应的数字来输出该项运动当

m0_62488776的博客 2257

29、几何形态测量学在蝴蝶翅膀研究中的应用

本博客介绍了几何形态测量学在花斑木蛱蝶(Pararge aegeria)翅膀研究中的应用,探讨了环境因素对蝴蝶翅膀大小和形状的影响。通过选择合适的地标并采用Procrustes分析方法,研究揭示了不同景观类型下翅膀形状的差异及其潜在的适应性意义。文章还讨论了发育可塑性在飞行形态变异中的作用,并比较了几何形态测量学与其他传统方法的优势。最后提出了未来研究方向,包括多因素综合分析、发育机制探索及长期生态监测,为理解蝴蝶的适应性进化提供了重要线索。

ff678的博客 107

数据结构 计数排序(Counting Sort) 详解 附C++代码实现:

目录 简介: 算法描述: 代码实现: 总结: 简介: 计数排序是一种非比较类排序算法,稳定的算法,时间、空间复杂度为o(n+k) 算法描述: 找出待排序的数组中最大和最小的元素; 统计数组中每个值为i的元素出现的次数,存入数组C的第i项; 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加); 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放...

小渣渣的博客 7323

基于Simulink的智能车辆电子稳定控制(ESC)仿真

本文完成了基于 Simulink 的智能车辆电子稳定控制(ESC),实现了:✅ 构建含轮胎非线性的车辆动力学模型✅ 设计滑模横摆稳定性控制器✅ 实现四轮制动力分配逻辑✅ 验证在双移线低附着场景下的卓越稳定性✅ 为主动安全控制系统开发提供完整范式。

xiaoheshang_123的博客 78

数据结构 插入排序(InsertionSort Sort) 详解 附C++代码实现:

目录 简介: 算法描述: 代码实现: 总结: 简介: 是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 时间复杂度为o(n^2),是一种稳定的排序算法,比较类排序算法 算法描述: 从第一个元素开始,该元素可以认为已经被排序; 取出下一个元素,在已经排序的元素序列中从后向前扫描; 如果该元素(已排序...

小渣渣的博客 5642

CGAL AlphaShape算法(自适应Alpha值,二维版本)

CGAL AlphaShape算法(自适应Alpha值,二维版本)

dayuhaitang1的博客 2323

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

数据结构 基数排序(Radix Sort) 详解 附C++代码实现:

目录 简介: 图解: C++代码实现: 总结: 简介: 基数排序的发明可以追溯到1887年赫尔曼何乐礼在打孔卡片制表机上的贡献。 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。 按低位优先级排序或按高位优先级排序取决于自己兴趣,下面的代码按低位优先。 算法简述: 1.取得数组中最大的数的位数max_bit 2.分散:将数组的所有数按...

小渣渣的博客 7277

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

php函数sort,rsort,ksort,krsort,asort,arsort区别详解

sort,rsort,ksort,krsort,asort,arsort函数用来对数组进行排序,本篇笔记对这6个函数的功能区别,传入参数和返回值进行了介绍,并通过范例加深了解他们之间的区别 函数功能 函数 功能 sort 按数组的值进行升序排序 rsort 按数组的值进行降序排序 ksort 按数组的键进行升序排序,并保留键值关系 krsort 按数组的键进行降序排序,并保留键值关系 asort 按数组的值进行升序排序,并保留键值关系 arsort 按数组的值进行降序.

再现理想 865

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

蓝桥杯C/C++排序实战:std::sort高效安全调用指南

排序是计算机基础算法中的核心操作,其本质是通过比较和交换实现数据的有序重排;底层依赖于稳定的时间复杂度模型(如O(n log n))与内存访问局部性优化;技术价值在于显著降低工程实现成本、规避手写排序的边界错误与性能陷阱;典型应用场景包括竞赛题中的多级排序、Top-K筛选、贪心预处理及结构体字段优先级控制;在蓝桥杯等限时编程赛事中,正确使用std::sort配合lambda自定义比较,可将原本需15分钟的手写调试压缩至5分钟内精准AC,尤其适用于vector容器与严格弱序要求的struct排序。

weixin_30522983的博客 201

C++ sort()排序详解

本文对C++中的sort()做了一个比较详细的说明,包括为什么选择使用sort()、sort()函数的实现原理、sort()的使用方法等等。

晴空๓ 59万+

Golang sort 排序

1.前言 开发过程中,我们经常需要对元素进行排序,使用 Go 我们可以轻松实现。 Go 内置 sort 包中提供了根据一些排序函数可对任何序列进行排序,并提供自定义排序规则的能力。 sort 包实现了四种基本排序算法:插入排序(Shell 排序)、归并排序、堆排序和快速排序。 但是这四种排序方法是不公开的,它们只被用于 sort 包内部使用,sort 包会根据实际数据自动选择高效的排序算法。 Go sort 包主要提供了三种排序能力: (1)基本类型切片排序; (2)自定义比较器; (3)排序任意数据结构。

Doublelv 的博客专栏。 9805

Java的Arrays.sort()方法到底用的什么排序算法

Java的Arrays.sort()方法到底用的什么排序算法 . 转:https://www.cnblogs.com/baichunyu/p/11935995.html . 暂时网上看过很多JDK8中Arrays.sort的底层原理,有些说是插入排序,有些说是归并排序,也有说大于域值用计数排序法,否则就使用插入排序。。。其实不全对。让我们分析个究竟: // Use Quicksort on small arrays if (right - left < QUICKSORT_THRESHOLD)

yuanchangliang的博客 1万+

知无涯之std::sort源码剖析

从事程序设计行业的朋友一定对排序不陌生,它从我们刚刚接触数据结构课程开始便伴随我们左右,是需要掌握的重要技能。任何一本数据结构的教科书一定会介绍各种各样的排序算法,比如最简单的冒泡排序、插入排序、希尔排序、堆排序等。在现已知的所有排序算法之中,快速排序名如其名,以快速著称,它的平均时间复杂度可以达到O(N logN),是最快排序算法之一。 转自:http://feihu.me/blog/2014

刘日新的博客 1万+

go排序sort包详解

sort包提供了排序切片和用户自定义数据集的函数。

General_zy的博客 1135

【C++数据结构算法】排序sort---实用Algorithm函数详解

sort进行排序操作---十分隐蔽的使用规范!!

起床气233的博客 873

Arrays.sort与Collections.sort:深入解析Java中的排序算法

Arrays.sort和Collections.sort是最常用的两种。尽管它们都旨在实现数据的排序,但在实现细节、算法选择以及应用场景上却存在显著的差异。本文将深入探讨这两种排序方法,并详细解析它们背后的排序算法,以期为Java开发者提供更为全面的指导。

星途码客的博客 1592

【原子工具】排序之sort(您没见过的全新视角)

如何在理想情况下的激进优化与最坏情况下的稳健防御之间取得平衡?这其实与你选择考研策略时的风险决策完全同构。根据数据特征动态切换三种算法,而这就是sort的决策内核。快速排序是冲刺阶段的创业团队(快速试错)​堆排序是危机时期的审计部门(稳定兜底)插入排序是优化细节的品控小组(局部微调)这是sort函数的内省(Introsort),而在我们的学习生活中我们也应该不断内省(introspection)// 自定义比较函数(注意参数类型要与malloc分配的数组匹配)

xiexunshizz的博客 807
上一篇: 数据结构:查找(Search)【详解】
下一篇: 共计10万余字!数据结构知识详细梳理!
UniqueUnit
博客等级 码龄9年 4万+粉丝 15原创
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值