排序算法全景指南:从冒泡到快排,一次搞定(第二期)

目录

3.2.2 归并排序

3.2.2.1 递归方法

3.2.2.2 非递归方法

3.2.3 堆排序(需要学过堆和二叉树)

3.3 ​​第三梯队:线性时间排序(有特殊使用条件)​

3.3.1 计数排序

3.4 特殊的排序:希尔排序(约为O(N^1.3))

4 排序算法分类

5 排序算法复杂度及稳定性分析


3.2.2 归并排序

3.2.2.1 递归方法

基本思想:归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide and Conquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,再使子序列段间有序。若将两个有序表合并成⼀个有序表,称为二路归并。
 

代码实现:(tmp为开辟的辅助数组,暂存每次递归排序后的有序数组,最后再拷贝回去)

void _MergeSort(int* a, int* tmp, int left, int right) {
	if (left >= right) return;
	int mid = left + (right - left) / 2;
	_MergeSort( a, tmp,  left, mid);
	_MergeSort(a,  tmp, mid+1, right);
	int begin1 = left;
	int begin2 = mid + 1;
	int i = left;
	while (begin1<=mid&&begin2<=right) {
		if (a[begin1] <= a[begin2]) {//保证稳定,要用<=
			tmp[i++] = a[begin1++];
		}
		else {
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= mid) {
		tmp[i++] = a[begin1++];

	}
	while (begin2 <= right) {
		tmp[i++] = a[begin2++];
	}
	memcpy(a+left, tmp+left, (right-left+1)* sizeof(int));
}

void MergeSort(int* a, int n) {
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL) {
		perror("MergeSort:malloc fail");
		exit(1);
	}
	_MergeSort(a, tmp, 0, n - 1);
	free(tmp);
	tmp = NULL;
}

时间复杂度: O(NlogN)

空间复杂度: O(N)

稳定性:稳定  

3.2.2.2 非递归方法

思路:利用循环来实现递归的分治思想

图示:

代码实现:

void MergeSortNonR(int* a, int n)//非递归归并排序
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL) {
		perror("MergeSortNonR:malloc fail");
		exit(1);
	}
	int gap = 1;
	while (gap < n) {
		for (int i = 0; i < n; i += 2*gap) {
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			//第二组越界,不用归并
			if (begin2 >= n) {
				break;
			}
			//只有end2越界,修正end2的值
			if (end2 >= n) {
				end2 = n - 1;
			}
			int j = i;
			while (begin1 <= end1 && begin2 <= end2) {
				if (a[begin1] <= a[begin2]) {//保证稳定,要用<=
					tmp[j++] = a[begin1++];
				}
				else {
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1) {
				tmp[j++] = a[begin1++];

			}
			while (begin2 <= end2) {
				tmp[j++] = a[begin2++];
			}
			memcpy(a + i, tmp + i, (end2-i+1)* sizeof(int));//不能用2*gap表示元素个数,因为可能越界。
		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

时间复杂度: O(NlogN)

空间复杂度: O(N)

稳定性:稳定  

3.2.3 堆排序(需要学过堆和二叉树)

思路:堆排序(Heapsort)是指利⽤堆积树(堆)这种数据结构所设计的⼀种排序算法,排升序要建⼤堆,排降序建小堆。

堆的概念:堆是一种特殊的完全二叉树,其中每个节点的值都大于等于(或小于等于)其子节点的值,使得最大(或最小)元素始终位于堆顶。它主要分为​​最大堆​​和​​最小堆​​两种类型,常通过数组实现,高效应用于优先队列、堆排序和Top-K问题等场景。

算法实现:先建堆,再排序

向下调整算法图示:

代码实现:

void AdjustDwon(int* a, int n, int root) {//向下调整
	int parent = root;
	while (parent*2+1<=n) {
		int bigchild = 2 * parent + 1;
		int smallchild = bigchild + 1;
		if (smallchild <= n && a[bigchild] < a[smallchild]) {
			bigchild = smallchild;
		}
		if (a[bigchild] > a[parent]) {
			Swap(&a[bigchild], &a[parent]);
		}
		else break;
		parent = bigchild;
	}
}

void HeapSort(int* a, int n) {
	for (int i = (n - 2) / 2; i >= 0; i--) {//建堆
		AdjustDwon(a, n-1, i);
	}
	for (int i = n-1; i >0; i--) {//排序
		Swap(&a[0], &a[i]);
		AdjustDwon(a,i-1, 0);
	}
}

时间复杂度: O(NlogN)

空间复杂度: O(1)

稳定性:不稳定 

3.3 ​​第三梯队:线性时间排序(有特殊使用条件)​

3.3.1 计数排序

思路:计数排序⼜称为鸽巢原理,是对哈希直接定址法的变形应⽤。 操作步骤:

1)统计相同元素出现次数

2)根据统计的结果将序列回收到原来的序列中

代码实现:

void CountSort(int* a, int n) {
	int max = a[0], min = a[0];
	for (int i = 1; i < n; i++) {
		if (a[i] > max) {
			max = a[i];
		}
		if (a[i] < min) {
			min = a[i];
		}
	}
	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	for (int i = 0; i < n; i++) {
		count[a[i] - min]++;
	}
	int j = 0;
	for (int i = 0; i < range; i++) {
		while (count[i]--) {
			a[j++] =min+i;
		}
	}
	free(count);
	count = NULL;
}

计数排序的特性:

计数排序在数据范围集中时,效率很⾼,但是适⽤范围及场景有限(只适合整数排序)。

时间复杂度: O(N + range)

空间复杂度: O(range)

稳定性:稳定

3.4 特殊的排序:希尔排序(约为O(N^1.3))

思路:希尔排序法⼜称缩⼩增量法。希尔排序法的基本思想是:先选定⼀个整数(通常是gap = n/3+1),把待排序⽂件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进⾏排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插⼊排序。

它是在直接插⼊排序算法的基础上进⾏改进⽽来的,综合来说它的效率肯定是要⾼于直接插⼊排序算法的。

代码实现:

void ShellSort(int* a, int n) {
	int gap = n;
	while(gap>1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++) {
			int end = i;
			int tmp = a[end + gap];
			while (end>=0) {
				if (a[end] > tmp) {
					a[end + gap] = a[end];
					end -= gap;
				}
				else  break;
			}
			a[end+gap] = tmp;
		}
	}
}

希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化。

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算。

时间复杂度: 可以认为是O(N^1.3)

空间复杂度: O(1)

稳定性:不稳定

4 排序算法分类

内排序
​​定义​​:指待排序的所有记录(数据)可以​​完全存放在内存中​​进行的排序过程。
​​适用场景​​:数据量相对较小,内存足以容纳所有数据。
​​关注点​​:主要关注​​时间和空间的复杂度​​,即如何减少数据之间的​​比较次数​​和​​移动次数​​。因为内存的访问速度非常快,算法效率主要体现在CPU的计算速度上。
​​常见算法​​:
快速排序
堆排序
归并排序(内存中的版本)
冒泡排序、插入排序等(适用于小规模或部分有序数据)
​​特点​​:
​​速度快​​:所有操作都在高速内存中进行。
​​数据量受限​​:受限于可用内存的大小。

外排序
​​定义​​:当待排序的记录数量非常庞大,无法一次性全部加载到内存中,必须借助​​外部存储器(如硬盘)​​ 进行分批处理,然后再合并的排序过程。
​​适用场景​​:处理海量数据,例如大型数据库的排序、搜索引擎对索引的排序等。
​​关注点​​:主要关注​​减少磁盘I/O(读写)的次数​​。因为磁盘读写速度比内存慢几个数量级,磁盘I/O成为性能的主要瓶颈。算法的目标是尽可能地减少读写磁盘的次数。
​​核心思想​​:采用 ​​“排序-归并”​​ 的策略。
​​排序阶段​​:将大文件分割成若干个能装入内存的小片段,每次读入一个片段到内存,用高效的​​内排序算法​​(如快速排序)对其进行排序,然后将这个​​有序的小片段(称为“归并段”或“顺串”)​​ 写回磁盘。重复这个过程,直到整个大文件被切分成多个有序的归并段。
​​归并阶段​​:将上一步产生的多个有序归并段合并成一个完整的有序文件。这个过程通常使用 ​​k路归并​​ 算法,每次从k个归并段中取出最小的元素,放入输出缓冲区。这个阶段也需要精心设计,以平衡内存使用和I/O次数。
​​特点​​:
​​能处理海量数据​​:不受内存大小限制。
​​速度慢​​:性能瓶颈在于磁盘I/O速度。
​​算法复杂​​:需要综合考虑内存管理、磁盘I/O和归并策略。

5 排序算法复杂度及稳定性分析

排序⽅法平均情况最好情况最坏情况辅助空间稳定性
冒泡排序O(n2)O(n)O(n2)O(1)稳定
直接选择排序O(n2)O(n2)O(n2)O(1)不稳定
直接插⼊排序O(n2)O(n)O(n2)O(1)稳定
希尔排序O(nlog n) ~ O(n2)O(n1.3)O(n2)O(1)不稳定
堆排序O(nlog n)O(nlog n)O(nlog n)O(1)不稳定
归并排序O(nlog n)O(nlog n)O(nlog n)O(n)稳定
快速排序O(nlog n)O(nlog n)O(n2)O(log n) ~ O(n)不稳定
计数排序O(N + range)O(N + range)O(N + range)O(range)稳定

那么第二期的内容就到这里了,觉得有收获的同学们可以给个点赞、关注、收藏哦,谢谢大家。

评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值