常用排序算法分析

本文介绍了分析排序算法的三个维度:执行效率、内存消耗和稳定性。并对冒泡排序、插入排序、选择排序、归并排序和快速排序进行分析,包括各算法的实现过程、时间复杂度、空间复杂度以及是否为稳定排序。

如果分析一个排序算法

1.执行效率

执行效率分析一般从以下几个情况分析

(1)最好、最坏、平均情况时间复杂度

对于排序数据的有序度不同,排序算法在的性能表现会有影响,所以需要分析不同情况下的时间复杂度

(2)时间复杂度的系数、常数 、低阶

时间复杂度反应的是代码执行时间随数据规模增长的变化趋势,在实际软件开发中,排序的数据规模可能很小,在对比同一阶时间复杂度的排序算法性能的时候,就需要把系数、常数、低阶也一起考虑进去。

(3)比较次数和交换(或移动)次数

基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。所以,如果我们在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去。

2.内存消耗

算法的内存消耗可以通过空间复杂度来衡量,空间复杂度O(1)的排序算法称为“原地排序”。

3.稳定性

在一组数据排序后,相同数据的前后顺序没有改变,那么就把这种排序称为“稳定的排序算法”,反之就叫做“不稳定的排序算法”。因为排序可能会有多种条件,不稳定排序可能会打乱之前的排序。

冒泡排序

在一组数据中,比较相邻的两个数,看是否满足大小关系,如果不满足,则交换,重复n次,直到最后到已排序区间。
如:一组数据4,2,3,1。每相邻的两个数比较,第一次冒泡后结果为2,3,1,4,一直到最后冒泡结束,过程如下图。
在这里插入图片描述

最后写成代码如下:

/// <summary>
/// 冒泡排序
/// </summary>
/// <param name="a">需要排序的数组</param>
/// <param name="n">数组长度</param>
void bubbleSort(int a[], int n) {
	if (n <= 0) return;

	for (int i = n - 1; i >= 0; --i) {
		
		for (int j = 0; j < i; ++j) {
			if (a[j] > a[j + 1]) {
				int temp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = temp;
			}
		}
	}

}

冒泡排序分析

1.执行效率
最好时间复杂度为O(n),最坏时间复杂度为O(n2),平均时间复杂度为O(n2)。
2.内存消耗。
冒泡排序只涉及相邻数据的交换操作,生成常量的内存空间,空间复杂度为O(1),所以是“原地排序”。
3.稳定性。
在相邻两个数据交换中,只有数据不同且满足交换条件时才会交换,数据相同不会交换,保持原有顺序,所以是稳定的排序算法。

插入排序

一组数据中,分为已排序区间和未排序区间,将未排序区间中第一个数据从已排序区间的后往前比较,如果不满足大小关系,则将数据向后移,直到满足或者达到首项,将数据插入,此时已排序区间+1,未排序区间-1。重复操作直到未排序区间为空。过程如下图。

在这里插入图片描述

最后写成代码如下:

/// <summary>
/// 插入排序
/// </summary>
/// <param name="a">需要排序的数组</param>
/// <param name="n">数组长度</param>
void insertionSort(int a[], int n)
{
	if (n <= 0) return;
	for (int i = 0; i < n - 1; ++i) {	//0 到 i 为已排序好的数据
		int j;
		int insert = a[i + 1];
		for (j = i + 1; j > 0; --j) {
			if (insert < a[j - 1]) {
				a[j] = a[j - 1];
			}
			else {
				break;
			}
		}
		a[j] = insert;
	}
}

插入排序分析

1.执行效率
最好时间复杂度为O(n),最坏时间复杂度为O(n2),平均时间复杂度为O(n2)。
2.内存消耗。
插入排序只涉及数据的移动和插入操作,生成常量的空间,空间复杂度为O(1),所以是“原地排序”。
3.稳定性。
数据的移动过程中,只有数据不同且满足交换条件时才会交换,数据相同不会移动,保持原有顺序,所以是稳定的排序算法。

选择排序

选择排序有点类似插入排序,也是分为已排序区间和未排序区间,每一次遍历未排序区间,选择相应条件的数据和已排序区间末尾的数据交换。重复操作直到未排序区间为空。过程如下图。

在这里插入图片描述

最后写成代码如下:

/// <summary>
/// 选择排序
/// </summary>
/// <param name="a">需要排序的数组</param>
/// <param name="n">数组长度</param>
void selectionSort(int a[], int n) 
{
	int index;
	for (int i = 0; i < n - 1; ++i)
	{
		index = i;
		for (int j = i + 1; j < n; ++j)
		{
			if (a[index] > a[j])
			{
				index = j;
			}
		}

		int temp = a[index];
		a[index] = a[i];
		a[i] = temp;
	}
}

选择排序分析

1.执行效率
最好时间复杂度为O(n2),最坏时间复杂度为O(n2),平均时间复杂度为O(n2)。
2.内存消耗。
选择排序只涉及数据的交换操作,生成常量空间,空间复杂度为O(1),所以是“原地排序”。
3.稳定性。
数据在交换过程中可能打乱原有的顺序,所以不是稳定的排序算法。

归并排序

归并排序的核心就是将需要排序的数组分成两部分,然后将这两部分分别排序,最后将这两部分和成一个数组。在分成两个部分中,我们继续将数组分成两个部分,直到最后一个部分只有一个数据为止。过程如下图。
在这里插入图片描述
最后写成代码如下:

void Merge(int a[], int l, int m, int r) {

	int len = r - l + 1;	//临时数组长度
	int* temp = new int[len];
	int i = 0;
	int left = l;
	int right = m + 1;
	while (left <= m && right <= r) {
		temp[i++] = a[left] <= a[right] ? a[left++] : a[right++];
	}

	while (left <= m)
		temp[i++] = a[left++];
	while (right <= r)
		temp[i++] = a[right++];

	for (int j = 0; j < len; ++j) {
		a[l + j] = temp[j];
	}
	delete[] temp;
}

/// <summary>
/// 归并排序
/// </summary>
/// <param name="a">需要排序的数组</param>
/// <param name="left">左下标</param>
/// <param name="right">右下标</param>
void MergeSort(int a[], int left, int right) {
	if (left >= right) return;

	int mid = (right - left) / 2 + left;
	MergeSort(a, left, mid);
	MergeSort(a, mid + 1, right);
	Merge(a, left, mid, right);
}

归并排序分析

1.执行效率
我们假设对 n 个元素进行归并排序需要的时间是 T(n),那分解成两个子数组排序的时间都是 T(n/2)。我们知道,merge() 函数合并两个有序子数组的时间复杂度是 O(n)。所以,套用前面的公式,归并排序的时间复杂度的计算公式就是

T(1) = C;   n=1时,只需要常量级的执行时间,所以表示为C。
T(n) = 2*T(n/2) + n; n>1


T(n) = 2*T(n/2) + n
     = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
     = 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
     = 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
     ......
     = 2^k * T(n/2^k) + k * n
     ......

T(n) = 2kT(n/2k)+kn。当 T(n/2^k)=T(1) 时,也就是 n/2^k=1,我们得到 k=log2n 。我们将 k 值代入上面的公式,得到 T(n)=Cn+nlog2n 。如果我们用大 O 标记法来表示的话,T(n) 就等于 O(nlogn)。所以归并排序的时间复杂度是 O(nlogn)。

2.内存消耗。
归并排序需要创建临时数组用于存储数据,合并完成才会释放,所以空间复杂度为O(n),不是原地排序。
3.稳定性。
合并的条件为<=,所以在数据相等的情况下,不会交换顺序,所以是稳定的排序算法。

快速排序

快排和归并排序有点类似,他会选择一个点作为pivot(分区点),遍历数组将小于pivot的数据放在左边,大于pivot的数据放在右边,将分成两区间的数据重复以上操作,直到区间为1。

在这里插入图片描述

最后写成代码如下:

int Parition(int a[], int low, int high) {
	int pivot = a[high];
	int i = low;
	for (int j = low; j < high; ++j)
	{
		//j指向当前遍历元素,如果大于等于pivot,继续向前
		//如果小于当前元素,则和i指向的元素交换
		if (a[j] < pivot) {
			swap(a[j], a[i]);
			i++;
		}
	}
	swap(a[i], a[high]);
	return i;

}

/// <summary>
/// 快速排序
/// </summary>
/// <param name="a">需要排序的数组</param>
/// <param name="low">左下标</param>
/// <param name="high">右下标</param>
void QuickSort(int a[], int low, int high) {
	if (low < high)
	{
		int q = Parition(a, low, high);
		QuickSort(a, low, q - 1);
		QuickSort(a, q + 1, high);

	}
}

快速排序分析

1.执行效率
快速排序算法虽然最坏情况下的时间复杂度是 O(n2),但是平均情况下时间复杂度都是 O(nlogn)。
2.内存消耗。
快排只需要交换数据,不会产生多余的空间,所以空间复杂度为O(1),是原地排序
3.稳定性。
交换过程中可能打乱顺序,所以是不稳定排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值