排序算法汇总

排序

排序:将原本无序的序列按照关键字排列成有序的序列。
排序的稳定性:是指待排序的排序序列中有两个或两个以上相同的关键字,排序前和排序后这些关键字的相对位置没有发生变化。
eg:
排序前:2 3(a) 7 3(b) 1 3©
排序后:1 2 3(a) 3(b) 3© 7
这样的排序就是稳定的,如果出现:
排序后:1 2 3(a) 3© 3(b) 7
则该排序是不稳定的

内部排序

内部排序:将所有待排的数据全部载入到计算机内存中。
外部排序:文件比较大或数据量多涉及到对外部存储器的操作。

分类

排序

交换类排序

交换的算法

void swap(ElementType *left,ElementType *right){
	int temp = *right;
	*right = *left;
	*left = temp;
}

冒泡排序

冒泡排序是入门级的排序了,学C++的语法的时候这个冒泡排序就用了很多次了。
思想:
通过一系列的交换从上往下地完成有序。大的关键字就会沉淀到最底部,小的就会像气泡浮动到上层。
原始序列:
18 10 14 19 13 18 15
i = 最后一位
1) i 跟 i–比较。18>15,交换
18 10 14 19 13 18 15

2)18 10 14 19 13 15 18
i 跟 i–比较。13<15,不交换

3)18 10 14 19 13 15 18
i 跟 i–比较。19>13,交换

依次进行到底部

第一次结束后,得到得排序为:
10 18 13 14 19 15 18
可以发现最小的元素已经在最开始了,所以第二次不需要再拿最开始的元素进行比较
第二次类似,得到的排序为:
10 13 18 14 15 19 18
第三次:
10 13 14 18 15 18 19
第四次:
10 13 14 15 18 18 19
第五次:
10 13 14 15 18 18 19
第六次:
10 13 14 15 18 18 19
第七次:
10 13 14 15 18 18 19

不难发现:冒泡排序需要两层的循环才能把无序的序列遍历并做交换。内层循环完成除已排序的序列外,剩余关键字的最小值,外层循环是保证内层循环找到所有的最小关键字。

冒泡排序的基本算法

void Bubble_Sort(ElementType array[] , int N){
	for(int i = 0; i < N;i++){
		//记录前面的元素是否元素发生互换,若没有,则可以跳出循环了
		for(int j = N - 1;j > i ; j--){
			if(array[j] < array[j-1]){
				swap(&array[j],&array[j-1]);
			}
		}
	}
}

通过上面的例子,可以看出第五次之后的循环其实已经没有意义了,因为待排的序列已经有序了,所以冒泡算法优化可以在一趟排序中没有发生关键字交换提前结束改循环

冒泡算法的优化

void Bubble_Sort(ElementType array[] , int N){
	for(int i = 0; i < N;i++){
		//记录前面的元素是否元素发生互换,若没有,则可以跳出循环了
		bool flag = false;
		for(int j = N - 1;j > i ; j--){
			if(array[j] < array[j-1]){
				swap(&array[j],&array[j-1]);
				flag = true;//标识是否发生了互换
			}
		}
			if(flag == false){
				//全程无互换,则说明已经排好序了
				break;
			}
	}
}

时间复杂度的分析:通过代码可以看出,最好的情况就是第一次就是有序的序列则时间复杂度为O(N),最差的情况就是完全逆序,总的执行次数就是n(n-1)/2。最差时间复杂度为O(n*n)
空间复杂度在O(1).
以下是冒泡排序的运行时间,当数据量在10^5的时候,时间已经超时了。
在这里插入图片描述

快速排序

快速排序思路:分而治之!
1、每趟排序选择当前子序列的一个关键字作为基准
2、将子序列中比基准小的移到基准前面,比基准大的移到基准的后面,这样就会得到基准准确的位置。
这样找到的位置是基准最后一定所在的位置,没有意外!这可能是快速排序称为快速的原因!
原始序列:
18 10 19 14 13 18 15

首先是找到基准,找基准的方式有很多:
1、可以直接去头或尾部的元素
2、随机,但是rand()函数会消耗空间
3、取中位数

因为我学的是浙大的数据结构,奶奶教的是中位数的方法。所以这里也采用取中位数的方式。
首先,对初始序列找出18 14 15排序后就是15是中位数,同时对数组元素进行置换,可以得到:
14 10 19 15 13 18 18
然后我们把基准15换到最后一个位置,这样我们就保证了最左边是小于最右边的值,此时序列为:
14 10 19 18 13 18 15
接下来就是给基准找对位置:
14 10 19(i) 18 13(j) 18 15
此时出现(对应基准来说)左大右小的情况,我们要的是升序的排列,置换i,j对应的元素
14 10 13 18 19 18 15
下一步是
14 10 13(j) 18(i) 19 18 15
此时i>j,则说明已经遍历完了,基准应该跟i对应的位置互换,这时基准15必定是在i=3的位置,不会再移动了。
接着采用分而治之的思路,把数组从 0 ~( i-1)和i ~ end按照相同的方式确定每一个元素的位置

实现代码

//在三个数中寻找基准
ElementType Median3(ElementType A[] , int left,int Right){
	int center = (left + Right) / 2;
	if(A[left] > A[center]){
		swap( &A[left], &A[center]);
	}
	if(A[left] > A[Right]){
		swap( &A[left], &A[Right]);
	}
	if(A[center] > A[Right]){
		swap( &A[center], &A[Right]);
	}
	//此时确保了A[left] <= A[center] <= A[Right]
	//最后把基准A[center]塞到最后的位置,这样排序只需要从left+1->Right-2范围,因为left<center<Right-1
	swap( &A[center] , &A[Right-1] );
	//返回基准	
	return A[Right-1];
}
//快速排序的优点:每次让基准顺利找到自己的位置,下一次递归基准不再参与
void QSort(ElementType A[],int left,int Right){
	//基准
	int Pivot;
	//阀门 阀门必须大于1,元素大于阀门数量是采用快排,小于时才用简单排序
	int Cutoff = 2;
	if(Right - left >= Cutoff ){
		Pivot = Median3(A,left,Right);
		int Low = left;
		int Hight = Right - 1;
		while(1){
			while(A[++Low] < Pivot){;}
			while(A[--Hight] > Pivot){;}
			//如果low<height,则两边出现警报
			//否则遍历完成
			if(Low < Hight){
				//出现左右发出警报,则说明对应位置应该互换
				swap( &A[Low] , &A[Hight]);
			}else{
				break;
			}
		}
		//找到基准的位置,确保左小右大
		swap(&A[Low],&A[Right-1]);
		//递归解决左边
		QSort( A , left , Low-1);
		//递归解决右边
		QSort( A , Low + 1 , Right);
	}else{
		//元素太少采用插入排序
		InsertionSort(A + left,Right-left + 1);
	}
	

}

插入排序InsertionSort马上就讲,考虑到所有的排序算法有统一的接口,给出接口的调用

//快速排序的接口
void QuickSort(ElementType A[],int N){
	QSort(A,0,N-1);
}

快速排序的总结:
1、用到了递归,会占用内存空间
2、对小规模的数据尽量采用插入算法
3、阀门必须大于1,不然在分而治之的时候会把单个元素作为基准位置出现错误
4、基准的选择也是影响算法的一个重要的指标
5、时间复杂度的分析:平均时间复杂度O(nlog2n)最坏复杂度O(nn),空间复杂度主要是在递归的时候O(log2N),需要注意的是其实快速排序的时间复杂度是O(Xnlog2n),但快速排序的X在之后的排序算法中,X是最小的
快速排序的运行时间
在这里插入图片描述

插入类排序

插入排序

这个算法也不陌生。我的理解是腾位置算法。
看个例子,就懂了。
原始序列:
18 10 19 14 13 18 15
第一次遍历
18 10(i) 19 14 13 18 15
10比18小,直接取代那个位置
18 18 19 14 13 18 15
然后10再取代18最开始的位置
10 18 19 14 13 18 15
第二次遍历,i后移一位,遍历后没有变化
10 18 19(i) 14 13 18 15
第三次遍历
10 18 19 14(i) 13 18 15
19大于14,位置互换(后移)
10 18 19 19 13 18 15
18大于14,位置互换(后移)
10 18 18 19 13 18 15
元素后移实际就是给目标元素腾空位置,将14插入到对应位置
10 14 18 19 13 18 15
接下来按这种方式依次遍历后面的元素,就会得到:
10 13 14 15 18 18 19

插入排序的算法实现

/*
	插入排序
*/
void InsertionSort(ElementType array[] , int N ){
	int P , i;
	ElementType Tmp;

	for(P = 1; P < N ; P++){
		Tmp = array[P];
		for( i = P;i > 0 && Tmp < array[i-1];i--){
			array[i] = array[i - 1];
		}
		array[i] = Tmp;
	}
}

总结:
1、最坏情况是O(n*n),最好的情况是O(n)
2、空间复杂度为O(1)
然而,插入排序的测试运行居然比冒泡要快,震惊
在这里插入图片描述

折半插入排序

折半插入排序跟插入排序看了哈源码其实差不多,主要区别是查找插入位置的方法不一样,折半插入排序是通过折半查找找到对应的位置。有兴趣的小伙伴也可以去看看折半插入排序的源码。
关于折半插入排序的时间复杂度,最好情况就是O(nlogn),毕竟找的快,最坏的情况也是(nn),平均情况为O(nn)。空间复杂度是O(1)

希尔排序

是在直接排序上对序列分组,比如可以最开始以隔5个的分组,然后是隔2个的分组,最后再1个1个的分组
例子:
18 10 19 14 13 18 15

首先,隔5个就能分为18 18一组 10 15一组,这个例子没啥变化
然后是18 19 13 15一组 10 14 18一组,这样分组后再采用插入排序就得到
13 15 18 19一组 10 14 18一组,数组就是:
13 10 15 14 18 18 19
最后再一个一个的使用插入排序。实际上,如果数据量大并且选对正确的分增量,希尔排序的算法是非常快的,甚至是逼近了快速排序

关于选对正确的增量:
1、是希尔自己的规则:
⌊n/2⌋、⌊n/4⌋、⌊n/2^k⌋、2、1
时间复杂度是O(n*n)
2、Papernov&Stasevich提出的:
2^k+1、……、33、17、9、5、3、1
平均时间复杂度是O(n^1.5)
3、Hibbard 增量序列:
Dk= 2k–1
时间复杂度:
最坏情况:T = O ( N3/2)
猜想:Tavg= O ( N5/4)
4、Sedgewick增量序列:
9×4次方i–9×2i次方+ 1 或4i次方–3×2i次方+ 1
时间复杂度:
Tavg= O ( N7/6)
Tworst= O ( N4/3)

以Sedgewick增量的希尔排序源码:

void Shell_Sort(ElementType array[] , int N){
	int Si,i,j,k;
	int Tmp;
	//用一组Sedgewick增量
	int sedgewick[] = {36289,16001,8929,3905,2161,929,927,505,209,109,41,19,5,1,0};

	for(Si = 0;sedgewick[Si] >= N;Si++){
		;
	}
	//从大范围到小范围
	for(i = sedgewick[Si]; i > 0;i = sedgewick[++Si]){
		//快速排序
		for( j = i;j < N;j++){
			Tmp = array[j];
			for( k = j ;  k >= i && Tmp < array[k-i];k -= i ){
				array[k] = array[k-i];
			}
			array[k] = Tmp;
		}
	}
}

测试数据运行时间
在这里插入图片描述

归并类

归并排序

核心思路就是分而治之。同样举个例子,就明白了:
18 10 19 14 13 18 15
1)、把7个数组看成一个一个的数据
1、18
2、10
3、19
4、14
5、13
6、18
7、15
2)、两两合并
1、10 18
2、14 19
3、13 18
4、15
3、再两两合并
1、10 14 18 19
2、13 15 18
4、两两合并
结果就是10 13 14 15 18 18 19
如果对分治法很熟悉的小伙伴,归并排序就是小case了
归并排序的源码

/*
	归并排序 - 递归实现
	核心思路是分而治之
	属于外部排序,设计到了外部内存
*/
//将A[L]->A[R-1]和A[R]->A[RightEnd]合并
void Merge( ElementType A[], ElementType TmpA[], int L, int R, int RightEnd ){
	//得到左边结束的位置
	int LeftEnd = R-1;
	int Tmp = L;
	//记录操作的个数
	int NumsEles =  RightEnd - L + 1;
	while(L <= LeftEnd && R <= RightEnd){
		if(A[L] <= A[R]){
			TmpA[Tmp++] = A[L++];
		}else{
			TmpA[Tmp++] = A[R++];
		}
	}
	while(L <= LeftEnd){
		TmpA[Tmp++] = A[L++];
	}
	while(R <= RightEnd){
		TmpA[Tmp++] = A[R++];
	}
	for(int i = 0;i < NumsEles;i++,RightEnd--){
		A[RightEnd] = TmpA[RightEnd];
	}
}

//归并排序的核心代码
void Msort( ElementType A[], ElementType TmpA[], int L, int RightEnd ){
	//左边小于右边
	if(L < RightEnd){
		int center = ( L + RightEnd) / 2;
		//分而治之
		Msort(A,TmpA,L,center);//递归解决左边
		Msort(A,TmpA,center + 1,RightEnd);//递归解决右边
		Merge(A,TmpA,L,center + 1,RightEnd);
	}


}
/*归并排序的接口*/
void MergeSort( ElementType A[], int N ){
	ElementType *TmpA;
	//TmpA = new ElementType[N];
	TmpA = (ElementType*)malloc(sizeof(ElementType) * N);
	if(TmpA != NULL){
		Msort(A,TmpA,0,N-1);
		free(TmpA);
	}else{
		printf("overflow");
	}
}

归并排序非递归实现

void Merge_pass( ElementType A[], ElementType TmpA[], int N, int length ){
	int i;
	for(i = 0; i <= N - 2*length;i += 2*length){
		Merge(A	, TmpA , i , i+length , i+2*length-1);
	}
	//合并最后的一小段(剩余元素大于length 小于2*length)
	if(i + length < N){
		Merge( A , TmpA , i , i+length , N-1);
	}else{//剩余元素小于length
		for(int j = i; j <N ;j++){
			//直接移动到tmp中,下一次合并是通过tmp合到A中的
			TmpA[j] = A[j];
		}
	}
}
void Merge_Sort( ElementType A[], int N ){
	int length = 1;
	ElementType *TmpA = new ElementType[N];
	if(TmpA != NULL){
		while(length < N){
			Merge_pass( A , TmpA , N , length);
			length *= 2;
			Merge_pass( TmpA , A , N , length);
			length *=2 ;

		}
		delete TmpA;
	}else{
		cout << "error" << endl;
	}

}

归并排序时间复杂度分析:
所有情况时间复杂度都是O(NlogN),空间复杂度由于要用tmp数组所以空间复杂度为O(N)
归并排序的数据测试时间
在这里插入图片描述

选择类排序

选择排序

我jio得,选择排序实际上跟冒泡排序是很像的。大家可以去搜搜源码,看看就懂了。时间复杂度依然是O(n*n),甚至没有最好的情况。主要是堆排序

堆排序

堆排序的核心思路就是
1、将序列调整为一个最大堆。
2、然后把堆顶放到数组最后一位,然后再把0~size-1的元素调整为一个最大堆,堆顶放到已排好序的序列最前端。再把0 ~size-2调整为一个最大堆,不断进行。
3、最后就能得到一个排好序的数组。
这里就不举例子了,主要是最大堆的实现。最大堆在树的时候有码
堆排序的源码

//将N个元素的数组以A[p]为根的子堆调整为最大堆(该最大堆无哨兵)
void PercDown(ElementType A[] ,int p ,int N){
	int parent;
	int child;
	ElementType X;
	X = A[p];
	for(parent = p; ( parent * 2 + 1) < N;parent = child ){
		child = parent * 2 + 1;
		//指向左右孩子较大者,并且要防止后面child+1数组越位
		if( child != N-1 && A[child] < A[child  + 1]){
			child++;
		}
		//找到合适的位置
		if(X >= A[child]){
			break;
		}else{
			A[parent] = A[child];
		}
	}
	A[parent] = X;
}
//堆排序的核心代码
void headSort(ElementType A[] , int N){
	//建立堆
	int i;
	for( i = N/2 - 1;i >= 0;i--){
		PercDown(A , i , N);
	}
	//将最大堆的堆顶放到最后一段排好序的数组第一个上
	for(i = N - 1;i > 0;i--){
		swap(&A[i],&A[0]);
		PercDown(A , 0 , i);
	}

}

堆排序的数据测试时间
在这里插入图片描述时间复杂度是O(NlogN),空间复杂度O(1)。与快速排序相比,最大的优势就是堆排序的最坏时间复杂度也是O(NlogN),空间复杂度也是所有排序算法中最小的。

基数排序

基数排序的思想:多关键字排序
主要有两种实现方式:
1、最高位优先(MSD):先按最高位排成若干个子序列,再把子序列按次高位排序
2、最低为优先(LSD):最低为优先进行,不需要比较,只通过“收集”和“分配”。
以LSD举个例子(因为我也没有通过MSD解决过实际问题)
278 109 063 930 589 184 269 008 083
1)、第一次收集和分配,按照最后一位(个位),得到
桶0:930
桶1:无
桶2:无
桶3:063、083
桶4:184
桶5:无
桶6:无
桶7:无
桶8:278 008
桶9:109 589 269
得到序列为:
930 063 083 184 278 008 109 589 269
2)、第二次,在第一次的基础上按照中间位(十位)收集和分配
桶0:008 109
桶1:无
桶2:无
桶3:930
桶4:无
桶5:无
桶6:063 269
桶7:278
桶8:083 184 589
桶9:无
得到序列为:
008 109 930 063 269 278 083 184 589
3)、第三次,在第二次的基础上按照最高位(百位)收集和分配
桶0:008 063 083
桶1:109 184
桶2:269 278
桶3:无
桶4:无
桶5:589
桶6:无
桶7:无
桶8:无
桶9:930
最后就得到了有序的序列:
008 063 083 109 184 269 278 589 930
此时最高位有序,最高位相同的序列按中间为排序,中间位也相同的按最低为有序,最后整个序列达到有序。理解起来不难,但是实现还是有难度的
基数排序的实现源码

/*
	基数排序 - LSD算法
*/

//初始化关键字MaxDigit,基数Radix为十进制
#define MaxDigit 4
#define Radix 10
typedef int ElementType;
//桶结点
typedef struct Node *PtrToNode;
struct Node{
	int key;
	PtrToNode next;
};
/*桶的头节点*/
struct HeadNode{
	PtrToNode head,tail;
};
typedef struct HeadNode Bucket[Radix];

//得到当前位数,D必须<=MaxDigit
int GetDigit( int X, int D){
	//d记录小位值
	int d,i;
	for(i = 1;i <= D;i++){
		d = X % Radix;
		X = X / Radix;
	}
	return d;
}
//LSD以次位优先的基数排序
void LSDRadixSort(ElementType A[],int N){
	Bucket bucket;
	PtrToNode tmp,p,list = NULL;
	int D,Di;
	int i = 0;
	//初始化桶
	for( i = 0; i < Radix ; i++ ){
		bucket[i].head = bucket[i].tail = NULL;
	}
	//将A[]传入,逆序产生链表
	for(i = 0; i < N;i++){
		tmp = new Node;
		tmp->key = A[i];
		tmp->next = list;
		list = tmp;
	}
	//遍历所有的次序数字
	for(D = 1;D <= MaxDigit;D++){
		p = list;
		while(p){
			//对所有数字的当前次序数字依次入桶排列
			Di = GetDigit(p->key,D);
			//从链表中删除该数字
			tmp = p;
			p = p->next;
			tmp->next = NULL;
			//将当前值插入表尾
			if(bucket[Di].head == NULL){
				bucket[Di].head = bucket[Di].tail = tmp;
			}else{
				bucket[Di].tail->next = tmp;
				bucket[Di].tail = bucket[Di].tail->next;
			}

		}
		
		//重新构建list
		list = NULL;
		for(Di = Radix - 1;Di >= 0;Di--){
			//按照每个桶的元素顺序收集到list中
			if(bucket[Di].head){
				bucket[Di].tail->next = list;
				list = bucket[Di].head;
				//清空每一个桶
				bucket[Di].head = bucket[Di].tail = NULL;
			}
		}

	}
	for(i = 0;i < N ; i++ ){
		tmp = list;
		A[i] = tmp->key;
		list = list->next;
		delete tmp;
	}

}

时间复杂度分析:时间复杂度为O(P(N+B)),P表示关键字的位数,B表示关键字的个数,如930 P表示3位,B表示10个基数。空间复杂度是O(N+B)。MSD以后遇到了再加上源码。

这几种排序算法的比较
图片来自浙江大学数据结构后续遇到了具体应用的场合,再把不同场合应该使用的排序算法总结出来!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
排序算法中,时间复杂度是评估算法性能的重要指标。根据引用和引用的内容,下面是一些常见排序算法的时间复杂度汇总: 1. 冒泡排序:冒泡排序是一种简单但效率较低的排序算法。最坏情况下,冒泡排序的时间复杂度是O(n^2),其中n是待排序元素的数量。最好情况下,当数据已经有序时,冒泡排序的时间复杂度是O(n)。 2. 插入排序插入排序算法根据待排序序列中的元素逐个插入已排序序列的合适位置。最坏情况下,插入排序的时间复杂度也是O(n^2)。最好情况下,当数据已经有序时,插入排序的时间复杂度是O(n)。 3. 选择排序:选择排序是一种简单的排序算法,每次从未排序的部分选择最小(或最大)的元素,然后放到已排序部分的末尾。选择排序的时间复杂度始终为O(n^2),无论数据是否有序。 4. 快速排序快速排序是一种高效的排序算法,基于分治的思想。最坏情况下,快速排序的时间复杂度是O(n^2),但通常情况下,快速排序的平均时间复杂度是O(nlogn)。 5. 归并排序:归并排序是一种稳定且高效的排序算法,基于分治和合并的思想。归并排序的时间复杂度始终为O(nlogn),无论数据是否有序。 综上所述,不同的排序算法其时间复杂度不同。冒泡排序和插入排序的时间复杂度是O(n^2),选择排序的时间复杂度也是O(n^2),而快速排序和归并排序的时间复杂度是O(nlogn)。请注意,这些时间复杂度都是在最坏情况下估计的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值