【数据结构】第十五弹---C语言实现直接插入排序

时间:2024-06-13 13:37:19

 

个人主页: 熬夜学编程的小林

????系列专栏: 【C语言详解】 【数据结构详解】【C++详解】

目录

1、排序的概念及其运用

1.1、排序的概念与分类

1.2、排序运用

1.3、常见的排序算法

1.4、常见的排序算法性能测试

2、常见排序算法的实现

2.1、直接插入排序

2.1.1、基本思想

2.1.2、代码实现

2.1.3、代码测试

2.1.4、时空复杂度分析

总结


1、排序的概念及其运用


1.1、排序的概念与分类


排序:

排序是一种将一组对象按照某种特定顺序重新排列的过程。在计算机科学中,排序是数据处理中非常基本且重要的操作,它可以帮助人们更有效地理解和分析数据。排序的顺序通常是升序或降序,也可以按照数字、字母、大小或其他标准进行排序。


稳定性:

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


内部排序:

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


外部排序:

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

1.2、排序运用

排序的运用非常常见,在我们购物的软件上面,经常会根据不同的属性来进行排序,我们以前上学时候的成绩通常也会对各科进行排序!!!!


1.3、常见的排序算法

常见的排序算法有冒泡排序、选择排序、插入排序、快速排序、希尔排序、堆排序、归并排序等等!!!

注意:在本专栏中,还会讲解计数排序,它的效率非常高,但是局限性也很大。 

如下为常见排序实现的接口:

// 排序实现的接口

// 插入排序
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.4、常见的排序算法性能测试

实现好排序之后必不可少要对效率进行测试,前面我们用到的都是时间空间复杂度来评价一个算法的好坏,此处我们再用实际的时间来证明排序的性能!!!

// 测试排序的性能对比
void TestOP()
{
	srand(time(0));// 生成随机数的种子
	const int N = 10000000;// 动态开辟空间的元素个数,开辟较大的空间适用于效率高的排序
	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);
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand() + i;// rand()随机数只有几万个,不能达到真正的随机,加上i让它更随机
		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];
	}
	//clock计算程序运行到此时的时间 毫秒
	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();
	HeapSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	QuickSort(a5, 0, N - 1);
	int end5 = clock();

	int begin6 = clock();
	MergeSort(a6, N);
	int end6 = clock();

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

	int begin8 = clock();
	CountSort(a8, N);
	int end8 = clock();
    
    // 计算排序所用时间
	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("QuickSort:%d\n", end5 - begin5);
	printf("MergeSort:%d\n", end6 - begin6);
	printf("BubbleSort:%d\n", end7 - begin7);
	printf("CountSort:%d\n", end8 - begin8);

    // 释放空间
	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);
}

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

排序OJhttps://leetcode.cn/problems/sort-an-array/description/

2、常见排序算法的实现


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]插入,原来位置上的元素顺序后移。

直接插入排序动图如下:


2.1.2、代码实现

第一步:创建函数,确定函数的形参,毋庸置疑的是需要传一个待排序的数组,其次进行排序的时候是需要遍历数组的,因此需要知道元素个数。为什么不能再函数内部计算元素个数呢???

答案是将数组作为形参传给函数,此数组的实质是一个地址,而计算大小是用数组大小/数组元素大小,此时sizeof(数组名) 计算的大小是4或者8,不能准确计算元素个数。

因此函数为:

void InsertSort(int a[], int n);//升序

第二步:分析单趟排序的情况

  情况一:待排序的数大于其中任意一个已经排序好的数( tmp > a[i] )。

默认0 - end区间是有序的,将end + 1位置的数插入到有序数组中。

  • 这里的end代表已排序序列的最后一个元素的索引。将插入的数(tmp)依次往前比较,如果比前面的end位置的数小,则将此end位置的数放到end + 1 位置,并将end --。
  • 如果大于等于前面的数字,说明此位置就是插入的数的位置
  • 将插入的数给end + 1下标即可。

  情况二:待排序的数比所有已经排序好的数要小。

通过上面两种情况可以总结出循环的条件是end >=0。

int end;
int tmp = a[end + 1];
while (end >= 0)
{
	if (a[end] > tmp)
	{
		a[end + 1] = a[end];
		end--;
	}
	else
    {
		break;
    }
}
a[end+1]=tmp;

这里跳出循环有两种情况:

1.找到插入位置:当tmp大于等于a[end],这表明tmp应该插入在a[end]的后面,因为在tmp之前的元素(包括a[end]自己)都已经排序好了,并且都是小于等于tmp的。这就是tmp的正确位置,在这种情况下,我们执行break语句跳出循环,并将tmp放置在end + 1的位置。


2.达到有序序列的起点:当循环保持进行,end值在每次迭代中不断递减,如果tmp小于所有已排序的元素,end可能会变成-1,这意味着tmp比有序序列中所有现有的元素都要小,应该被放在整个序列的最开始位置。


在这两种跳出循环的情况下,我们总是需要执行a[end + 1] = tmp;来将tmp元素放置到正确的位置上。因为无论是找到合适的插入点还是tmp成为新的最小元素,我们都需要将它实际插入到有序序列中,这就是为什么这行代码放在循环之外,确保跳出循环后,我们执行最终的插入动作。

第三步:封装整个排序

一个元素就是有序数组,因此end从0开始。

// 升序
void InsertSort(int a[], int n)
{
	// [a,end]有序
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp) // 前面的数大于后面则覆盖前面
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;// 填充空位置
	}
}

2.1.3、代码测试

按照我们正常的惯例,实现完之后都需要进行测试,此处有两种测试方式,其一为打印测试,其二为调试测试,我们现在实现的都是排序算法,因此可以直接封装一个打印数组函数进行测试。

 打印函数如下:

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

测试代码:

int main()
{
	int a[] = { 9,8,7,6,5,4,3,2,1,0 };//给一组数据
	int sz = sizeof(a) / sizeof(a[0]);//计算数组元素个数
	printf("排序前:\n");
	ArrayPrint(a, sz);
	InsertSort(a, sz);
	printf("排序后:\n");
	ArrayPrint(a, sz);
	return 0;
}

测试结果: 

 

2.1.4、时空复杂度分析

时间复杂度:

插入排序算法的时间复杂度取决于输入数组中元素的初始排序状态

最坏情况 :如果数组是完全逆序的,那么每次插入操作都需要将元素移到已排序部分的开头。这就意味着对于第i个元素,可能需要进行i次比较和移动。这种情况下,算法的时间复杂度是O(N2),因为需要进行总计约1 + 2 + 3 + … + (n-1)次比较,这是一个n(n-1)/2的等差数列

最好情况 :这种情况发生在数组已经完全有序时。在这种情况下,每次比较后,很快就会找到插入位置(在已排序元素的末尾),不需要进行额外的移动。因此,最好情况下插入排序的时间复杂度是O(N),因为外层循环只会遍历一次数组,内层循环不会进行任何实际的比较和移动操作。

我们时间复杂度分析看的是最坏情况,因此直接插入排序的时间为O(N^2)。

空间复杂度:

插入排序的空间复杂度为O(1),因为它是一个原地排序算法,不需要额外的存储空间来排序。

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

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

总结

本篇博客就结束啦,谢谢大家的观看,如果公主少年们有好的建议可以留言喔,谢谢大家啦!