八大排序算法分析

0

博客内容

  • 简要解释每一种排序算法思想,对每种排序的代码进行解读
  • 对比复杂度,稳定性,并列表。
  • 典型题目分析

时间复杂度下界

  • 逆序对:i<j,如果A[i]>A[j],(i,j)即为逆序对,而交换两个相邻元素正好是消去一个逆序对。

  • 定理:任意N个不同元素组成的序列平均有N(N-1)/4个逆序对,所以任何以交换相邻两个元素来排序的算法,平均时间复杂度为 O ( N 2 ) O(N^2) O(N2)

冒泡排序

主要思想:两个for循环,一个元素一个元素进行比较,是稳定的
t7ECUe.gif

void Bubble_Sort(ElementType A[], int N) {
	for(int P=N-1;P>=0;P--) {
		int flag = 0;
		for(int i=0;i<P;i++) {
			if(A[i]>A[i+1]) 
				Swap(A[i],A[i+1]); //如果没有被执行,则这个序列本来就有序
				flag = 1; //标识发生了交换
		}
		if(flag==0) break;
	}
}

最好:开始即有序
最坏:正好是逆序, T = O ( N 2 ) T=O(N^2) T=O(N2)

插入排序

不断插入就不断地排序,即确保子序列为有序的

  1. 从第二位开始遍历,
  2. 当前数(第一趟是第二位数)与前面的数依次比较,如果前面的数大于当前数,则将这个数放在当前数的位置上,当前数的下标-1,
  3. 重复以上步骤,直到当前数不大于前面的某一个数为止,这时,将当前数,放到这个位置,1-3步就是保证当前数的前面的数都是有序的,内层循环的目的就是将当前数插入到前面的有序序列里
  4. 重复以上3步,直到遍历到最后一位数,并将最后一位数插入到合适的位置,插入排序结束。

2

  • 代码
void Insertion_Sort(ElementType A[], int S, int N) {
	for(int P=S; P<N; P++) {
		int tmp = A[P]; //摸下一张牌
		for(int i=P; i>0 && A[i-1]>tmp; i--) 
			A[i] = A[i-1];
		A[i] = tmp; //新牌
	}
}

最好:只需要摸牌, T = O ( N ) T=O(N) T=O(N)
最坏:逆序, T = O ( N 2 ) T=O(N^2) T=O(N2)

希尔排序

从大到小的间隔,来进行多次排序。每次排序都不会影响上一次排序的结果。

  • 定义增量序列,最后一次间隔都是1

在这里插入图片描述

  • 原始希尔排序算法:
void Shell_sort(ElementType A[], int N) {
	for(int D=N/2;D>0;D/=2) { //定义增量序列
		for(int P=D;P<N;P++) { //插入排序
			ElementType tmp = A[P];
			for(int i=P;i>=D&&A[i-D]>tmp; i-=D)
				A[i] = A[i-D];
			A[i] = tmp;
		}
	}
}
  • 时间复杂度

最坏: T = O ( N 2 ) T=O(N^2) T=O(N2)

当增量序列元素之间并不互质时,原始希尔排序每一次增量排序都没有进行实质性操作,导致只有最后一次有效。例如:16:8,4,2,1。

  • Hibbard增量序列: D k = 2 k − 1 D_k = 2^k-1 Dk=2k1,此时最坏情况: O ( N ( 5 / 4 ) ) O(N^(5/4)) O(N(5/4))

  • 空间复杂度

选择排序

从A[i]到A[N-1]中寻找最小元,并将其位置献给MinPosition。

再将此未排序部分的最小元换到有序部分的最后位置。
在这里插入图片描述

  • 原始代码:
void Selection_Sort(ElementType A[], int N) {
	for(int i=0;i<N;i++) {
		int MinPosition = ScanForMin(A,i,N-1);//寻找最小元
		Swap(A[i], A[MinPosition])
	}
}
  • 时间复杂度

最坏:每一次排序都需要交换

不管原始数组是否有序,时间复杂度都是 O ( n 2 ) O(n^2) O(n2),关键是快速找到最小元。

  • 空间复杂度
    只定义了两个辅助变量且与n的大小无关,所以空间复杂度为O(1)

堆排序

对选择排序的优化

堆:完全二叉树且具有如下性质

  • 大顶堆:每个结点值都大于或等于其左右孩子结点的值
  • 小顶堆:每个结点值都小于或等于其左右孩子结点的值

左右孩子的结点下标为2i+1,2i+2

具体过程

  • 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
  • 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  • 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

在这里插入图片描述

  • 算法1: O ( n l o g n ) O(nlogn) O(nlogn),需要额外的O(N)空间,复制元素消耗时间
void Heap_Sort(ElementType A[], int N) {
	BuildHeap(A); //建立最小堆 O(N)
	for(int i=0;i<N;i++)
		tmpA[i] = DeleteMin(A);//最小元存储 O(logN)
	for(int i=0;i<N;i++)
		A[i] = tmpA[i];//又将存储的最小元都导回原数组 O(N)
}
  • 合理算法::调整为最大堆(升序)
void Heap_Sort(ElementType A[], int N) {
	for(int i=N/2; i>=0; i--)
		PercDown(A, i, N); //建立最大堆
	for(int i=N-1; i>0; i--) {
		Swap(&A[0], &A[i]);
		PercDown(A, 0, i);
	}
}
  • 算法中的子方法:
//调整大顶堆
void PercDown(ElementType A[], int i, int N) {
	int temp = A[i];//取出当前元素
	for(int k=i*2+1; k<N; k=k*2+1) {
		if(k+1<N&& A[k]<A[k+1]) 
			k++;//左子结点小于右结点,k指向右子结点
		if(A[k]>temp) {//子结点大于父节点,直接赋值给父节点
			A[i] = A[k];
			i = k;
		} else {
			break;
		}
	}
	A[i] = temp;
}

//交换
void Swap(ElementType A[], int a, int b) {
	int temp = A[a];
	A[a] = A[b];
	A[b] = temp;
}

归并排序

对有序子序列的归并

  • 具体代码
    核心交换
/* L:左边起始位置,R:右边起始位置,RightEnd:右边终点位置*/
void Merge(ElementType A[], ElementType tempA[], int L, int R, int RightEnd) {
	int LeftEnd = R - 1;
	int temp = L;
	int NumElements = RightEnd - L + 1;//元素总个数
	//归并过程
	while(L<=LeftEnd&&R<=RightEnd) {
		if(A[L]<=A[R]) tempA[temp++] = A[L++];
		else tempA[temp++] = A[R++];
	}
	//多余就直接拼接
	while(L<=LeftEnd)
		tempA[temp++] = A[L++];
	while(R<=RightEnd)
		tempA[temp++] = A[R++];
	//L已经被使用过了
	for(int i=0;i<NumElements;i++,RightEnd--) {//RightEnd未被使用
		A[RightEnd] = tempA[RightEnd];
	}
}

分而治之:递归: T ( N / 2 ) + T ( N / 2 ) + O ( N ) , T ( N ) = O ( N l o g N ) T(N/2)+T(N/2)+O(N),T(N)=O(NlogN) T(N/2)+T(N/2)+O(N),T(N)=O(NlogN)
4

void MSort(ElementType A[], ElementType tempA[], int L, int RightEnd) {
	int center;
	if(L<RightEnd) {
		center = (L+RightEnd) /2;
		MSort(A,tempA,L,center);
		MSort(A,tempA,center+1,RightEnd);
		Merge(A,tempA,L,center+1,RightEnd);
	}
}

void Merge_sort(ElementType A[], int N) {
	ElementType *tempA;
	tempA = malloc(N*sizeof(ElementType));//申请空间
	if(tempA!=NULL) {
		MSort(A,tempA,0,N-1);
		free(tempA);
	} else {
		ERROR("空间不足");//报错方法
	}
}

分而治之:非递归算法
并不需要开很多临时数组,只需要开一个临时数组,临时数组与原数组之间进行数据交换。
4

void Merge_pass(ElementType A[], ElementType tempA[], int N, int length) {
	for(int i=0;i<=N-2*length; i+= 2*length) 
		Merge1(A, tempA, i, i+length, i+2*length-1);//不需要将tempA中元素导回A中,最后有序也是放在tempA中的
	if(i+length<N)
		Merge1(A,tempA,i,i+length,N-1);
	else //最后只剩下一个子列
		for(int j=i;j<N;j++) tempA[j] = A[j];
}

void Merge_sort(ElementType A[], int N) {
	ElementType *tempA;
	tempA = (ElementType *)malloc(N*sizeof(ElementType));//动态分配,<cstdlib>
	if(tempA!=NULL) {
		while(length<N) {
			Merge_pass(A,tempA,N,length);
			length *= 2;
			Merge_pass(tempA,A,N,length);//确保最后A是有序的
			length *= 2;
		}
		free(tempA);
	}
	else ERROR("空间不足");
}

优点:时间复杂度、稳定
缺点:空间不足

快速排序

分而治之:从数组中挑选一个数作为主元,将原来的数组分成两部分:小于和大于。最后将左右两边放在一起
t7AO3R.jpg

  • 选中元:取头、中、尾的中位数
/*选中元:*/
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]);
	//得到了三个原始位置的小,中,大
	Swap(&A[center],&A[Right]);//这个时候利用三个元素的中间值作为基准数
	return A[Right];
}
  • 子集划分

当元素=pivot时:停下来交换

  • 小规模数据处理:先用递归,但是递归会影响性能,所以设置一个阈值cutoff值,当数据小于阈值时停止递归。

运行1000次测试【java】发现,这种快排的方法运行速度大于原始快排的比例只有不到5%,疑惑…

//递归程序
void Quicksort(ElementType A[], int Left, int Right) {
	int Cutoff = 3//自行设置一个值,当间距小于阈值时进行快速排序
	if(Cutoff<=Right-Left) {
		int Pivot = Median3(A, Left, Right);
		int i = Left; int j = Right - 1;
		for(;;) {
			while(A[j]>=Pivot&&i<j) j--;
			while(A[i]<=Pivot&&i<j) i++;
			if(i<j)
				Swap(&A[i],&A[j]);
			else break;
		}
		Swap(&A[i],&A[Right]);//基准位置交换
		Quicksort(A, Left, i-1);
		Quicksort(A, i+1, Right);
	} else {
		//只剩下cutoff元素,传入对应位置的地址即可
		Insertion_Sort(A, Left, Right-Left+1);//因为Java中不支持对数组地址的引用操作,而传入的可以只是数组的部分数据。c可以直接对数组进行+Left操作
	}
}

//符合标准接口要求主接口
void Quick_Sort(ElementType A[], int N) {
	Quicksort(A, 0, N-1);
}

最好:每次都是正好中分
最坏:每次取到的元素都是数组中最值

  • 时间复杂度
    平均复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

  • 空间复杂度
    平均空间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n)

表排序

间接排序:只移动指针而不移动元素

环的思想
物理排序:取出一个元素至temp,空位作为华容道。
table[i]==i时环结束

1

最好:初始即有序
最坏:有N/2个环,每个环只有两个元素,每个环都要操作多次

T = O ( m N ) T=O(mN) T=O(mN)

桶排序

建立桶(相同的值放入对应的桶中)

9

时间复杂度: T ( N , M ) = O ( M + N ) T(N,M) = O(M+N) T(N,M)=O(M+N)
t7EJK0.md.jpg

基数排序

处理整数基数/多关键字排序

当桶排序中N<<M时,次位优先,比如数字还可以按照十位数、百位数来放入不同的桶中t7A80O.jpg

时间复杂度: T = O ( P ( N + B ) ) T=O(P(N+B)) T=O(P(N+B)),需要收集数

排序算法比较

排序方法平均时间复杂度最坏情况时间复杂度额外空间复杂度稳定性
简单选择排序* O ( N 2 ) O(N^2) O(N2) O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1)N
冒泡排序* O ( N 2 ) O(N^2) O(N2) O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1)Y
插入排序* O ( N 2 ) O(N^2) O(N2) O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1)Y
希尔排序 O ( N d ) O(N^d) O(Nd) O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1)N
堆排序 O ( N l o g N ) O(NlogN) O(NlogN) O ( N l o g N ) O(NlogN) O(NlogN) O ( 1 ) O(1) O(1)N
快速排序 O ( N l o g N ) O(NlogN) O(NlogN) O ( N 2 ) O(N^2) O(N2) O ( l o g N ) O(logN) O(logN)N
归并排序 O ( N l o g N ) O(NlogN) O(NlogN) O ( N l o g N ) O(NlogN) O(NlogN) O ( N ) O(N) O(N)Y
基数排序 O ( P ( N + B ) ) O(P(N+B)) O(P(N+B)) O ( P ( N + B ) ) O(P(N+B)) O(P(N+B)) O ( N + B ) O(N+B) O(N+B)Y

归并排序稳定快速但是需要空间N

注:*标表示简单排序

练习题

  1. 关键字序列T=(21,25,49,25*,16,08),写出快速排序算法的一趟实现过程
    解答:|08|16|21|25*|49|25
21| |25|49|25*|16|08
21|08|25|49|25*|16|
21|08| |49|25*|16|25
21|08|16|49|25*| |25
21|08|16| |25*|49|25
 |08|16|21|25*|49|25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值