数据结构和算法之排序总结

一、排序的概念及应用

💦 排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次
序保持不变,即在原序列中,r[i]=r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排
序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

数据结构和算法动态可视化

💦 排序的运用

❗ 现实中排序的运用非常广泛,无处不在 ❕

好一个凡尔赛
在这里插入图片描述

💦 常见的排序算法

在这里插入图片描述

二、常见排序算法的实现

// 排序实现的接口
 
// 插入排序 void InsertSort(int* a, int n);
 
// 希尔排序
void ShellSort(int* a, int n);
 
// 选择排序 
void SelectSort(int* a, int n);

// 堆排序 
void AdjustDwon(int* a, int n, int root); 
void HeapSort(int* a, int n);
 
// 冒泡排序 
void BubbleSort(int* a, int n)

// 快速排序递归实现 
// 快速排序hoare版本 
int PartSort1(int* a, int left, int right); 
// 快速排序挖坑法 
int PartSort2(int* a, int left, int right); 
// 快速排序前后指针法 
int PartSort3(int* a, int left, int right); 
void QuickSort(int* a, int left, int right);

// 快速排序 非递归实现 
void QuickSortNonR(int* a, int left, int right)

// 归并排序递归实现 
void MergeSort(int* a, int n) 

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
 
// 计数排序 void CountSort(int* a, int n);
}
💦 插入排序
1、直接插入排序

🔑 核心思想 🔑

  把待排序的记录按关键码的大小逐个插入到一个已经排好的序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

实际中我们玩扑克牌时,就用了插入排序的思想

在这里插入图片描述
❗ 过程:❕

当插入第 i(i>=1) 个元素时,前面的 array[0], array[1], … , array[i-1] 已经排好序,此时用 array[i] 的排序码与 array[i-1], array[i-2],… 的排序码顺序进行比较,找到插入位置即将 array[i] 插入,原来位置上的元素顺序后移

❗ 直接插入排序的特性总结:❕

  1️⃣ 元素集合越接近有序,直接插入排序算法的时间效率越高

  2️⃣ 时间复杂度:O(N^2)

  3️⃣ 空间复杂度:O(1),它是一种稳定的排序算法

  4️⃣ 稳定性:稳定

❗ 动图演示:❕

请添加图片描述
🧿 实现代码 :

void InserSort(int* a, int n)
{
	//多趟控制
	int i = 0;
	for (i = 0; i < n - 1; i++)
	{
		//单趟控制
		int end = i ;
		int temp = a[end + 1];
		while (end >= 0)
		{
			//目标数小于其它数时,其它数就往后挪;大于则插入
			if (temp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = temp;
	}
}

❓ 插入排序的时间复杂度 ❔

  最坏的情况 - 逆序:O(N2)

  最好的情况 - 接近有序 :O(N)

2、希尔排序

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

🔑 核心思想 🔑

  希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成若干个组,所有距离为 gap 的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工## 标题作。当到达 = 1 时,所有记录在统一组内排好序。

  人话就是:

    1️⃣ 预排序 (接近升序) - gap > 1

    2️⃣ 直接插入排序 - gap == 1

在这里插入图片描述
❗ 希尔排序特性总结 ❕

  1️⃣ 希尔排序是对直接插入排序的优化

  2️⃣ 当 gap > 1 时都是预排序,目的是让数组更接近于有序。当 gap == 1 时,其实就是直接插入排序,且数组已经接近有序的了。整体而言,可以达到优化的效果,我们实现后可以进行性能测试的对比

  3️⃣ 希尔排序的时间复杂度并不好计算,因为 gap 的取值方法很多,导致很难去计算,因此在好些数中给出的希尔排序的时间复杂度都不固定,官方给出的时间复杂度是 O(N1.3)

  4️⃣ 稳定性:不稳定

👁‍🗨 知识扩展
在这里插入图片描述
🧿 实现代码 :

代码的核心并不是一组一组的排,而是多组并排

以下只是预排序代码,还需要再调用 InsertSort 进行直接插入排序

void ShellSort(int* a, int n)
{
	int i = 0;
	int gap = 3;
	//多组并排
	for (i = 0; i < n - gap; i++)
	{
		int end = i;
		int temp = a[end + gap];
		while (end >= 0)
		{
			if (temp < a[end])
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
			{
				break;
			}
		}
		a[end + gap] = temp;
	}
}

❓ 对于 gap 的值写成固定的并不好 ❔

  这里只是建议

void ShellSortPro(int* a, int n)
{
	//gap > 1 预排序
	//gap == 1 直接插入排序
	int i = 0;
	//gap的初始值为n
	int gap = n;
	while (gap > 1)
	{
		//每次循环gap都在减少,直到gap变成1
		gap = gap / 3 + 1;
		//gap /= 2;
		
		for (i = 0; i < n - gap; i++)
		{
			int end = i;
			int temp = a[end + gap];
			while (end >= 0)
			{
				if (temp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = temp;
		}
	}
}
💦 选择排序
1、直接选择排序

🔑 核心思想 🔑

  每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

❗ 过程:❕

  1️⃣ 在元素集合 array[i] - array[n-1] 中选择关键码最大 (小) 的数据元素

  2️⃣ 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换

  3️⃣ 在剩余的 array[i] - array[n-2] (array[i+1]–array[n-1]) 集合中,重复上述步骤,直到集合剩余 1 个元素

❗ 直接选择排序的特性总结:❕

  1️⃣ 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

  2️⃣ 时间复杂度:O(N^2) - 最好 / 最坏

  3️⃣ 空间复杂度:O(1)

  4️⃣ 稳定性:不稳定

❗ 动图演示:❕

请添加图片描述
🧿 实现代码 :

void Swap(int* px, int* py)
{
	int temp = *px;
	*px = *py;
	*py = temp;
}
void SelectSort(int* a, int n)
{
	int i = 0;
	int begin = 0;
	while (begin < n)
	{
		int mini = begin;
		//选最小
		for (i = begin; i < n; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		//交换
		Swap(&a[begin], &a[mini]);
		//迭代
		begin++;
	}
}

🧿 实现 SelectSort 的优化代码 :

   遍厉一遍选出最小的和最大的,然后把最小的放在左边,最大的放在右边

void Swap(int* px, int* py)
{
	int temp = *px;
	*px = *py;
	*py = temp;
}
void SelectSortPro(int* a, int n)
{
	int i = 0;
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		//选最大和最小
		int mini = begin, maxi = begin;
		for (i = begin; i <= end; i++)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		//交换
		Swap(&a[begin], &a[mini]);
		//当a数组里第1个元素是最大值时,此时经过上面的Swap,最大值的位置已经更改了,所以需要修正最大值的位置,让下一个Swap正确交换
		if (begin == maxi)
		{
			maxi = mini;
		}
		Swap(&a[end], &a[maxi]);
		//迭代
		++begin;
		--end;
	}
}
2、堆排序

🔑 核心思想 🔑

  堆排序 (Heapsort) 是指利用堆积树 (堆) 这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
  关于堆排序详解请转到 ➡ 仅不到五万字轻松了解二叉树和堆

❗ 堆排序的特性总结:❕

  1️⃣ 堆排序使用堆来选数,效率就高了很多。

  2️⃣ 时间复杂度:O(N*logN)

  3️⃣ 空间复杂度:O(1)

  4️⃣ 稳定性:不稳定

❗ 动图演示:❕

请添加图片描述
请添加图片描述
🧿 实现代码 :

void Swap(int* px, int* py)
{
	int temp = *px;
	*px = *py;
	*py = temp;
}
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (a[child] < a[child + 1] && child + 1 < n)
		{  
			child++;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* a, int n)
{
	//建大堆
	int i = 0;
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	//交换并调整
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}
💦 交换排序
1、冒泡排序

🔑 核心思想 🔑

  所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

❗ 冒泡排序的特性总结:❕

  1️⃣ 冒泡排序是一种非常容易理解的排序

  2️⃣ 时间复杂度:O(N^2)

  3️⃣ 空间复杂度:O(1)

  4️⃣ 稳定性:稳定

❗ 动图演示:❕

请添加图片描述
🧿 实现代码 :

void Swap(int* px, int* py)
{
	int temp = *px;
	*px = *py;
	*py = temp;
}
void BubbleSort(int* a, int n)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < n - 1; i++)
	{
		for (j = 0; j < n - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}

🧿 实现代码 BubbleSort 的优化版本 :

   当遍厉一遍后发现没有 Swap 时,那么说数组就是有序的

   时间复杂度:最坏 O(N2)

         最好 O(N)

void Swap(int* px, int* py)
{
	int temp = *px;
	*px = *py;
	*py = temp;
}
void BubbleSortPro(int* a, int n)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < n - 1; i++)
	{
		int flag = 1;
		for (j = 0; j < n - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				flag = 0;
				Swap(&a[j], &a[j + 1]);
			}
		}
		//如果flag等于1说明此时数组是升序
		if (flag == 1)
			break;
	}
}
2、快速排序

🔑 核心思想 🔑

  快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

❗ 过程:❕

在这里插入图片描述
  1️⃣ 选出一个关键字 key,一般是头或者尾

  2️⃣ 经过一次单趟后,key 放到了正确的位置,key 左边的值比 key 小,key 右边的值比 key 大

  3️⃣ 再让 key 的左边区间有序、key 的右边区间有序

❗ 动图演示:❕

一、首次单趟 (注意这三种方法首次单趟后不一定相同)

   💨 hoare 版本

请添加图片描述
 ❓ 如何保证相遇位置的值小于 key ❔

   💨 挖坑版本
请添加图片描述

   💨 前后指针法

请添加图片描述

二、非首次单趟

在这里插入图片描述

🧿 实现代码 :首次 + 非首次 + 递归版本

void Swap(int* px, int* py)
{
	int temp = *px;
	*px = *py;
	*py = temp;
}
void PartSortHoare(int* a, int left, int right)
{
	int keyi = left;
	while(left < right)
	{
		//左边作key,右边先走找小
		while(a[right] >= a[keyi] && left < right)
		{
			right--;
		}
		//右边找到小,再找左边的大
		while(a[left] <= a[keyi] && left < right)
		{
			left++;
		}
		//交换小大
		Swap(&a[right], &a[left]);
	}
	//交换key
	Swap(&a[keyi], &a[right]);
	//返回分割大小的那个下标
	return left;
}
int PartSortHole(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小,填左坑
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];//填坑
		hole = right;//新的坑

		//左边找大,填右坑
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];//填坑
		hole = left;//新的坑
	}

	//将key填最后一个坑
	a[hole] = key;
	return hole;
}
int PartSortPoint(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		//cur比keyi大时,prev不会++;且排除了自己交换自己
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);	
		}
		//两种情况cur都要++
		cur++;
	}
	//交换keyi
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	//递归的结束条件
	if (left >= right)
	{
		return ;
	}
	//keyi拿到分割大小的下标 - [left, keyi - 1]; [keyi]; [keyi + 1, right]
	//int keyi = PartSortHoare(a, left, right);//版本1
	//int keyi = PartSortHole(a, left, right);//版本2
	int keyi = PartSortPoint(a, left, right);//版本3
	
	//递归左
	QuickSort(a, left, keyi - 1);
	//递归右
	QuickSort(a, keyi + 1, right);
}

❓ QuickSort 的时间复杂度 ❔

在这里插入图片描述
🧿 实现 QuickSort 的优化代码 —— 优化有序的情况

  三数取中选 key —— left、mid、right 中不是最大也不是最小的数

//三数取中
int GetMidIndex(int* a, int left, int right)
{
	//int mid = (left + right) / 2;
	int mid = left + (right - left) / 2;//防止溢出版本
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if(a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
int PartSortHoarePro(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);
	int keyi = left;
	while (left < right)
	{
		while (a[right] >= a[keyi] && left < right)
		{
			right--;
		}
		while (a[left] <= a[keyi] && left < right)
		{  
			left++;
		}
		Swap(&a[right], &a[left]);
	}
	Swap(&a[keyi], &a[right]);

	return left;
}
int PartSortHolePro(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小,填左坑
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];//填坑
		hole = right;//新的坑

		//左边找大,填右坑
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];//填坑
		hole = left;//新的坑
	}

	//将key填最后一个坑
	a[hole] = key;
	return hole;
}
int PartSortPointPro(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		//cur比keyi大时,prev不会++;且排除了自己交换自己
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		//两种情况cur都要++
		cur++;
	}
	//交换keyi
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSortPro(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	//int keyi = PartSortHoarePro(a, left, right);//版本1
	//int keyi = PartSortHolePro(a, left, right);//版本2
	int keyi = PartSortPointPro(a, left, right);//版本3

	QuickSortPro(a, left, keyi - 1);
	QuickSortPro(a, keyi + 1, right);
}

❓ QuickSortHoarePro 的时间复杂度 ❔

  这里就不会出现最坏的情况 —— 有序,因为有了三数取中算法。


🧿 实现代码 :首次 + 非首次 + 非递归版本

  任何一个递归代码,要改成非递归

   1、循环

   2、栈 (数据结构) 模拟

  显然这里的快排不好直接改成循环,还要借助栈,所以这里复用了之前 C 实现的栈,详解请转 ➡ 爆肝两万字,我爷爷都看的懂的《栈和队列》,建议各位观众姥爷先收藏

🔑 核心思想 🔑
在这里插入图片描述

void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	StackInit(&st);
	//先入第一组区间
	StackPush(&st, right);
	StackPush(&st, left);
	//栈不为空
	while (!StackEmpty(&st))
	{
		//取栈顶left,并Pop
		int begin = StackTop(&st);
		StackPop(&st);

		//再取栈顶right,并Pop
		int end = StackTop(&st);
		StackPop(&st);

		//这里就用前后指针版本进行单趟排
		int keyi = PartSortPointPro(a, begin, end);
		
		//再入区间 [left, keyi - 1]; [keyi]; [keyi + 1, end]
			//右区间 —— 只有1个值不会入
		if (keyi + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}
			//左区间 —— 只有1个值不会入
		if (begin < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}
	StackDestory(&st);
}

❗ 快速排序的特性总结:❕

  1️⃣ 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

  2️⃣ 时间复杂度:O(N*logN)

  3️⃣ 空间复杂度:O(logN)

  4️⃣ 稳定性:不稳定

💦 归并排序

🔑 核心思想 🔑

  归并排序 (MERGE-SORT) 是建立在归并操作上的一种有效的排序算法,该算法是采用分治法 (Divide and Conquer) 的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列有序。若将两个有序表合并成一个有序表,称为二路归并。

  归并排序核心步骤:
在这里插入图片描述

❗ 动图演示:❕

请添加图片描述
🧿 实现代码 —— 递归版 :

void _MergeSort(int* a, int left, int right, int* temp)
{
	//只有一个值
	if (left >= right)
		return;

	//[left, mid][mid+1, right]
	int mid = (right + left) / 2;

	//递归
	_MergeSort(a, left, mid, temp);
	_MergeSort(a, mid + 1, right, temp);

	//归并到temp
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	
		//&&其中一段区间结束就结束了
	int index = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			temp[index++] = a[begin1++];
		}
		else
		{
			temp[index++] = a[begin2++];
		}
	}
		//begin2结束了,拷贝剩下的begin1
	while (begin1 <= end1)
	{
		temp[index++] = a[begin1++];
	}
		//begin1结束了,拷贝剩下的begin2
	while (begin2 <= end2)
	{
		temp[index++] = a[begin2++];
	}
		//归并后的结果,拷贝至原数组
	int i = 0;
	for (i = left; i <= right; i++)
	{
		a[i] = temp[i];
	}
}
void MergeSort(int* a, int n)
{
	//临时数组 
	int* temp = (int*)malloc(sizeof(int) * n);
	//子函数递归
	_MergeSort(a, 0, n - 1, temp);
	//释放临时数组
	free(temp);
}

🧿 实现代码 —— 非递归版 :

🔑 核心思想 🔑
在这里插入图片描述

void MergeSortNonR(int* a, int n)
{
	//临时数组 
	int* temp = (int*)malloc(sizeof(int) * n);
	int groupNum = 1;
	int i = 0; 
	while (groupNum < n)
	{
		for (i = 0; i < n; i += 2 * groupNum)
		{
			//[begin1, end1][begin2, end2]
			int begin1 = i, end1 = i + groupNum - 1;
			int begin2 = i + groupNum, end2 = i + groupNum * 2 - 1;
			//归并
			int index = begin1;
			//数组的数据个数,并不一定是按整数倍,所以分组可能越界或不存在
				//1.[begin2,end2]不存在或越界,修正为一个不存在的区间
			if (begin2 >= n)
			{
				begin2 = n + 1;
				end2 = n;
			}
				//2.end1越界,修正后归并
			if (end1 >= n)
			{
				end1 = n - 1;
			}
				//3.end2越界,修正后归并
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					temp[index++] = a[begin1++];
				}
				else
				{
					temp[index++] = a[begin2++];
				}
			}
			//begin2结束了,拷贝剩下的begin1
			while (begin1 <= end1)
			{
				temp[index++] = a[begin1++];
			}
			//begin1结束了,拷贝剩下的begin2
			while (begin2 <= end2)
			{
				temp[index++] = a[begin2++];
			}
		}
		//拷贝回原数组
		for (i = 0; i < n; i++)
		{
			a[i] = temp[i];
		}
		//迭代
		groupNum *= 2;

		//输出每层
		//PrintArray(a, n);

	}
	//释放临时数组
	free(temp);
}

❗ 归并排序的特性总结:❕

  1️⃣ 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题

  2️⃣ 时间复杂度:O(N*logN)

  3️⃣ 空间复杂度:O(N)

  4️⃣ 稳定性:稳定

💦 非比较排序
1、计数排序

🔑 核心思想 🔑

  计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

  计数排序核心步骤:

   1️⃣ 统计相同元素出现次数

   2️⃣ 根据统计的结果将序列回收到原来的序列中

在这里插入图片描述

❗ 动图演示:❕
请添加图片描述
🧿 实现代码 :

void CountSort(int* a, int n)
{
	//遍厉一遍找出最小值和最大值
	int min = a[0], max = a[0];
	int i = 0;
	for (i = 1; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	//求出这个区间 
	int range = max - min + 1;
	//calloc空间,并初始化为0 
	int* count = (int*)calloc(range, sizeof(int));
	//统计  
	for (i = 0; i < n; i++)
	{
		//相对位置
		count[a[i] - min]++;
	}
	//根据count数组排序
	i = 0; 
	int j = 0; 
	for (j = 0; j < range; j++)
	{
		while (count[j]--)  
		{
			a[i++] = j + min;
		}
	}
	free(count);
} 

❗ 计数排序的特性总结:❕

  1️⃣ 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限

  2️⃣ 时间复杂度:O(MAX(N,范围))

  3️⃣ 空间复杂度:O(范围)

  4️⃣ 稳定性:稳定

  5️⃣ 只适合整数排序,浮点数/字符串不能排

2、基数排序

🔑 核心思想 🔑

  基数排序又称桶排序,它分别按数据的个、十、百、千、万 … 排序,当然也可以先万、千、…

❗ 动图演示:❕

请添加图片描述

❓ 这里就不实现了,为什么 ❔

  因为这种排序实际在校招中和现实中已经很少使用了,各位码友有兴趣的也可以自己了解下

💦 文件排序 (拓展)

⚠ 注意

  小文件排序是没有意义的,当然我们这里只是模拟,所以给 100 个数据
在这里插入图片描述

🔑 核心思想 🔑

  磁盘的读取速度相比内存差距非常大,所以我们不可能像在内存中两两归并。正确的归并方法是大文件平均分割成 N 份,保证每份大小都可以加载到内存,那么就可以把每个小文件加载到内存中,使用快排排序,再写回小文件,这时就达到文件中归并的先决条件

在这里插入图片描述

🧿 实现代码 :

void _MergeFile(const char* File1, const char* File2, const char* mFile)
{
	//读文件1
	FILE* fout1 = fopen(File1, "r");
	if (fout1 == NULL)
	{
		printf("打开文件失败\n");
		exit(-1);
	}
	//读文件2
	FILE* fout2 = fopen(File2, "r");
	if (fout2 == NULL)
	{
		printf("打开文件失败\n");
		exit(-1);
	}
	//写文件3,把文件1和文件2写到文件3里
	FILE* fin = fopen(mFile, "w");
	if (fin == NULL)
	{
		printf("打开文件失败\n");
		exit(-1);
	}
	int num1, num2;
	//对于内存中没有问题,但是磁盘就有问题了。不管num1和num2谁小谁大,只要读了fout1和fout2它们都会往后走
	/*while (fscanf(fout1, "%d\n", &num1) != EOF 
		&& fscanf(fout2, "%d\n", &num2) != EOF)
	{
		if (num1 < num2)
			fprintf(fin, "%d\n", num1);
		else
			fprintf(fin, "%d\n", num2);
	}*/
	int ret1 = fscanf(fout1, "%d\n", &num1);
	int ret2 = fscanf(fout2, "%d\n", &num2);
	//下面保证了谁读谁走;fout1和fout2都不为空再比较
	while (ret1 != EOF && ret2 != EOF)
	{
		if (num1 < num2)
		{
			fprintf(fin, "%d\n", num1);
			ret1 = fscanf(fout1, "%d\n", &num1);//更新字符
		}
		else
		{
			fprintf(fin, "%d\n", num2);
			ret2 = fscanf(fout2, "%d\n", &num2); //更新字符
		}
	}
	/*注意这样会导致少写一个数据
		//fout2完了,写剩下的fout1
	while (fscanf(fout1, "%d\n", &num1) != EOF)
	{
		fprintf(fin, "%d\n", num1);
	}
		//fout1完了,写剩下的fout2
	while (fscanf(fout2, "%d\n", &num2) != EOF)
	{
		fprintf(fin, "%d\n", num2);
	}*/

	//fout2完了,写剩下的fout1
	while (ret1 != EOF)
	{
		fprintf(fin, "%d\n", num1);
		ret1 = fscanf(fout1, "%d\n", &num1);//更新字符
	}
	//fout1完了,写剩下的fout2
	while (ret2 != EOF)
	{
		fprintf(fin, "%d\n", num2);
		ret2 = fscanf(fout2, "%d\n", &num2); //更新字符
	}
	//关闭文件 
	fclose(fout1);
	fclose(fout2);
	fclose(fin);
}
void MergeSortFile(const char* file)
{
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		printf("打开文件失败\n");
		exit(-1);
	}
	int n = 10;
	int a[10];
	int i = 0;
	int num = 0;
	char subfile[20];
	int filei = 1;
	memset(a, 0, sizeof(int) * n);
	//从fout文件流里读,直至EOF
	while (fscanf(fout, "%d\n", &num) != EOF)
	{
		//每次循环读10个数据放在内存中(if里先放9个,else再放最后一个)
		if (i < n - 1)
		{
			a[i++] = num;
		}
		else
		{
			a[i] = num;
			//快排10个数据
			QuickSort(a, 0, n - 1);
			//生成文件名sub_sort1/2/3...
			sprintf(subfile, "%d", filei++);
			//写文件,subfile里存储生成的文件名
			FILE* fin = fopen(subfile, "w");
			if (fin == NULL)
			{
				printf("打开文件失败\n");
				exit(-1);
			}
			//写回小文件
			for (int i = 0; i < n; i++)
			{
				fprintf(fin, "%d\n", a[i]);
			}
			//关闭文件
			fclose(fin);
			//重置i
			i = 0;
			memset(a, 0, sizeof(int) * n);
		}
	}
	//互相归并到文件,实现整体有序
	char mFile[100] = "12";
	char File1[100] = "1";
	char File2[100] = "2";
	for (i = 2; i <= n; i++)
	{
		//读取File1和File2,归并出mFile
		_MergeFile(File1, File2, mFile);
		//拷贝迭代File1的文件名12/123/1234...
		strcpy(File1, mFile);
		//循环迭代File2的文件名3/4/5...
		sprintf(File2, "%d", i + 1);
		//循环迭代mFile的文件名123/1234/12345...
		sprintf(mFile, "%s%d",mFile, i + 1);
	}
	//关闭文件
	fclose(fout);
}
💦 性能测试

❓ 测试所有排序 && 怎么保证它是公平的 ❔

  数组里放的数据都是一样的

//测试排序的性能对比
void TestOP()
{
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (int*)malloc(sizeof(int) * N);
	int* a9 = (int*)malloc(sizeof(int) * N);
	int* a10 = (int*)malloc(sizeof(int) * N);
	int* a11 = (int*)malloc(sizeof(int) * N);
	int* a12 = (int*)malloc(sizeof(int) * N);

	for (int i = 0; i < N; i++)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i]; 
		a5[i] = a1[i];
		a6[i] = a1[i];
		a7[i] = a1[i];
		a8[i] = a1[i];
		a9[i] = a1[i];
		a10[i] = a1[i];
		a11[i] = a1[i];
		a12[i] = a1[i];
	}

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin2_1 = clock();
	ShellSortPro(a3, N);
	int end2_1 = clock();

	int begin3 = clock();
	SelectSort(a4, N);
	int end3 = clock();

	int begin3_1 = clock();
	SelectSortPro(a5, N);
	int end3_1 = clock();

	int begin4 = clock();
	HeapSort(a6, N);
	int end4 = clock();

	int begin5 = clock();
	BubbleSort(a6, N);
	int end5 = clock();

	int begin5_1 = clock();
	BubbleSortPro(a7, N);
	int end5_1 = clock();

	int begin6 = clock();
	QuickSort(a8, 0, N - 1);
	int end6 = clock();

	int begin6_1 = clock();
	QuickSortPro(a9, 0, N - 1);
	int end6_1 = clock();

	int begin6_2 = clock();
	QuickSortNonR(a10, 0, N - 1);
	int end6_2 = clock();

	int begin7 = clock();
	MergeSort(a11, N);
	int end7 = clock();

	int begin7_1 = clock();
	MergeSortNonR(a12, N);
	int end7_1 = clock();

	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("ShellSortPro:%d\n", end2_1 - begin2_1);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("SelectSortPro:%d\n", end3_1 - begin3_1);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("BubbleSort:%d\n", end5 - begin5);
	printf("BubbleSortPro:%d\n", end5_1 - begin5_1);
	printf("QuickSort:%d\n", end6 - begin6);
	printf("QuickSortPro:%d\n", end6_1 - begin6_1);
	printf("QuickSortNonR:%d\n", end6_2 - begin6_2);
	printf("MergeSort:%d\n", end7 - begin7);
	printf("MergeSortNonR:%d\n", end7_1 - begin7_1);


	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);
	free(a9);
	free(a10);
	free(a11);
	free(a12);
}

💨 输出结果 (这里使用 Release 版本 && 10 万个数据)

  这里测试 3 次

在这里插入图片描述

三、排序算法复杂度及稳定性分析

请添加图片描述
在这里插入图片描述

❗ 稳定性 (比较重要,注意不要死记,要结合思想来看) ❕

  假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次
序保持不变,即在原序列中,r[i]=r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排
序算法是稳定的;否则称为不稳定的。

在这里插入图片描述

❓ 稳定性的意义 ❔

  假设有一考试,并规定前 6 名发奖状,如果分数相同,则按交卷时间的先后计算名次。此时排序稳定性的意义就有所体现了

四、概念选择题

1、快速排序算法是基于 ( ) 的一个排序算法

A. 分治法

B. 贪心法

C. 递归法

D. 动态规划法

📝 分析:快速排序是一种分治的算法,其次递归不是一种算法


2、对记录(54, 38, 96, 23, 15, 72, 60, 45, 83)进行从小到大的直接插入排序时,当把第8个记录45插入到有序表时,为找到插入位置需比较 ( ) 次?(采用从后往前比较)

A. 3

B. 4

C. 5

D. 6

📝 分析:

15 23 38 54 60 72 96 45

所以需要比较 5 次


3、以下排序方式中占用 O(n) 辅助存储空间的是 ( )

A. 简单排序

B. 快速排序

C. 堆排序

D. 归并排序

📝 分析:注意没有简单排序;归并排序的空间复杂度是 O(N)


4、下列排序算法中稳定且时间复杂度为 O(n2) 的是 ( )

A. 快速排序

B. 冒泡排序

C. 直接选择排序

D. 归并排序

📝 分析:

冒泡排序是稳定的算法,且时间复杂度是 O(N2)

直接选择排序是不稳定的,例如:

55 1

1 55


5、关于排序,下面说法不正确的是 ( )

A. 快排时间复杂度为 O(N*logN),空间复杂度为 O(logN)

B. 归并排序是一种稳定的排序,堆排序和快排均不稳定

C. 序列基本有序时,快排退化成冒泡排序,直接插入排序最快

D. 归并排序空间复杂度为 O(N),堆排序空间复杂度的为 O(logN)

📝 分析:堆排序没使用递归,没有辅助空间,所以它的空间复杂度为 O(1)


6、下列排序法中,最坏情况下时间复杂度最小的是 ( )

A. 堆排序

B. 快速排序

C. 希尔排序

D. 冒泡排序

📝 分析:堆排序 (归并) 最坏情况下和最好情况下时间复杂度最小的 —— O(N*lonN)


7、设一组初始记录关键字序列为 (65,56,72,99,86,25,34,66),则以第一个关键字 65 为基准而得到的第一趟快速排序结果是 ( )

A. 34,56,25,65,86,99,72,66

B. 25,34,56,65,99,86,72,66

C. 34,56,25,65,66,99,86,72

D. 34,56,25,65,99,86,72,66

📝 分析:

我们前面已经了解到快速排序首次单趟的三种方法 —— hoare版本、挖坑版本、前后指针版本,注意在某些情况下需要都考虑到,因为三种版本得到的结果不一定都一样

在这里插入图片描述

结合情况此题选择 A 选项

  • 20
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

跳动的bit

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值