【数据结构】9. 排序

发布于:2025-07-29 ⋅ 阅读:(9) ⋅ 点赞:(0)

一、排序的概念及其运用

1、排序的概念

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

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

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

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

2、常见的排序算法

排序OJ(可使用各种排序跑这个OJ)OJ链接

二、常见排序算法的实现

0、准备工作

实现排序前我们先创建三个文件:
Sort.h:函数的声明,以及头文件的包含。
Sort.c:函数的实现。
Test.c:函数的测试。

首先创建Sort.h文件:

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>

//打印数组
void PrintArray(int* a, int n);

//交换
void Swap(int* a, int* b);

//插入排序
void InsertSort(int* a, int n);

//希尔排序
void ShellSort(int* a, int n);

//选择排序
void SelectSort(int* a, int n);

//堆排序
void HPSort(int* a, int n);

//冒泡排序
void BubbleSort(int* a, int n);

//快速排序
void QuickSort(int* a, int left, int right);

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

//归并排序
void MergeSort(int* a, int n);

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

//计数排序
void CountSort(int* a, int n);

接下来就在Sort.c中实现各种排序函数:

在此之前,先编写数组打印的函数以及交换函数,方便我们后序使用:

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* a, int* b)
{
	int temp = *b;
	*b = *a;
	*a = temp;
}

1、插入排序

插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

实际中我们玩扑克牌时,就用了插入排序的思想
在这里插入图片描述

1)插入排序

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

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		//一趟
		int end = i;
		int temp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > temp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = temp;
	}
}

再在test.c中来进行测试:

void TestInsertSort()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	InsertSort(arr, 9);
	PrintArray(arr, 9);
}

int main()
{
	TestInsertSort();
	
	return 0;
}

运行结果:
在这里插入图片描述

可以看到排序成功!

2)希尔排序

在这里插入图片描述

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

在这里插入图片描述
具体解析如下所示:

在这里插入图片描述

void ShellSort(int* a, int n)
{
	int gap = n;

	while (gap > 1)
	{
		gap = gap / 3 + 1;//+1保证最后一个gap一定是1

		//多组并着走
		for (int 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;
		}
	}
}

再进行测试:

void TestShellSort()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	ShellSort(arr, 9);
	PrintArray(arr, 9);
}

int main()
{
	TestShellSort();
	
	return 0;
}

运行结果:
在这里插入图片描述

可以看到成功排序!

2、选择排序

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

1)选择排序

在这里插入图片描述

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin, maxi = begin;

		for (int i = begin + 1; i <= end; i++)
		{
			//找到最大、最小数对应下标
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		//先交换最小值
		Swap(&a[mini], &a[begin]);

		//如果第一位就是最大值  
		if (maxi == begin)
			maxi = mini;//更新最大值

		//交换最大值
		Swap(&a[maxi], &a[end]);
		begin++;
		end--;
	}
}

再进行测试:

void TestSelectSort()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	SelectSort(arr, 9);
	PrintArray(arr, 9);
}

int main()
{
	TestSelectSort();
	
	return 0;
}

运行结果:
在这里插入图片描述

发现排序成功!

2)堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。

堆排序即利用堆的思想来进行排序,总共分为两个步骤:
①建堆
升序:建大堆
降序:建小堆

修改判断条件即可选择建造大堆还是小堆。

②利用堆删除思想来进行排序。
在堆中被删除的数据,实际上在数组中还存在,并且已经处于有序状态。

void AdjustDown(int* a, int n, int parent)
{
	//先假设左孩子大
	int child = parent * 2 + 1;

	while (child < n)
	{
		//先找到较大孩子
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}

		if (a[child] > a[parent])//建大堆
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

void HPSort(int* a, int n)
{
	//向下调整   O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	//堆删除   O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

再进行测试:

void TestHPSort()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	HPSort(arr, 9);
	PrintArray(arr, 9);
}

int main()
{
	TestHPSort();
	
	return 0;
}

运行结果:
在这里插入图片描述

3、交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。

交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

1)冒泡排序

没轮两两比较,大的就往后交换,总共交换n-1轮。

在这里插入图片描述

void BubbleSort(int* a, int n)
{
	for (int j = 1; j < n; j++)
	{
		int flag = 0;//假设初始有序
		//一趟
		for (int i = 0; i < n-j; i++)
		{
			if (a[i] > a[i + 1])
			{
				int temp = a[i];
				a[i] = a[i + 1];
				a[i + 1] = temp;
				flag = 1;
			}

		}

		if (flag == 0)	
			break;
	}
}

再进行测试:

void TestBubbleSort()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	BubbleSort(arr, 9);
	PrintArray(arr, 9);
}

int main()
{
	TestBubbleSort();

	return 0;
}

运行结果:
在这里插入图片描述

排序成功!

2)快速排序

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

在这里插入图片描述

int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	int begin = left, end = right;

	while (begin < end)
	{
		//右边找小
		while (begin < end && a[end] >= a[keyi])
		{
			--end;
		}
		//左边找大
		while (begin < end && a[begin] <= a[keyi])
		{
			++begin;
		}

		//交换
		Swap(&a[begin], &a[end]);
	}

	//相遇后
	Swap(&a[keyi], &a[begin]);
	return begin;
}

void QuickSort(int* a, int left, int right)
{
	//返回条件
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);

	//递归
	//[left ,keyi-1] keyi [keyi+1,right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);	
}

再进行测试:

void TestQuickSort()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	QuickSort(arr, 0, 8);
	PrintArray(arr, 9);
}

int main()
{
	TestQuickSort();
	
	return 0;
}

运行结果:
在这里插入图片描述
排序成功!

将区间按照基准值划分为左右两半部分的常见方式有:

  1. hoare版本 如上所示:

2 前后指针版本

具体解析如下:

//前后指针
int PartSort2(int* a, int left, int right)
{
	//三数取中(优化)
	int midi = GetMidi(a, left, right);
	Swap(&a[left], &a[midi]);

	int keyi = left;
	int prev = left;
	int cur = prev + 1;

	while (cur <= right)
	{
		//找到小,prev++,再交换
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}

	Swap(&a[keyi], &a[prev]);
	return prev;
}

void QuickSort(int* a, int left, int right)
{
	//返回条件
	if (left >= right)
		return;

	int keyi = PartSort2(a, left, right);

	//递归
	//[left ,keyi-1] keyi [keyi+1,right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);	
}

所达到的效果和使用Hoare版本是相同的。

2.a)快速排序优化

①:三数取中法选key。

当数据是有序的情况下,如果以第一个位置作为基准值,那么最后所需的时间复杂度是O(N2)左右,效率大大降低,这时就可以采用三数取中法来选基准值key。

int GetMidi(int* a, int left, int right)
{
	int midi = (left + right) / 2;

	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

int PartSort1(int* a, int left, int right)
{
	//三数取中
	int midi = GetMidi(a, left, right);
	Swap(&a[left], &a[midi]);
	
    //...
}

②:递归到小的子区间时,可以考虑使用插入排序。

当递归到很小的区间时,如果采用递归,那么每一层需要开辟的函数栈帧都等于前面开辟的函数栈帧总和。而小的区间数据少,就可以不用递归来排序,使用其他排序方法来进行排序。

int PartSort1(int* a, int left, int right)
{
	//小区间优化
	if ((right - left + 1) < 10)
	{
		InsertSort(a + left, right - left + 1);
		return right;
	}
	else
	{
		//... 快速排序
	}
}

3)快速排序非递归

使用栈来模仿递归的过程,就不需要开辟太多函数栈帧。

#include"Stack.h" //导入栈的方法

void QuickSortNorR(int* a, int left, int right)
{
	ST st;
	STInit(&st);
	STPush(&st, right);//先压入右边界
	STPush(&st, left); //再压入左边界

	while (!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);	//弹出左边界
		int end = STTop(&st);
		STPop(&st);	//弹出右边界

		int keyi = PartSort2(a, begin, end);//分割区间,获取基准值下标

		//[begin,key-1] keyi [keyi+1,end]
		//右区间
		if (keyi + 1 < end)
		{
			STPush(&st, end);//压入右边界
			STPush(&st, keyi + 1);//压入左边界
		}

		//左区间
		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);//压入右边界
			STPush(&st, begin);//压入左边界
		}
	}
}

4、归并排序

1)归并排序

基本思想:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并排序核心步骤:

具体解析如下:
在这里插入图片描述

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	//递归
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);


	//归并
	mid = (begin + end) / 2;
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}

	//begin1走完
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	//begin2走完
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	//拷贝回去
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

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

	free(tmp);
	tmp = NULL;
}

再进行测试:

void TestMergeSort()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	MergeSort(arr, 9);
	PrintArray(arr, 9);
}

int main()
{
	TestMergeSort();

	return 0;
}

运行结果:
在这里插入图片描述

2)归并排序非递归

在这里插入图片描述

void MergeSortNorR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		//一趟
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
		
			//检查越界
			if (begin2 >= n)
				break;

			if (end2 >= n)
				end2 = n - 1;

			int j = i;

			//归并
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			//begin1走完
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}

			//begin2走完
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			//拷贝回去
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		
		gap *= 2;
	}

	//释放
	free(tmp);
	tmp = NULL;
}

5、非比较排序

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

具体解析如下:
在这里插入图片描述

void CountSort(int* a, int n)
{
	//找到最小最大值
	int min = a[0], max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}

		if (a[i] > max)
		{
			max = a[i];
		}
	}

	//开辟一块大小为max-min+1长度的数组
	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail");
		return;
	}

	//统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;//将值映射到count的下标
	}

	//排序  依次还原即可
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}

	//释放
	free(count);
}

再进行测试:

void TestCountSort()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	CountSort(arr, 10);
	PrintArray(arr, 10);
}

int main()
{
	TestCountSort();

	return 0;
}

运行结果:
在这里插入图片描述

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

在这里插入图片描述

最后我们来测试一下使用各个排序方法排序10万个数据所需的时间:

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);

	for (int i = 0; i < N; i++)
	{
		a1[i] = rand() + 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];
	}

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();
	
	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

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

	int begin4 = clock();
	HPSort(a4, N);
	int end4 = clock();

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

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

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

	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HPSort:%d\n", end4 - begin4);
	printf("BubbleSort:%d\n", end5 - begin5);
	printf("QuickSort:%d\n", end6 - begin6);
	printf("MergeSort:%d\n", end7 - begin7);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
}


int main()
{
	TestOP();

	return 0;
}

运行结果:

得出的结果是各自算法所需的毫秒数,可以发现希尔、堆、快速、归并排序是相对较快的。


网站公告

今日签到

点亮在社区的每一天
去签到