C++中的排序算法

1. 排序的概念

排序是将一批无序的记录(数据)重新排列成按关键字有序的记录序列的过程。

2. 排序的稳定性

  • 稳定排序:对于关键字相等的记录,排序前后相对位置不变。
  • 不稳定排序:对于关键字相等的记录,排序前后相对位置可能发生变化。

3. 内部排序和外部排序

  • 内部排序:在排序期间,需要将待排序元素整体添加到内存中排序
  • 外部排序:在排序期间,不需要将待排序元素整体添加到内存中(归并是外部排序)

4. 排序的分类

  1. 比较类型排序
  • 插入方式排序:插入排序、希尔排序
  • 选择排序:直接选择排序、堆排序
  • 交换排序:冒泡排序、快速排序
  • 归并排序
  1. 非比较排序:计数排序和桶排序

5. 详述各个排序算法

5.1 插入排序

  • 概念:

插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。

  • 实现思路:

在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动 。

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:稳定
  • 适用场景:接近有序或者元素个数较少
//插入排序
void InsertSort(int array[], int size)
{
	for (int i = 0; i < size; ++i) 
	{
		//取待插入元素
		int key = array[i];
		//取待插入元素的前一个位置的下标
		int end = i - 1;
		while (end >= 0 && key < array[end])
		{
			array[end + 1] = array[end];
			end--;
		}
		array[end + 1] = key;
	}
}

在这里插入图片描述

5.2 希尔排序

  • 希尔排序的思想:(一般将增量设置为dk=dk/3+1,初始值为数组元素个数)
  1. 先取一个小于数组长度的增量d1作为第一个增量,把数组元素全部记录分组。所有距离为d1的倍数的放在同一个组中
  2. 然后开始循环对各组相邻增量元素,进行插入排序,只不过在这个插入排序中每次元素移动后,end的减少是以当前增量为单位的
  3. 当所有组内都已经排序好后,再将增量设置为更小的值继续执行上述步骤,直到增量变为1,实现最后一次插入排序,则排序完成
  • 时间复杂度: O(n^(1.3~2))
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 适用场景:数据量较小且基本有序时
void ShellSort(int array[], int first, int last)
{
	int dk = last - first;//初始增量
	while (dk > 1)
	{
		dk = dk / 3 + 1;//控制增量逐步减小
		for (int i = first ; i < last - dk; ++i)//每轮循环次数为总长度减去一个增量的距离
		{
			if (array[i + dk] < array[i])//比较相邻增量的两数
			{
				int end = i;//记录下标
				int tmp = array[end + dk];//记录值
				while (end >= first && tmp < array[end])//对每组进行插入排序
				{
					array[end + dk] = array[end];
					end -= dk;
				}
				array[end + dk] = tmp;
			}
		}
	}
}

在这里插入图片描述

5.3 直接选择排序

  • 直接选择排序的思想:

从头到尾对序列进行扫描,每一轮通过比较找到序列中的最小值(最大值)放进数组中,然后后续的所有轮次中这样做,直到所有元素都排完为止,最终得到一个有序的数组

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 适用场景:当数据规模较小时,选择排序性能较好
void SelectionSort(int array[], int len) {
	for (int i = 0; i < len-1; i++) {//外层只需循环len-1次,最后一个数不用再循环
		int min = array[i];	//初始时的最小值
		int index = i;//记录初始时的最小值下标
		for (int j = i+1; j < len; j++) {//内层中每次从所有剩余元素的第二个位置开始比较
			if (array[j] < min) {
				min = array[j];//查找最小值
				index = j;//将最小数的下标保存
			}
		}
		//把该轮中的最小数放入数组中
		int temp = array[index];
		array[index] = array[i];
		array[i] = temp;
	}
}

在这里插入图片描述

5.4 堆排序

  • 堆排序的概念:

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

  • 堆排序的思想:

先将整个堆调整为一个大堆,调整完之后,每次将堆顶元素和堆的最后一个元素交换,之后end- -,将交换后的节点排除出去,然后再对堆进行调整,最终得到一个有序序列。

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 适用场景:堆排序适合处理数据量大的情况,及数据呈流式输入的情况
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void _AdjustDown(int* elem, int first, int last, int start)//从每个分支自顶向下调整为大堆
{
	int n = last - first;
	//建立父节点指标和子节点指标
	int i = start;
	int j = 2 * i + 1;
	int tmp = elem[i];//保存父节点值
	while (j < n)//如果子节点指标在范围内才做比较
	{
		if (j + 1 < n && elem[j] < elem[j + 1])//先比较两个子节点,选择大的
			j++;
		if (tmp < elem[j])//如果父节点的值小于子节点,就交换父子内容,再继续子节点和孙节点比较
		{
			elem[i] = elem[j];
			i = j;
			j = 2 * i + 1;
		}
		else
			break;
	}
	elem[i] = tmp;
}

void HeapSort(int* elem, int first, int last)
{
	//调整成大堆
	int n = last - first;
	int curpos = n / 2 - 1;
	while (curpos >= 0)
	{
		_AdjustDown(elem, first, last, curpos);
		curpos--;
	}
	//排序
	int end = last - 1;
	while (end > first)//每次将堆顶元素和堆的最后一个元素交换,之后end--,将交换后的节点排除出去,然后再对堆进行调整,最终得到一个有序序列
	{
		Swap(&elem[end], &elem[first]);
		end--;
		_AdjustDown(elem, first, end + 1, first);
	}
}

5.5 冒泡排序

  • 冒泡排序思想:
  1. 嵌套两层for循环,外层负责控制排序的总趟数为len-1次,内层负责控制每躺循环的比较次数len-1-i
  2. (升序)在每次比较时如果j位置的元素>j+1位置的元素,则将其交换,否则不交换,继续下次比较
  3. 最终循环结束,排序完成
  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:稳定
  • 适用场景:当数据已经基本有序,且数据量较小时
void bubbleSort(int array[], int len) 
{
	int temp;
	int i, j;
	for (i = 0; i < len - 1; i++) // 外循环为排序趟数,len个数进行len-1趟 
		for (j = 0; j < len - 1 - i; j++)
		{ //内循环为每趟比较的次数,第i趟比较len-i次 
			if (array[j] > array[j + 1]) 
			{ //相邻元素比较,若逆序则交换(升序为左大于右,降序反之)
				temp = array[j];
				array[j] = array[j + 1];
				array[j + 1] = temp;
			}
		}
}

在这里插入图片描述

5.6 快速排序

  • 实现方法:直接快速排序法、挖坑法、前后指针(主要针对划分方式上有差异)
  • 实现思路:
  1. 取一个基准值(区间最右侧的数据或区间最左侧的数据)
  2. 按照基准值对区间中的元素进行划分:基准值左侧的数据小于基准值,基准值右侧的数据大于基准值,返回值基准值在划分好的区间中的下标
  3. [left, div) [div, right)继续递归使用快排的思想来进行排序
  • 快速排序空间复杂度:O(log2^N)

递归算法:单次递归所需要耗费的空间(没有借助赋值空间是常量)*递归的深度

  • 快速排序时间复杂度:
  1. 最差情况:如果每次分割时,机制准都是区间中的最大值|最小值,则分割完成:基准值一侧肯定是没有数据的
    图解:最终就是一个单支树,而每次进行数据划分,都是将区间中的每个元素遍历了—遍,因此三个划分时间复杂度都是:O(N)
  2. 最优情况:如果每次分割时,基准值都是区间中所有元素最中间的元素,则分割完成之后,基准值左右两侧的数据是均等的
    图解:分割完成后是一个平衡二叉树,每一层的时间复杂度是O(N,深度是log2^N,则时间复杂度O(Nlog2 ^N)
  • 稳定性:不稳定
  • 适用场景:数据量稍微比较大,数据越随机越好
  • 优化:针对取极值的方式进行优化,让取到极值的概率尽可能的降低 采用三数取中方式:最左侧、最右侧、中间,然后以三个数据最中间的数据作为基准值
  • 存在问题:采用递归的方式来进行处理,当数据量非常大—>递归层次非常深—>可能就会导致栈溢出
  • 解决方法:递归越深,每次递归期间,数据量越少,比较适合插入排序,让递归深度较深导致栈溢出的可能性降低
//三数取中找基准值
int GetMiddleIndex(int array[], int left, int right)
{
	int mid = left + ((right - left) >> 1);
	if (array[left] < array[right - 1])
	{
		if (array[mid] < array[left])
			return left;
		else if (array[mid] > array[right - 1])
			return right - 1;
		else
			return mid;
	}
	else
	{
		// array[left] > array[right-1]
		if (array[mid] > array[left])
			return left;
		else if (array[mid] < array[right - 1])
			return right - 1;
		else
			return mid;
	}
}
//直接快速排序
int partion(int array[],int left,int right)
{
	int midIdx = GetMiddleIndex(array, left, right);
	swap(array[midIdx], array[right - 1]);	
	int key = array[right - 1];
	//下标
	int begin = left;
	int end = right - 1;
	while (begin < end)
	{
		while (begin < end && array[begin] <= key)
		{
			begin++;
		}
		while (begin < end && array[end] >= key)
		{
			end--;
		}
		if(begin<end)
		{
			swap(array[begin], array[end]);
		}	
	}
	//如果key是区间的最大值
	if (begin != right - 1)
		swap(array[begin], array[right-1]);
	return begin;
}
//挖坑法
int partion1(int array[], int left, int right)
{
	int midIdx = GetMiddleIndex(array, left, right);
	swap(array[midIdx], array[right - 1]);
	int key = array[right - 1];
	//下标
	int begin = left;
	int end = right - 1;
	while (begin < end)
	{
		while (begin < end && array[begin] <= key)
			begin++;
		if (begin < end)
		{
			array[end] = array[begin];
			end--;
		}
		while (begin < end && array[end] >= key)
			end--;
		if (begin < end)
		{
			array[begin] = array[end];
			begin++;
		}
	}
	array[begin] = key;
	return begin;
}
//前后指针法
int partion2(int array[], int left, int right)
{
	int midIdx = GetMiddleIndex(array, left, right);
	swap(array[midIdx], array[right - 1]);
	int key = array[right - 1];
	//下标
	int cur = left;
	int prev = cur - 1;
	while (cur < right)
	{
		if (array[cur] < key && ++prev != cur)
		{
			swap(array[cur], array[prev]);
		}

		++cur;
	}
	if (++prev != right - 1)
		swap(array[prev], array[right - 1]);
	return prev;
}
//快速排序
void QuickSort(int array[], int left, int right)
{
	if (right - left > 1)
	{
		int div = partion2(array, left, right);
		//递归排基准值的左侧
		QuickSort(array, left, div);
		//递归排基准值的右侧
		QuickSort(array, div, right);
	}
}
  • 借助栈将递归转换成循环,实现循环快排
#include <stack>

void QuickSortNor(int array[], int size)
{
	stack<int> s;
	s.push(size);
	s.push(0);

	while (!s.empty())
	{
		int left = s.top();
		s.pop();
		int right = s.top();
		s.pop();

		if (right - left > 1)
		{
			int div = partion2(array, left, right);
			// [div+1, right)
			s.push(right);
			s.push(div + 1);

			// [left, div)
			s.push(div);
			s.push(left);
		}
	}
}

在这里插入图片描述

5.7 归并排序

  • 实现思路:
  1. 先将数组中的元素先递归均分成最小的单个元素,然后对单个元素两两归并成有序的数组,然后在逐层向上归并,每次归并后的数组都是有序的,最后合成一个完整的有序数组
  2. 归并的方法是对每两个有序数组中从左到右的元素依次进行比较,每次将较小的元素拷贝到temp数组里,然后在对该数组中的元素位置加一,然后再进行比较,直到有一方数组中的元素走完,再将另一个数组的剩余元素也拷贝到temp中即可。
  3. 最终把temp里最后存放的值拷贝到array数组中,就完成了排序(注意要对临时数组开辟空间,使用完释放空间)
  • 时间复杂度:O(Nlog2^N)
  • 空间复杂度:O(N)
  • 稳定性:稳定
  • 适用场景:数据量较大且要求排序稳定时

思路图解:
在这里插入图片描述

  • 最后一次合并的过程:

在这里插入图片描述

void MergeData(int array[], int left, int mid, int right, int* temp)
{
	int begin1 = left, end1 = mid;
	int begin2 = mid , end2 = right ;
	int index = left;
	while (begin1 < end1 && begin2 < end2)
	{
		if (array[begin1] <= array[begin2])
			temp[index++] = array[begin1++];
		else
			temp[index++] = array[begin2++];
	}
	while (begin1 < end1)
	{
		temp[index++] = array[begin1++];
	}
	while (begin2 < end2)
	{
		temp[index++] = array[begin2++];
	}
}

void _MergeSort(int array[],int left,int right,int* temp)
{
	if (right - left > 1)//至少有两个元素
	{
		int mid = left + ((right - left) >> 1);
		//[left,mid)
		_MergeSort(array, left, mid, temp);
		//[mid,right)
		_MergeSort(array, mid, right, temp);

		// 需要将左半侧和右半侧的数据进行归并
		MergeData(array, left, mid, right, temp);

		// 将temp中的元素拷贝会array
		memcpy(array + left, temp + left, sizeof(array[0]) * (right - left));
	}
	
}

void MergeSort(int array[], int size)
{
	int* temp = new int[size];
	_MergeSort(array, 0, size, temp);
	delete[]temp;
}

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值