基础排序算法:冒泡、选择、插入、希尔

冒泡排序

排序过程:

反复遍历处理一个未排序的数组。将元素与下一个元素比较,其中较大的元素交换到后面。这样最终就可以把最大的元素排在最后。在下一次遍历时忽略最后一位,再次将剩余的最大元素放在剩余元素的最后一位。

对于下面这个含有十个随机数的数组:

18 37 82 97 4 35 83 42 11 52

首次处理:(比较n-1次)

比较18和37,37较大不用交换。比较37和82,82较大不用交换。比较82和97,97较大不用交换。比较97和4,97较大与4交换。比较97和35,97较大与35交换。比较97和83,97较大与83交换。比较97和42,97较大与42交换。比较97和11,97较大与11交换。比较97和52,97较大与52交换。

18 37 82 4 35 83 42 11 52 ||| 97

第二次处理:(比较n-2次)

......

移动了82和83

18 37 4 35 82 42 11 52 ||| 83 97

......

经过9轮比较后数组变为了有序数组:

4 11 18 35 37 42 52 82 83 97

性能特点:

空间复杂度:

时间复杂度:

最好时间复杂度: 进入第二层循环后循环一次没有交换就退出(下面优化后的代码)

平均&最坏时间复杂度:

稳定性:稳定

稳定性------在原始数据序列中,相同的元素在经过排序后,他们的前后顺序并没有发生改变,则排序时稳定的排序。比如对于键值对<int, string>,如果序列中的string是有顺序的,则稳定的排序在排序后可以保持string的顺序。

冒泡排序原理上非常简单直接,空间占用小,但是时间复杂度较大。

冒泡排序是所有排序算法中效率最低的,原因是数据交换次数太多。

代码用到两层for循环:第一层for (int i ......表示处理不同次数,第二层for (int j......表示依序对于数组中元素处理。

cpp 复制代码
#include <iostream>
#include <random>
#include <ctime>

void BubbleSort(int arr[], int size)
{
	for (int i = 0; i < size - 1; i++)
	{
		for (int j = 0; j < size - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j + 1];
				arr[j + 1] = arr[j];
				arr[j] = temp;
			}
		}
	}
}

//用系统时间种随机数种子,使用随机数生成100以内随机数10个,之后排序数组
int main()
{
	int arr[10];
	srand(time(NULL));
	for (int i = 0; i < 10; i++)
	{
		arr[i] = rand() % 100;
		std::cout << arr[i] << " ";
	}
	BubbleSort(arr, 10);
	//换行输出排序后数组
	std::cout << std::endl << "Array Sorted: " << std::endl;
	for (int i = 0; i < 10; i++)
	{
		std::cout << arr[i] << " ";
	}
}

输出:

5 12 24 90 80 53 28 70 43 30

Array Sorted:

5 12 24 28 30 43 53 70 80 90

优化:

cpp 复制代码
void BubbleSort(int arr[], int size)
{
	for (int i = 0; i < size - 1; i++)
	{
		//优化代码:如果一次处理没有做过任何数据交换,则数组已经有序
		bool flag = true;		//flag表示没有做过交换
		for (int j = 0; j < size - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j + 1];
				arr[j + 1] = arr[j];
				arr[j] = temp;
				flag = false;	//如果进入if语句,把flag置为false
			}
		}
		if (flag)		//如果没有做过交换,跳出循环
			return;
	}
}

选择排序

排序过程:

反复遍历序列,每次遍历选出范围内的最小值,将这个最小值与范围内第一个元素交换。每次遍历范围去掉上一次放置了最小值的位置。

同样举例这个数组:

18 37 82 97 4 35 83 42 11 52

第一次遍历:

4 37 82 97 18 35 83 42 11 52

我们在遍历时设计一个min记录当前最小值的位置,在遍历完后确认最小值位于min = 4的位置,将这个位置上的元素与第一个元素交换。

第二次遍历:

4 ||| 11 82 97 18 35 83 42 37 52

排除掉第一个元素,在剩余九个元素中找到最小值位于min = 8的位置,将这个位置与遍历范围内的第一个元素交换。

第三次遍历:

4 11 ||| 18 97 82 35 83 42 37 52

min = 4

......

在完成了9次遍历后,我们得到了最终结果:

4 11 18 35 37 42 52 82 ||| 83 97

性能特点:

对比冒泡排序的过程可以发现,选择排序大量减少了交换次数,所以效率比冒泡排序高一些。但是没有办法像冒泡排序那样跳过已经有序的数组。

平均&最好&最坏时间复杂度:

空间复杂度:

稳定性:不稳定

稳定性举例:5① 5② 3 => 3 5② 5①,在排序后相同的元素原本的顺序被改变了。

有上面的思路,容易得到代码:

cpp 复制代码
void ChoiceSort(int arr[], int size)
{
	
	//遍历 size - 1 次
	for (int i = 0; i < size - 1; i++)
	{
		int min = i;
		//每次从第 i + 1 个开始遍历,寻找最小值
		for (int j = i + 1; j < size; j++)
		{
			if (arr[j] < arr[min])
				min = j;
		}
		//找到最小值位置后,如果最小值不在遍历范围首位,与遍历的第一个元素交换
		if (min != i)
		{
			int temp = arr[min];
			arr[min] = arr[i];
			arr[i] = temp;
		}
	}
}

//用系统时间种随机数种子,使用随机数生成100以内随机数10个,之后排序数组
int main()
{
	int arr[10];
	srand(time(NULL));
	for (int i = 0; i < 10; i++)
	{
		arr[i] = rand() % 100;
		std::cout << arr[i] << " ";
	}
	ChoiceSort(arr, 10);
	//换行输出排序后数组
	std::cout << std::endl << "Array Sorted: " << std::endl;
	for (int i = 0; i < 10; i++)
	{
		std::cout << arr[i] << " ";
	}
}

插入排序

如果数据趋于有序,那么插入排序是效率最高的排序算法。

在基础排序算法中,插入排序效率>冒泡排序&选择排序。插入排序不仅仅没有交换,比较的次数也很少。

排序过程:

用同样的数组演示:

18 37 82 97 4 35 83 42 11 52

插入排序每次将前i个数组成的序列默认为有序,从第i个数开始向前遍历到第一个数,与第i+1个数比较,将第i+1个数插入到首个小于等于它的数的后方。

第一次遍历:

18 ||| 37 82 97 4 35 83 42 11 52

将18这个序列默认为有序,将37与之对比,18 <= 37,将37插入到18后方。

第二次遍历:

18 37 ||| 82 97 4 35 83 42 11 52

将18 37这个序列默认为有序,从第二个元素向前遍历与82对比,37 <= 82,将82插入到37后方。

第三次遍历:

18 37 82 ||| 97 4 35 83 42 11 52

将18 37 82这个序列默认为有序,从第三个元素向前遍历与97对比,82 <= 97,将97插入到82后方。

第四次遍历:

18 37 82 97 ||| 4 35 83 42 11 52

从第四个元素向前遍历与4对比,没有数字小于等于4,将4插入到第一位前面。

4 18 37 82 97 ||| 35 83 42 11 52

第五次遍历:

4 18 37 82 97 ||| 35 83 42 11 52

从第五个元素向前遍历与35对比,18<=35,将35插入到18前面。

4 18 35 37 82 97 ||| 83 42 11 52

......

最后我们得到有序序列:

4 11 18 35 37 42 52 82 83 97 |||

性能特点:

空间复杂度:

时间复杂度:

最好时间复杂度: 已经有序的循环,内层循环会直接跳出

平均时间复杂度&最坏时间复杂度:

稳定性:稳定

参照上面的过程,容易得出代码:

cpp 复制代码
void InsertSort(int arr[], int size)
{
	for (int i = 1; i < size; i++)
	{
		int temp = arr[i];
		int j = i - 1;
		//寻找前序元素中小于等于temp的
		for (; j >= 0; j--)
		{
			//如果元素小于等于temp,则跳出循环
			if (arr[j] <= temp)
			{
				break;
			}
			//如果元素大于temp,则覆盖后面一位,相当于后移
			arr[j + 1] = arr[j];
		}
		//跳出循环后,将temp放置在小于等于它的元素的下一位
		//如果循环完成,此时的j = -1,temp会放在第一位
		arr[j + 1] = temp;
	}
}

//用系统时间种随机数种子,使用随机数生成100以内随机数10个,之后排序数组
int main()
{
	int arr[10];
	srand(time(NULL));
	for (int i = 0; i < 10; i++)
	{
		arr[i] = rand() % 100;
		std::cout << arr[i] << " ";
	}
	InsertSort(arr, 10);
	//换行输出排序后数组
	std::cout << std::endl << "Array Sorted: " << std::endl;
	for (int i = 0; i < 10; i++)
	{
		std::cout << arr[i] << " ";
	}
}

希尔排序

对数据进行分组插入排序,使得整个数组逐渐区域有序。

排序过程:

用同样的数组演示:

18 37 82 97 4 35 83 42 11 52

希尔排序需要对数组进行分组,我们可以间隔几个元素分组为一组,分组间隔越来越小,直到间隔为1,分组只剩下一个。

对于这个10个元素的数组,分组间隔每次为上一次的1/2,设 gap = size / 2 = 10 / 2 = 5

将数据分组为:

18 35

37 83

82 42

97 11

4 52

对每一组进行插入排序:

18 35

37 83

42 82

11 97

4 52

得到:

18 37 42 11 4 35 83 82 97 52

此时在局部分组中元素有序,在整个数组中元素趋于有序。

再将分组间隔变为原来的1/2,gap = gap / 2 = 2,数据分组为:

18 42 4 83 97

37 11 35 82 52

对每一组进行插入排序:

4 18 42 83 97

11 35 37 52 82

得到:

4 11 18 35 42 37 83 52 97 82

再令gap = gap / 2 = 1,数据分组为一组,对整个数组进行插入排序:

4 11 18 35 42 37 83 52 97 82

得到:

4 11 18 35 37 42 52 82 83 97

性能特点:

空间复杂度:

时间复杂度:

虽然在外层增加了分组循环,但是分组以原来的 减小,循环层数并不是原来的线性关系,而是对数关系。

对于规模的数据,循环只进行 次。

对于n = 10000000 规模的数据,循环只进行了 次。

增加的时间复杂度相比其他for循环增加的时间复杂度可以忽略。

平均时间复杂度: 由于希尔排序可以使序列整体更快趋于有序,平均时间复杂度相对更小。详细推导可以另行搜索。

最好时间复杂度:

最坏时间复杂度:

另外,上面仅仅演示一种分组方式,还有其他的分组方式可以更好优化时间复杂度。

稳定性:不稳定 可能将值相同的元素分入不同的组,造成顺序变化

希尔排序是对插入排序的改进,从插入排序的代码优化得到希尔排序的代码:

可以从插入排序的代码对照修改,更有利于理解希尔排序的代码。

cpp 复制代码
void ShellSort(int arr[], int size)
{
	//外层嵌套分组循环,以越来越小的间隔将元素分组
	for (int gap = size / 2; gap > 0; gap = gap / 2)
	{
		//i从第一个分组的第二个元素开始遍历后面的元素,就可以对所有的分组进行排序
		for (int i = gap; i < size; i++)
		{
			int temp = arr[i];
			int j = i - gap;	//同个分组中的上一个元素的位置
			//寻找前序元素中小于等于temp的
            //只需要将原来的对j的+1-1操作改为+gap-gap
			for (; j >= 0; j = j - gap)	//对同个分组中的前序元素依次遍历
			{
				//如果元素小于等于temp,则跳出循环
				if (arr[j] <= temp)
				{
					break;
				}
				arr[j + gap] = arr[j];	//将元素在同个分组中前移
			}
			//循环结束后,将temp放在同组中标记元素的后一位
			arr[j + gap] = temp;
		}
	}
}

//用系统时间种随机数种子,使用随机数生成100以内随机数10个,之后排序数组
int main()
{
	int arr[10];
	srand(time(NULL));
	for (int i = 0; i < 10; i++)
	{
		arr[i] = rand() % 100;
		std::cout << arr[i] << " ";
	}
	ShellSort(arr, 10);
	//换行输出排序后数组
	std::cout << std::endl << "Array Sorted: " << std::endl;
	for (int i = 0; i < 10; i++)
	{
		std::cout << arr[i] << " ";
	}
}

四种基础排序算法性能对比

性能对比:

|------|------------------------------------|------------------------------------------|--------------------------------------|--------------------------------------|-----|
| | 空间复杂度 | 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 稳定性 |
| 冒泡排序 | | | | | 稳定 |
| 选择排序 | | | | | 不稳定 |
| 插入排序 | | | | | 稳定 |
| 希尔排序 | | | | | 不稳定 |

其中插入排序效率最好,尤其在数据趋于有序的情况下,插入排序效率最高。而希尔排序是对插入排序的优化,在随机的序列中表现往往更好。

冒泡排序由于发生过多的交换,效率最差。选择排序因为减少了数据交换的次数,效率次之。

我们完成了四种排序算法的函数的编写,接下来在main函数中设计代码比较四种排序方法消耗的时间。可以自行修改COUNT=100000或其他值比较。

代码:

cpp 复制代码
#include <iostream>
#include <random>
#include <ctime>

void BubbleSort(int arr[], int size)
{
	for (int i = 0; i < size - 1; i++)
	{
		//优化代码:如果一次处理没有做过任何数据交换,则数组已经有序
		bool flag = true;		//flag表示没有做过交换
		for (int j = 0; j < size - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j + 1];
				arr[j + 1] = arr[j];
				arr[j] = temp;
				flag = false;	//如果进入if语句,把flag置为false
			}
		}
		if (flag)		//如果没有做过交换,跳出循环
			return;
	}
}

void ChoiceSort(int arr[], int size)
{
	
	//遍历 size - 1 次
	for (int i = 0; i < size - 1; i++)
	{
		int min = i;
		//每次从第 i + 1 个开始遍历,寻找最小值
		for (int j = i + 1; j < size; j++)
		{
			if (arr[j] < arr[min])
				min = j;
		}
		//找到最小值位置后,如果最小值不在遍历范围首位,与遍历的第一个元素交换
		if (min != i)
		{
			int temp = arr[min];
			arr[min] = arr[i];
			arr[i] = temp;
		}
	}
}

void InsertSort(int arr[], int size)
{
	for (int i = 1; i < size; i++)
	{
		int temp = arr[i];
		int j = i - 1;
		//寻找前序元素中小于等于temp的
		for (; j >= 0; j--)
		{
			//如果元素小于等于temp,则跳出循环
			if (arr[j] <= temp)
			{
				break;
			}
			//如果元素大于temp,则覆盖后面一位,相当于后移
			arr[j + 1] = arr[j];
		}
		//跳出循环后,将temp放置在小于等于它的元素的下一位
		//如果循环完成,此时的j = -1,temp会放在第一位
		arr[j + 1] = temp;
	}
}

void ShellSort(int arr[], int size)
{
	//外层嵌套分组循环,以越来越小的间隔将元素分组
	for (int gap = size / 2; gap > 0; gap = gap / 2)
	{
		//i从第一个分组的第二个元素开始遍历后面的元素,就可以对所有的分组进行排序
		for (int i = gap; i < size; i++)
		{
			int temp = arr[i];
			int j = i - gap;	//同个分组中的上一个元素的位置
			//寻找前序元素中小于等于temp的
			for (; j >= 0; j = j - gap)	//对同个分组中的前序元素依次遍历
			{
				//如果元素小于等于temp,则跳出循环
				if (arr[j] <= temp)
				{
					break;
				}
				arr[j + gap] = arr[j];	//将元素在同个分组中前移
			}
			//循环结束后,将temp放在同组中标记元素的后一位
			arr[j + gap] = temp;
		}
	}
}

int main()
{
	const int COUNT = 10000;
	//将数组定义在堆上
	int* arr_b = new int[COUNT];
	int* arr_c = new int[COUNT];
	int* arr_i = new int[COUNT];
	int* arr_s = new int[COUNT];

	srand(time(NULL));
	//初始化,对每个数组赋相同的随机数
	for (int i = 0; i < COUNT; i++)
	{
		int val = rand() % COUNT;
		arr_b[i] = val;
		arr_c[i] = val;
		arr_i[i] = val;
		arr_s[i] = val;
	}

	//clock_t变量记录系统时间刻度
	clock_t begin, end;

	//冒泡排序消耗时间
	begin = clock();
	BubbleSort(arr_b, COUNT);
	end = clock();
	//计算clocks间隔,并将单位转换成秒
	std::cout << "BubbleSort Spend Time: " << (end - begin) * 1.0 / CLOCKS_PER_SEC << "s" << std::endl;

	//选择排序消耗时间
	begin = clock();
	ChoiceSort(arr_c, COUNT);
	end = clock();
	std::cout << "ChoiceSort Spend Time: " << (end - begin) * 1.0 / CLOCKS_PER_SEC << "s" << std::endl;
	
	//插入排序消耗时间
	begin = clock();
	InsertSort(arr_i, COUNT);
	end = clock();
	std::cout << "InsertSort Spend Time: " << (end - begin) * 1.0 / CLOCKS_PER_SEC << "s" << std::endl;
	
	//希尔排序消耗时间
	begin = clock();
	ShellSort(arr_s, COUNT);
	end = clock();
	std::cout << "ShellSort Spend Time: " << (end - begin) * 1.0 / CLOCKS_PER_SEC << "s" << std::endl;
}

输出:

BubbleSort Spend Time: 0.226s

ChoiceSort Spend Time: 0.101s

InsertSort Spend Time: 0.053s

ShellSort Spend Time: 0.002s

COUNT = 100000时的输出:

BubbleSort Spend Time: 23.41s

ChoiceSort Spend Time: 8.171s

InsertSort Spend Time: 4.862s

ShellSort Spend Time: 0.025s

相关推荐
ths5122 小时前
测试开发python中正则表达式使用总结(二)
开发语言·python·算法
不爱吃炸鸡柳2 小时前
5道经典贪心算法题详解:从入门到进阶
开发语言·数据结构·c++·算法·贪心算法
枫叶林FYL2 小时前
【自然语言处理 NLP】8.3 长文本推理评估与针在大海堆任务
人工智能·算法
智者知已应修善业2 小时前
【51单片机1,左边4个LED灯先闪烁2次后,右边4个LED灯再闪烁2次:2,接着所用灯一起闪烁3次,接着重复步骤1,如此循环。】2023-5-19
c++·经验分享·笔记·算法·51单片机
米啦啦.2 小时前
红黑树,,
数据结构·红黑树
xiaoye-duck2 小时前
《算法题讲解指南:优选算法-队列+宽搜》--70.N叉树的层序遍历,71.二叉树的锯齿形层序遍历,72.二叉树的最大宽度,73.在每个树行中找最大值
数据结构·c++·算法·队列
汀、人工智能2 小时前
[特殊字符] 第98课:数据流中位数
数据结构·算法·数据库架构··数据流·数据流中位数
Eloudy2 小时前
不同特征值的特征向量互相正交的矩阵
人工智能·算法·机器学习