0
点赞
收藏
分享

微信扫一扫

【常见排序算法实现总结-C语言】

请添加图片描述


文章目录


前言


一、排序相关概念

  1. 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作
  1. 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

二、插入排序

2.1 直接插入排序

2.1.1 逻辑思路

  • 当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序
  • 此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较
  • 如果array[i]的序列应该在该位置之前,则该位置上的元素顺序后移;如果array[i]的序列应该在该位置之后
  • 找到插入位置的后面,将array[i]插入

2.1.2 代码实现

// 插入排序-直接插入排序
void InsertSort(int* a, int n)
{
	// 画图分析!!!

	// 版本1:
	//for (int i = 0; i < n - 1; i++)
	//{
	//	int end = i;
	//	// tmp每轮for循环的值是固定的
	//	int tmp = a[end + 1];
	//	while (end >= -1)     // 这样虽然可以,在最小下标处越界不报错,但是最好别越界!!!!!!!
	//	{
	//		if (tmp < a[end])
	//		{
	//			// end所在位置的值后移
	//			a[end + 1] = a[end];
	//			// 更新end
	//			end--;
	//		}
	//		else
	//		{
	//			// 新值插入end位置的下一个位置
	//			a[end + 1] = tmp;
	//			// 退出本轮循环
	//			break;
	//		}
	//	}
	//}

	// 优化:
	for (int i = 0; i < n - 1; i++)
	{
		// 单趟排序:[0,end]有序,end+1位置的值,插入进入,保持他依旧有序
		int end = i;
		// tmp每轮for循环的值是固定的
		int tmp = a[end + 1];
		while (end >= 0)     
		{
			if (tmp < a[end])
			{
				// end所在位置的值后移
				a[end + 1] = a[end];
				// 更新end
				end--;
			}
			else
			{
				// 新值插入end位置的下一个位置
				//a[end + 1] = tmp;  
				// 因为循环外需将end为-1时的情况考虑进去,
				// 此时这里退出循环时也需要执行此代码,所有可以将其统一放在循环外
				// 退出本轮循环
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

2.1.3 特性总结

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  1. 空间复杂度:O(1)
  2. 稳定性:稳定

2.2 希尔排序(缩小增量排序)

2.2.1 逻辑思路

  • 先选定一个整数gap
  • 把待排序数组中所有元素分成gap个组,所有相互距离gap的元素分在同一组内(距离gap为几,则元素会被分为几组)
  • 并对每一组内的数据进行组内的直接插入排序
  • 然后,更新gap(按一定的规则缩小gap值),重复上述分组和排序的工作
  • 当gap==1时,相当于一次直接插入排序,结束后,所有记录在统一组内排好序

2.2.2 代码实现

// 插入排序-希尔排序
// 自己估计(不严谨):时间复杂度:o(n*log3 n);
// 有人推断证明:平均o(n^1.3)
void ShellSort(int* a, int n)
{
	// 版本1:
	//int gap = 3;
	//for (int j = 0; j < gap; j++)// 上限画图分析得
	//{
	//	// 其中一组的插入
	//	for (int i = j; i < n - gap; i += gap)// 每次循环i加gap,循环上限小于为n - gap,每组不同,单上限都是小于其,画图可分析得
	//	{
	//		int end = i;
	//		// 记录待插入的值
	//		int tmp = a[end + gap];
	//		while (end >= 0)
	//		{
	//			if (a[end] > tmp)
	//			{
	//				// end后移gap位
	//				a[end + gap] = a[end];
	//				// 更新end
	//				end -= gap;
	//			}
	//			else
	//			{
	//				// 退出本轮循环
	//				break;
	//			}
	//		}
	//		// 插入新值tmp(将a[end] <= tmp的条件下和end最终为end-gap的条件统一赋值)
	//		a[end + gap] = tmp;
	//	}
	//}

	
	// 优化1:
	// (不按每组排完再下一组,而是交替排)
	//int gap = 3;
	//for (int i = 0; i < n - gap; i ++)// 每次循环i加1,循环上限小于为n - gap,每组不同,单上限都是小于其,画图可分析得
	//{
	//	int end = i;
	//	// 记录待插入的值
	//	int tmp = a[end + gap];
	//	while (end >= 0)
	//	{
	//		if (a[end] > tmp)
	//		{
	//			// end后移gap位
	//			a[end + gap] = a[end];
	//			// 更新end
	//			end -= gap;
	//		}
	//		else
	//		{
	//			// 退出本轮循环
	//			break;
	//		}
	//	}
	//	// 插入新值tmp(将a[end] <= tmp的条件下和end最终为end-gap的条件统一赋值)
	//	a[end + gap] = tmp;
	//}


	// 优化2:
	// gap不固定
	// 1-gap > 1,预排序;2-gap == 1,直接插入排序
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;// 保证最后一次gap一定为1(最后一次相当于直接插入排序)
		for (int i = 0; i < n - gap; i++)// 每次循环i加1,循环上限小于为n - gap,每组不同,单上限都是小于其,画图可分析得
		{
			int end = i;
			// 记录待插入的值
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					// end后移gap位
					a[end + gap] = a[end];
					// 更新end
					end -= gap;
				}
				else
				{
					// 退出本轮循环
					break;
				}
			}
			// 插入新值tmp(将a[end] <= tmp的条件下和end最终为end-gap的条件统一赋值)!!!!!!!!!!!
			a[end + gap] = tmp;
		}
	}
	
}

2.2.3 特性总结

  1. 希尔排序是对直接插入排序的优化
  2. 时间复杂度:希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此很多书中给出的希尔排序的时间复杂度都不固定,综和《数据结构(C语言版)》— 严蔚敏、《数据结构-用面相对象方法与C++描述》— 殷人昆,选取:o(n ^ 1.25)~ o(1.6 * n ^ 1.25)
  1. 空间复杂度:O(1)
  2. 稳定性:不稳定

三、选择排序

3.1 直接选择排序

3.1.1 逻辑思路

  • 在元素集合array[i]–array[n-1]中选择最大和最小的元素
  • 若它不是这组元素中的最后一个或第一个元素,则将它与这组元素中的最后一个或第一个元素交换
  • 在剩余的array[i]–array[n-2](或者array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

3.1.2 代码实现

// 选择排序-直接选择排序
void SelectSort(int* a, int n)
{
	// 每次遍历选出最大值和最小值(也可以每次选一个)
	// 然后判断最小值如果不再当前数组段得首位置,或者最大值不在末尾,交换他们到首尾处
	// 直到数组段仅剩一个元素
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		// 每轮遍历选值,最大最小值下标仅需为该轮数据的有效下标即可,left就为一个有效下标
		int maxi = left;
		int mini = left;
		// 因为初始化maxi和mini都赋值了left,所有可以直接从left+1遍历,个人感觉不好理解,除非加注释
		for (int i = left + 1; i <= right; i++)
		{
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
			if (a[mini] > a[i])
			{
				mini = i;
			}
		}
		// 交换
		Swap(&a[left], &a[mini]);
		// 小心left下标处的数据为最大值,需确保不是,否则需更新最大值下标为刚刚交换过去的mini位置
		// 因为此时mini位置的数据是原先left位置处的最大值
		if (left == maxi)
		{
			maxi = mini;
		}
		Swap(&a[right], &a[maxi]);

		left++;
		right--;
	}
}

3.1.3 特性总结

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(N^2)
  1. 空间复杂度:O(1)
  2. 稳定性:不稳定

3.2 堆排序

3.2.1 逻辑思路

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

3.2.2 代码实现

// 向下调整新堆
void AdjustDown(int* a, size_t size, size_t root)
{
	size_t parent = root;

	// 找到较大孩子(此处为大堆)
	// 1-先默认赋值较大孩子为左孩子 !!!!!!!!!!!!循环条件为判断子结点
	size_t child = parent * 2 + 1;
	while (child < size) // 当左孩子已经存在时进入循环
	{
		// 2-如果右孩子存在且大于左孩子,则将较大孩子选为右孩子 !!!!!!!!
		// 注:(child + 1) < size,不能带等号,否则越界!!!!!!!!!!!!!!
		if (((child + 1) < size) && (a[child + 1] > a[child]))
		{
			child++;
		}

		if (a[child] > a[parent]) // 此处为大堆
		{
			Swap(&a[child], &a[parent]);

			// 向下更新父子结点
			parent = child;
			// 子结点默认更新为左孩子
			child = parent * 2 + 1;
		}
		else
		{
			// 此数据结构已经为堆
			break;
		}
	}
}


// 选择排序-堆排序
// 优化堆排序(升序建大堆,降序建小堆)
void HeapSortplus(int* a, int size)
{
	// 向上调整建堆
	// 根结点无需向上调整(向上调整传入子结点下标),所以从根结点下一个结点向上调整建堆
	/*for (int i = 1; i < size; i++)
	{
		AdjustUp(a, i);
	}*/

	// 向下调整建堆
	// 叶子结点无需向下调整(向上调整传入父结点下标),所以从最后一个非叶子结点(最后一个结点的父亲)开始向下调整建堆,
	// 依次下标减一就到下一个局部父结点位置
	for (int j = ((size - 1) - 1) / 2; j >= 0; j--)
	{
		AdjustDown(a, size, j);
	}


	// 升序,建大堆(降序建小堆) 时间复杂度o(n * logn)
	for (int i = size - 1; i > 0; i--)
	{
		// 将堆顶元素(最大值)交换至最后
		Swap(&a[i], &a[0]);
		// 有效下标减一,锁定升序数组最后的最大值,将它放在堆外
		size--;
		// 重新向下调整,构建新的大堆,重复上述
		AdjustDown(a, size, 0);
	}

}

3.2.3 特性总结

  1. 使用堆来选数,效率很高
  2. 时间复杂度o(n*logn)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

四、交换排序

4.1 冒泡排序

4.1.1 逻辑思路

  • 所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
  • 冒泡排序:依次选取下标较小的元素,然后通过依次向后每相邻两个元素来比较交换,完成一轮冒泡排序;接着初始下标选取比上一轮大1的元素,重复上述工作;直到每一轮没有发生交换,则退出循环。

4.1.2 代码实现

// 交换排序-冒泡排序
// 时间复杂度o(n^2),最坏情况逆序:n+(n-1)+...+2+1->o(n^2),

void BubbleSort(int* a, int n)
{
	// 版本1:比较a[j] 和 a[j+1]
	// 配合内循环控制最后一个需要比较的下标每轮循环前移一位
	//for (int i = 0; i < n; i++)
	//{
	//	// 判断某轮内层循环是否发生交换(某轮内层循环起后续数组元素是否已经有序)
	//	int index = 1;
	//	// 一轮循环后,最大值(升序)会被冒泡到最后,所以下一轮仅需从下标0开始向后比较,直到上一轮最后位置的前一个位置的前一个位置(向后比较)即可
	//	for (int j = 0; j < n - i - 1; j++) 
	//	{
	//		if (a[j] > a[j + 1])
	//		{
	//			int tmp = a[j];
	//			a[j] = a[j + 1];
	//			a[j + 1] = tmp;
	//			index = 0;
	//		}
	//	}
	//	if (index)
	//	{
	//		// 此轮循环未发生交换
	//		break;
	//	}
	}

	// 版本2:比较a[j - 1] 和 a[j]
	// 配合内循环控制最后一个需要比较的下标每轮循环前移一位
	for (int i = 0; i < n; i++)
	{
		// 判断某轮内层循环是否发生交换(某轮内层循环起后续数组元素是否已经有序)
		int index = 1;
		// 一轮循环后,最大值(升序)会被冒泡到最后,所以下一轮仅需从下标1开始向前比较,直到到上一轮最后位置的前一个位置即可
		for (int j = 1; j < n - i; j++)
		{
			if (a[j - 1] > a[j])
			{
				int tmp = a[j - 1];
				a[j - 1] = a[j];
				a[j] = tmp;
				index = 0;
			}
		}
		if (index)
		{
			// 此轮循环未发生交换
			break;
		}
	}
}

4.1.3 特性总结

  1. 易理解
  2. 时间复杂度o(n^2)
  1. 空间复杂度:o(1)
  2. 稳定性:稳定

4.2 快速排序(递归+hoare版本)

4.2.1 逻辑思路

  • 假设按照升序对数组中[left, right]区间中的元素进行排序
  • 按照基准值对数组的 [left, right]区间中的元素进行划分
  • 划分成功后以key为边界形成了左右两部分 [left, key - 1] 和 [key+1, right]
  • 递归排[left, key - 1]
  • 递归排[key+1, right]
  • 快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可

4.2.2 代码实现及hoare版本思路

// 快速排序子函数-hoare版本
int PartSort(int* a, int left, int right)
{
	// 求当前区间选出的新key位置
	// 初始赋值key为最左边位置,右边先走找小找到停,左边再走找大找到停,交换二者,直到相遇,将相遇位置与key位置元素交换
	// 为什么可以确定最后相遇的一定为小于key位置元素的值,从而和key位置元素交换,因为右边先走,所以可以保证最终相遇的位置一定为小值


	// 版本1:key选择最左边
	int key = left;
	while (left < right)
	{
		// 右边先走
		while (a[right] >= a[key] && left < right)
		{
			right--;
			
		}
		// 左边再走
		while (a[left] <= a[key] && left < right)
		{
			left++;
		}
		// 二者停下且未相遇,交换
		Swap(&a[left], &a[right]);
	}
	// 二者相遇
	Swap(&a[left], &a[key]);// 同Swap(&a[right], &a[key]);
	// 返回相遇点作为新key
	return left; // 同right


	// 版本2:key选择最右边,相应左边先走
	int key = right;
	while (left < right)
	{
		// 左边先走
		while (a[left] <= a[key] && left < right)
		{
			left++;
		}
		// 右边再走
		while (a[right] >= a[key] && left < right)
		{
			right--;

		}
		// 二者停下且未相遇,交换
		Swap(&a[left], &a[right]);
	}
	// 二者相遇
	Swap(&a[left], &a[key]);// 同Swap(&a[right], &a[key]);
	// 返回相遇点作为新key
	return left; // 同right
}


// 交换排序-快速排序
// 快速排序递归实现
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}

	int key = PartSort4(a, left, right);

	// 递归左右子区间[left, key-1] key [key+1, right]
	QuickSort(a, left, key - 1);
	QuickSort(a, key + 1, right);
}

4.2.3 计算示意图

请添加图片描述


4.3 快速排序(递归+挖坑法版本)

4.3.1 逻辑思路

  • 假设按照升序对数组中[left, right]区间中的元素进行排序
  • 按照基准值对数组的 [left, right]区间中的元素进行划分
  • 划分成功后以key为边界形成了左右两部分 [left, key - 1] 和 [key+1, right]
  • 递归排[left, key - 1]
  • 递归排[key+1, right]
  • 快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可

4.3.2 代码实现及挖坑法版本思路

// 快速排序子函数-挖坑法版本
int PartSort2(int* a, int left, int right)
{
	// 求当前区间选出的新key位置
	// 如果令最左边为key的初始位置,则右边先走(第一次右边一旦找到小值,立即填key的坑(需要填小值))
	// 首先将key位置值保存,然后key位置就作为第一个坑,右先走找小找到填第一个坑,自身成为第二个坑
	// 左边再走找大找到填上一个坑,自身又作为当前坑,直到二者相遇

	// 保存key位置值
	int key = a[left];
	// key位置作为第一个坑
	int pit = left;
	while (left < right)
	{
		// 右先走
		while (a[right] >= key && left < right)
		{
			right--;
		}
		// 填上一个坑
		a[pit] = a[right];
		// 更新坑
		pit = right;
			
		// 左后走
		while (a[left] <= key && left < right)
		{
			left++;
		}
		// 填上一个坑
		a[pit] = a[left];
		// 更新坑
		pit = left;
	}
	// 二者相遇
	a[left] = key;// 同right
	// 返回相遇点下标作为新key位置下标
	return left;

	// 右边作为第一个坑,左边先走,同理
}

4.3.3 计算示意图

请添加图片描述


4.4 快速排序(递归+前后指针法版本)

4.4.1 逻辑思路

  • 假设按照升序对数组中[left, right]区间中的元素进行排序
  • 按照基准值对数组的 [left, right]区间中的元素进行划分
  • 划分成功后以key为边界形成了左右两部分 [left, key - 1] 和 [key+1, right]
  • 递归排[left, key - 1]
  • 递归排[key+1, right]
  • 快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可

4.4.2 代码实现及前后指针法版本思路

// 快速排序子函数-前后指针法版本
int PartSort3(int* a, int left, int right)
{
	// 版本1:key选为最左边
	
	// 起初prev指向最左边,cur指向prev后面一个,取key为第一个元素位置(起始和prev相同)
	// cur先和key位置的元素相比较,
	// 如果小于key值,prev先后移,然后和cur位置值交换(优化:当他们不同时再交换),cur再后移;
	// 如果大于key值,cur后移。
	// 当cur越界(大于right),则交换prev位置和key位置元素
	int key = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key] && a[cur] != a[++prev])// 专注于找小,如何遇到和keyi值相同交换会出问题,和keyi值相等的留在左右哪边都无所谓
		{
			Swap(&a[cur], &a[prev]);
		}
		// 无论大小,cur每轮都后移
		cur++;
	}
	// 交换prev位置和key位置元素!!!!!!!!!!!!!!!
	Swap(&a[prev], &a[key]);
	// 返回prev!!!!!!!!!!!!!!!!!!
	return prev;


	
	// 版本2:key选为最右边
	int key = right;
	int prev = left - 1;
	int cur = prev + 1;
	while (cur < right)// cur最后为key之前位置
	{
		if (a[cur] < a[key] && a[++prev] != a[cur])
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	// 交换prev+1位置和key位置元素!!!!!!!!!!!!!!!
	Swap(&a[prev + 1], &a[key]);
	// 返回prev+1!!!!!!!!!!!!!!!!!!
	return prev + 1;
}

4.4.3 计算示意图

请添加图片描述


4.5 快速排序优化

4.5.1 三数取中法选key

为了让递归深度最小,可以通过每次尽可能使得递归区间二分,所以采用三数取中法选key,使得key的位置避免在有序或接近有序的数组中,每次都选在左右边缘位置致使递归深度极大

// 三数取中法选keyi
int GetMidIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[left] > a[mid])
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[left] > a[right])
		{
			return left;
		}
		else if (a[right] > a[mid])
		{
			return mid;
		}
		else
		{
			return right;
		}
	}
}


// 快速排序子函数-添加三数取中法版本
int PartSort4(int* a, int left, int right)
{
	// 防止每次key都算在首尾处,导致最终时间复杂度高
	int mid = GetMidIndex(a, left, right);
	// 仍然使key取最左边,只是提前将最左边的元素换为三数取中法下标元素,否则会影响后续代码!!!!!!!!!!!!!!!!
	Swap(&a[left], &a[mid]);

	// 当keyi在最左边时
	int key = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key] && a[cur] != a[++prev])// 专注于找小,如何遇到和keyi值相同交换会出问题,和keyi值相等的留在左右哪边都无所谓
		{
			Swap(&a[cur], &a[prev]);
		}
		// 无论大小,cur每轮都后移
		cur++;
	}
	// 交换prev位置和key位置元素
	Swap(&a[prev], &a[key]);
	// 返回prev
	return prev;
}

4.5.2 小区间优化

在递归区间更深时,区间会越来越小,递归的消耗性价比也极低,所以使用小区间优化,在小区间直接使用其他排序方式来排序

// 交换排序-快速排序(小区间优化版本)
// 快速排序递归实现
void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}

	if (right - left + 1 <= 30)
	{
		// 小区间直接使用直接插入排序
		// InsertSort(a, right - left + 1);// 闭区间元素计算为首尾下标相减加1 错误!!!!!!!!!!!!
		// 每次传的数组不是a,而是a+left!!!!!!!!!!!!!!!
		// 闭区间中的元素个数为下标差加1
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		int key = PartSort4(a, left, right);

		// 递归左右子区间[left, key-1] key [key+1, right]
		QuickSort2(a, left, key - 1);
		QuickSort2(a, key + 1, right);
	}
}

4.6 快速排序(非递归版本)

4.6.1 非递归版本代码实现:

实际就是模仿递归区间的过程,不过快排模仿时,单纯使用循环无法完成,所以需要借助数据结构:栈或队列来辅助完成。

使用栈模仿和递归顺序相同,类似二叉树前序遍历;
使用队列模仿类似二叉树层序遍历。

// 交换排序-快速排序
// 非递归版本
void QuickSort3(int* a, int left, int right)
{
	// 一般非递归方法可以用循环实现就用循环,实现不了可能会借助一些数据结构
	// 本方法可以使用栈或队列实现
	// 具体过程见博客图
	
	// 方法1:使用队列
	// 首先创建队列并初始化
	Queue q;
	QueueInit(&q);

	// 将初始区间入队列
	QueuePush(&q, left);
	QueuePush(&q, right);

	while (!QueueEmpty(&q))
	{
		// 从队列中获取当前区间的left和right(注意队列是先进先出),获取后将其出队列
		int left = QueueFront(&q);
		QueuePop(&q);
		int right = QueueFront(&q);
		QueuePop(&q);

		// 求当前循环获取的区间的keyi
		int keyi = PartSort3(a, left, right);
		// 新区间 [left, keyi-1] keyi [keyi+1, right]

		// 根据新区间的范围判断是否入队列
		// 有效区间:左边界小于右边界
		if (keyi - 1 > left)
		{
			QueuePush(&q, left);
			QueuePush(&q, keyi - 1);
		}
		if (right > keyi + 1)
		{
			QueuePush(&q, keyi + 1);
			QueuePush(&q, right);
		}
	}

	QueueDestroy(&q);

	// 方法2:讲(同理)
}

4.6.2 计算示意图

请添加图片描述


4.7 快速排序总结

4.7.1 快速排序递归示意图

请添加图片描述

4.7.2 快速排序特性总结

  • 综合性能和使用场景较好
  • 时间复杂度:o(n*logn)
  • 空间复杂度:o(logn)
  • 稳定性:不稳定

五、归并排序

5.1 逻辑思路

  • 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用(相当于二叉树遍历的后序遍历)。
  • 先分割元素区间直到无法继续分割,直到每个区间仅剩一个元素(该区间即可看为有序)
  • 然后开始归并
  • 将已有序的子序列合并,得到完全有序的序列;
  • 先使每个子序列有序,再使子序列段间有序。
  • 若将两个有序表合并成一个有序表,称为二路归并

5.2 代码实现

5.2.1 递归版本

// 归并排序子函数(递归版本)
void _MergeSort(int* a, int left, int right, int* tmp)
{
	// 分解无法继续分解(分解为1个1个单独成组,就可以看为有序,可以直接归并)
	if (left >= right)
	{
		return;
	}

	// 获取当前区间的分割点下标
	int mid = left + (right - left) / 2;
	// 如果新区间为:[left, mid-1] [mid, right]  当遇到区间[1,2]会死循环
	// 所有,取新区间为[left, mid] [mid+1, right]

	// 分割左子区间
	_MergeSort(a, left, mid, tmp);
	// 分割右子区间
	_MergeSort(a, mid + 1, right, tmp);


	// 归并(将两个小区间按顺序合并到数组tmp的不同位置(通过下标+两个小区间的最左边界实现))
	// (相当二叉树后序遍历)
	// 从左右子区间中,依次选取相对最小的数,依次放入tmp相对应位置上
	int left1 = left;
	int right1 = mid;
	int left2 = mid + 1;
	int right2 = right;
	int cur = left;

	// 子区间指针,有一个越界,就退出循环
	while (left1 <= right1 && left2 <= right2)
	{
		if (a[left1] < a[left2])
		{
			tmp[cur++] = a[left1++];
		}
		else
		{
			tmp[cur++] = a[left2++];
		}
	}
	// 将子区间指针未越界的子区间顺序接在tmp后
	while (left1 <= right1)
	{
		tmp[cur++] = a[left1++];
	}
	while (left2 <= right2)
	{
		tmp[cur++] = a[left2++];
	}
	// 将该两个小区间合并后的结果覆盖到原数组a的相应位置上
	// 每轮归并都必须将tmp复制到a对应位置,否则下一轮赋值数组a时,a的下标还没有变化,会出问题
	memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));

	// 或者:
	/*for (int j = left; j <= right; j++)
	{
		a[j] = tmp[j];
	}*/
	
}


// 归并排序
// 时间复杂度o(n*logn),空间复杂度o(n)
void MergeSort(int* a, int n)
{
	// 先把各个元素分割成一个一个的元素区间(一个一个的元素区间就可以被看为有序的区间)
	// 然后归并,将各个单元素的小区间按照有序的排序,依次归并在新的数组的指定位置
	// 相当于后序遍历

	// 创建新的数组
	int* tmp = (int*)malloc(n * sizeof(int));
	assert(tmp);

	_MergeSort(a, 0, n - 1, tmp);

}

5.2.2 非递归版本

// 归并排序(非递归版本)(相当二叉树层序遍历)
// 时间复杂度o(n*logn),空间复杂度o(n)
void MergeSortNonR(int* a, int n)
{
	// 创建新的数组
	int* tmp = (int*)malloc(n * sizeof(int));
	assert(tmp);

	// 用gap来控制分割,赋值给各子区间,然后直接归并为有序
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			// 归并(将两个小区间按顺序合并到数组tmp的不同位置(通过gap来控制实现))
			// 元素个数如果不是2的次方,小心越界!!!!!!!!!!!
			int left1 = i;
			int right1 = i + gap - 1;
			int left2 = i + gap;
			int right2 = i + 2 * gap - 1;   // 这几个边界的控制是核心!!!!!!!!!!!!
			//printf("归并前:[%d,%d][%d,%d]\n", left1, right1, left2, right2); //测试归并区间代码

			// i在for循环已经判定是合理下标
			if (right1 >= n)
			{
				// 左区间的右边界越界,修正为n-1
				right1 = n - 1;
			}

			if (left2 >= n)
			{
				// 右区间非法,左右边界修正为非法边界(left2 > right2),不进入后续循环
				left2 = n;
				right2 = n - 1;
			}

			if (left2 < n && right2 >= n) // 防止出现:n-1为9, [8,9][10,11] -> [8,9][9,9] -> 导致生成3个数:8,9,9
			{
				// 右区间的左边界合法
				// 右区间的右边界越界,修正为n-1
				right2 = n - 1;
			}
			//printf("归并后:[%d,%d][%d,%d]\n", left1, right1, left2, right2);//测试归并区间代码

			int cur = i;

			// 子区间指针,有一个越界,就退出循环
			while (left1 <= right1 && left2 <= right2)
			{
				if (a[left1] < a[left2])
				{
					tmp[cur++] = a[left1++];
				}
				else
				{
					tmp[cur++] = a[left2++];
				}
			}
			// 将子区间指针未越界的子区间顺序接在tmp后
			while (left1 <= right1)
			{
				tmp[cur++] = a[left1++];
			}
			while (left2 <= right2)
			{
				tmp[cur++] = a[left2++];
			}
		}

		// 每个gap(相当于递归一层)归并结束后,将tmp合并后的结果覆盖到原数组a的相应位置上
		memcpy(a, tmp, sizeof(int) * n);

		// 更新gap为2倍
		gap *= 2;
	}
}

5.3 计算示意图

请添加图片描述


5.4 特性总结

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题
  2. 时间复杂度o(n*logn)
  1. 空间复杂度:o(logn)
  1. 稳定性:稳定

六、计数排序(非比较排序)

6.1 逻辑思路

  • 计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

6.2 代码实现

// 非比较排序-计数排序
// 时间复杂度:o(范围+n),空间复杂度:o(范围)
void CountSort(int* a, int n)
{
	// 绝对版本:
	// 获得原数组最大值下标
	//int maxi = 0;
	//for (int i = 0; i < n; i++)
	//{
	//	if (a[i] > a[maxi])
	//	{
	//		maxi = i;
	//	}
	//}

	 通过最值来确定所要映射数组的空间大小
	//int size = a[maxi] + 1;

	 创建映射数组
	//int* tmp = (int*)calloc(size, sizeof(int));
	//assert(tmp);

	 遍历原数组,通过相对映射在新数组赋值
	//for (int i = 0; i < n; i++)
	//{
	//	tmp[a[i]]++;
	//}

	//int j = 0;
	 遍历新数组,将映射结果依次从小到大(通过下标从小到大映射来控制)写回原数组
	//for (int i = 0; i < size; i++)
	//{
	//	while (tmp[i]--)
	//	{
	//		a[j++] = i;
	//	}
	//}

	// 相对版本:
	// 获得原数组最大值最小值下标
	int maxi = 0;
	int mini = 0;
	for (int i = 0; i < n; i++)
	{
		if (a[i] > a[maxi])
		{
			maxi = i;
		}
		if (a[i] < a[mini])
		{
			mini = i;
		}
	}

	// 通过最值来确定所要映射数组的空间大小
	int max = a[maxi];
	int min = a[mini];
	int size = a[maxi] - a[mini] + 1;//画图分析

	// 创建映射数组
	int* tmp = (int*)calloc(size, sizeof(int));
	assert(tmp);

	// 计数
	// 遍历原数组,通过相对映射在新数组赋值
	for (int i = 0; i < n; i++)
	{
		tmp[a[i] - min]++;
	}

	// 排序
	int j = 0;
	// 遍历新数组,将映射结果依次从小到大(通过下标从小到大映射来控制)写回原数组
	for (int i = 0; i < size; i++)
	{
		while (tmp[i]--)// 循环嵌套,变量之前有关系,所以时间度不应相乘
		{
			a[j++] = i + min;
		}
	}
}

6.3 特性总结

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  2. 时间复杂度:o(max(范围, n))
  3. 空间复杂度:o(范围)
  4. 稳定性:-


总结

这里对文章进行总结:
以上就是今天总结的内容,本文包括了常见排序算法实现总结-C语言,分享给大家。
真💙欢迎各位给予我更好的建议,欢迎访问!!!小编创作不易,觉得有用可以一键三连哦,感谢大家。peace
希望大家一起坚持学习,共同进步。梦想一旦被付诸行动,就会变得神圣。

欢迎各位大佬批评建议,分享更好的方法!!!🙊🙊🙊

举报

相关推荐

0 条评论