一文解决 | 十大排序算法之冒泡排序、插入排序、希尔排序、快速排序、堆排序

前言

        本篇介绍十大经典排序算法中的五个最常用的算法,选择合适的排序算法取决于具体的应用场景和数据特点。若数据规模较小且简单,可以选择冒泡排序或插入排序;若数据规模较大且需要较高的性能,可以考虑快速排序、堆排序或希尔排序。

        根据时间复杂度区分:

        1、O(nlogn)    a、希尔排序 b、堆排序;     c、快速排序; d、归并排序

        2、O(n)           a、计数排序;  b、基数排序; c、桶排序

        3、O(n2)         a、冒泡排序;  b、选择排序; c、插入排序

        排序算法的优点主要包括以下几个方面:

        1、性能好:

        某些排序算法在平均情况下具有较好的性能表现,如快速排序和堆排序,它们的时间复杂度为O(nlogn),适用于大型数据集的排序。

        2、适应性强:

        有些排序算法对于部分有序或近乎有序的数据集具有较好的适应性,如插入排序,在这种情况下,它的效率较高。

        3、稳定性:

        稳定的排序算法能够保持相同元素的相对顺序不变,对于某些需要保持原有顺序的场景,这是一个重要的优点,如冒泡排序和插入排序。

        4、空间复杂度低:

        部分排序算法的空间复杂度较低,只需要常数级别的额外空间,如冒泡排序和插入排序,这在内存有限或需要节省内存空间的情况下是一个优点。

        5、易于实现:

       某些排序算法的实现相对简单,代码量较少,易于理解和实现,如冒泡排序和插入排序,这使得它们在一些特定情况下成为首选。

正文

        01-冒泡排序

        特点:冒泡排序是一种简单直观的排序算法,它会多次遍历要排序的数列,每次遍历都会比较相邻的元素,如果顺序错误就交换它们。

        优点:实现简单,易于理解和实现,适用于小型数据集。

        缺点:效率较低,对于大型数据集效率低下,时间复杂度为O(n2)。

        动图解释:

         代码实现:

#include <iostream>
using namespace std;

int main()
{
	// 利用冒泡排序实现升序排列
	int arr[9] = { 4,2,8,0,5,7,1,9,3 };
	int len = sizeof(arr) / sizeof(arr[0]);
	cout << "排序前数据:" << endl;
	for (int i = 0; i <= len;i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	// 开始冒泡排序
	//总共排序轮数为 元素个数-1

	for (int i = 0; i < len - 1;i++)
	{
		// 内层循环  次数 = 元素个数-当前轮数-1
		for (int j = 0; j < len - i -1 ;j++)
		{
			// 如果第一个数字比第二个数字大,则交换两个数字
			while (arr[j]>arr[j+1])  // 使用if或者while均可
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
	
	// 排序后结果
	cout << "排序后数据:" << endl;
	for (int i = 0; i <= len; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

	system("pause");
	return 0;

}
        02-插入排序     

        特点:插入排序通过构建有序序列,对未排序数据逐个进行插入操作,从而将序列扩大为有序序列。

        优点:对于小型数据集或部分有序的数据集,插入排序效率较高,且在实现上比较简单。

        缺点:对于大型数据集,效率较低,时间复杂度为O(n2),尤其是在数据集基本有序时效率降低。

         动图解释:

         代码实现与解释:

        下方函数中的参数列表中第一个参数这里用的是Array[],数组名加中括号,证明这里传入的是一个数组,也就是函数调用时,直接使用InsertSort(Array,len);   其实 Array[]等同于*Array,因为咱们定义指针是需要这个指针指向一段地址的,而数组名恰好就是该数组的首地址,因此,虽然定义函数时定义的是一个指针,但是调用函数传入时,仅需要将数组名传进去即可,那也就是说 int Array[] 还有一种写法 int *  Array   

#include <iostream>
using namespace std;

void InsertSort(int Array[], int len) {
	int i, j, temp;//第一层循环
	for (i = 1; i < len; i++) {   // 将各元素插入到已经排好的序列
		if (Array[i] < Array[i - 1]) {  // 若是当前的数值小于前面的
			temp = Array[i];      // 使用一个变量暂存该数据
			for (j = i - 1; j >= 0 && Array[j] > temp; --j) {
				Array[j + 1] = Array[j]; //所有大于temp的元素向后移位
			}
			Array[j + 1] = temp;
		}
	}
}

int main()
{

	int arr[9] = { 4,2,8,0,5,7,1,9,3 };
	int len = sizeof(arr) / sizeof(arr[0]);
	cout << "排序前数据:" << endl;
	for (int i = 0; i <= len;i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	
	InsertSort(arr, len);
	
	// 排序后结果
	cout << "排序后数据:" << endl;
	for (int i = 0; i <= len; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

	system("pause");
	return 0;

}

 

         03-希尔排序   

        特点:希尔排序是插入排序的改进版本,它将待排序的数组分割成若干个子序列,然后分别对子序列进行插入排序,最后再对整个数组进行一次插入排序。

        优点:相较于简单的插入排序,希尔排序在大型数据集上有较好的性能表现,时间复杂度介于O(nlogn)到O(n2)之间,取决于增量序列的选择。

        缺点:希尔排序的增量序列的选择对性能有较大影响,不同的增量序列可能导致不同的性能表现。

         具体代码实现与解释如下:

#include <iostream>
using namespace std;

void ShellSort(int Array[], int len)
{
	int d, i, j, temp;
	for (d = len / 2; d > 0; d = d / 2) {           // 将各元素插入到已经排好的序列
		for (i = d; i < len; i++)  {                //若是当前的数值小于前面的
		//这里还是应该加上条件语句的,因为不加的话,即使当前元素大于该子表上一个元素
		// 也会进行数据赋值给temp,会浪费时间
			if (Array[i] < Array[i - d]) {
				temp = Array[i];//这句代码用于检查该子表中前面还有没有其他数据需要处理
				for (j = i - d; j >= 0 && temp < Array[j]; j -= d) {
					// 这里让子表中的下一个元素等于当前元素,而不是直接安顺序后移
					// 这的j+d=6,也就是上面Array[i]的元素下标
					Array[j + d] = Array[j];
				}
				//上一步骤发现当前小于前面的元素就比如9在前,2在后,
				//此时在佛如循环里已经进行替换9在后面了,而2赋值在temp中
				//这里j+d=2也就是上面Array[i-d]的下标
				Array[j + d] = temp;
			}
		}
	}
}

int main()
{

	int arr[9] = { 4,2,8,0,5,7,1,9,3 };
	int len = sizeof(arr) / sizeof(arr[0]);
	cout << "排序前数据:" << endl;
	for (int i = 0; i <= len;i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	
	ShellSort(arr, len);
	
	// 排序后结果
	cout << "排序后数据:" << endl;
	for (int i = 0; i <= len; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

	system("pause");
	return 0;

}

        04-快速排序   

        特点:快速排序采用分治法,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的小,然后再按此方法对这两部分数据分别进行快速排序,最终实现整个序列有序。

        优点:在平均情况下,快速排序的性能非常好,时间复杂度为O(nlogn),且实现简单。

        缺点:最坏情况下的性能较差,时间复杂度为O(n2),且对于大型数据集,可能会出现栈溢出问题。

         动图解释:

        具体代码实现与解释如下: 

#include <iostream>
using namespace std;

// 用第一个元素将待排序序列划分成左右两个部分
int Partition(int Array[], int low, int high) {
	int pivot = Array[low];// 第一个元素作为基轴
	while (low < high) { //这里判断1ow是否小于high的循环条件
	//这里又进入一层循环,也就是如果high对应的值大于基轴值,则元素不用往前移,并且high自动--,位置前移
		while (low < high && Array[high] >= pivot) { --high; }
		//当high下标对应的元素比基轴值小的时候,跳出循环,将该元素移动到1ow下标对应的位置
		Array[low] = Array[high];
		// 当high下标对应的循环跳出之后,接着进行1ow下标对应的循环,
		//如果1ow对应的值小于基轴值,则元素不用往后移,并且1ow自动++,位置后移
		while (low < high && Array[low] <= pivot) { ++low; }
		//当1ow下标对应的元素比基轴值大的时候,跳出循环,将该元素移动到high下标对应的位置
		Array[high] = Array[low];
	}
	//当1ow<high的时候,证明所有元素全部比较完毕,将刚才做基轴的元素赋值给1ow对应元素即可
	Array[low] = pivot;
	//然后返回划分子表的下标位置
	return low;
}

// 这里就是调用上个函数进行递归运算
void Quicksort(int Array[], int low, int high) {
	if (low < high) {//这里就明显运用了递归的思想
		int pivotpos = Partition(Array, low, high);//划分
		//第一步执行完毕,返回一个pivotpos值,就是最终1ow和high共同停留的位置,
		//那么此时得到的一个子表就是0-pivotpos - 1范围的子表,对其再次进行Partition函数调用
		Quicksort(Array,low,pivotpos - 1);//划分左子表
		//此时得到的另一个子表就是pivotpos+1-0范围的子表,对其再次进行Partition函数调用
		Quicksort(Array,pivotpos + 1,high);//划分右子表
	}
}

int main()
{

	int arr[9] = { 4,2,8,0,5,7,1,9,3 };
	int low = 0;
	int len = sizeof(arr) / sizeof(arr[0]) - 1 ; 
	cout << "排序前数据:" << endl;
	for (int i = 0; i <= len;i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	
	Quicksort(arr, low, len);
	
	// 排序后结果
	cout << "排序后数据:" << endl;
	for (int i = 0; i <= len; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

	system("pause");
	return 0;

}

        05-堆排序

        特点:堆排序利用堆这种数据结构,通过建立最大堆或最小堆来进行排序。

        优点:堆排序在最坏情况下的性能也比较好,时间复杂度为O(nlogn),且不需要额外的空间开销。

        缺点:相对于快速排序,堆排序的实现稍显复杂,而且不够稳定。

        动图解释:

         具体代码实现与解释如下:  

        堆排序方法进行排序,首先建立一个大根堆,特点就是 根>=左右孩子首先应该将所有的非终端节点(分支节点)全部检查一遍,看是否满足大根堆的要求不满足则进行调整。在顺序存储的完全二叉树中,非终端节点就是编号为i<=[n/2]比如总共有8个数据,证明有8个节点,则i<=4,也就是前四个就是非终端节点,处理时先处理最远的,也就是4号节点,其实就是第四个数,这是线性表中的数,从1开始数对于非终端节点i,它的左孩子为2i,也就是第8个数,右孩子为2i+1,父节点为i/2.方法就是检查该节点是否大于左右孩子节点,若不大于,则与更大的孩子互换即可

#include <iostream>
using namespace std;

void HeadAdjust(int Array[], int k, int len) {
	int temp = Array[k];// 最好别用哨兵的方法,直接找一个变量存储
	//这里一开始让i=根节点的左孩子位置,进入循环
	for (int i = 2 * k; i <= len; i *= 2) {
		//下面的这个循环就是在做一个元素下坠的操作,首先判断左孩子(i)是否小于右孩子11
		//若是小于,则i++,此时i指向右孩子,若是大于,不满足,继续指向左孩子
		if (i < len && Array[i] < Array[i + 1]) { i++; }
		//这里如果节点大于左孩子或者右孩子,其实这里就比较了一个,因为左孩子和右孩子的比较
		//在上一步条件语句中已经执行,这里比较之后,如果满足,证明节点值大,直接退出循环即可
		if (temp >= Array[i]) { break; }
		//若是不满足,则将孩子与节点位置互换,并且同时k向前移动,占据i的位置
		//这里如果还没有结束,可以继续循环,此时k又变成了下一个节点,继续上述步骤
		else { Array[k] = Array[i]; k = i; }
	}
	//当i > len时,全部执行完毕,退出循环,此时再将在哨兵位置保存的值赋给Array[k] 
	Array[k] = temp;
}

void swap(int &a, int &b) {
	int temp = a;
	a = b;
	b = temp;
}

void Heapsort(int Array[], int len) {
	for (int i = len / 2 - 1; i >= 0; i--) {
		HeadAdjust(Array, i, len);
	}
	//用1en-1是因为对数组来说是从0下标开始计数
	for (int i = len - 1; i > 0; i--) {
		swap(Array[i],Array[0]);
		//如果不封装,直接交换也可以
// 		int temp = Array[i];
// 		Array[i] = Array[0];
// 		Array[0] = temp;
		HeadAdjust(Array, 0, i - 1);
	}
}

int main()
{

	int arr[9] = { 4,2,8,0,5,7,1,9,3 };
	int len = sizeof(arr) / sizeof(arr[0]); 
	cout << "排序前数据:" << endl;
	for (int i = 0; i < len;i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;	

	Heapsort(arr, len);
	
	// 排序后结果
	cout << "排序后数据:" << endl;
	for (int i = 0; i < len; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

	system("pause");
	return 0;

}

总结

        上述总结了最常用的几种排序算法,排序算法的优点使得它们能够在不同的场景下发挥作用,根据具体需求和数据特点选择合适的排序算法至关重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一伦明悦

感谢,您的支持是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值