【C】数据结构之八大排序(附动图及超详解)

本章我们进入到数据结构比较重要的一个版块了------排序

  • 本篇文章我们将介绍七大排序,这些排序是最核心也是最基础的,校招等考试必备,所以我想借此文章和大家一同学习并深入理解这八大排序
  • 在分块详解之前,我们要先了解我们准备讨论的是哪几个排序。常见的排序算法有四类:
  • 插入排序、选择排序、交换排序、归并排序
  • 四大类下可以分为七大排序,利用的核心思想是这四类,但实现途径不同,请大家和我一同看下面的分类:

除此之外,还有一款比较特殊的排序,他是一种非比较排序———计数排序。

ok,下面我们一一详解,请大家跟随我一同学习:

目录

1.直接插入排序

2.希尔排序

3.选择排序

4.堆排序

5.冒泡排序

6.快速排序

6.1.Partsort1----hoare版本

6.2.Partsort2----挖坑法

6.3.Partsort3----前后指针法

6.4.*递归算法完整实现快速排序

6.5.*快速排序优化(附完整代码)

6.6.快速排序的复杂度

6.7.非递归实现快排

7.归并排序

7.1.递归版

7.2.非递归版

7.3.归并排序的复杂度

8.计数排序

9.上述排序大总结


1.直接插入排序

这个排序方法我们多多少少在之前遇到过,首先我给出一个动图,有助于大家理解:

多看几遍后,我们会领悟到直接插入排序是一个一个取数据入数组比较,所以结合循环特质,我们最好先写内部比较过程再写外部循环过程,下面我们先完整给出代码过程:

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}

			else
			{
				break;

			}
		}

		a[end + 1] = tmp;
	}
}

上述代码我们按照思路来写的话就是先写内部比较过程,再控制循环取数过程:

特性总结:

  • 1.数组越接近有序,直接插入排序算法的时间效率越高。
  • 2.时间复杂度O(N^2)
  •    空间复杂度O(1)
  • 3.稳定性:稳定

ps:由于第一次遇到稳定性这一概念,我们介绍一下:

如图红色的1,2,3,4在蓝色1,2,3,4的前面,如果排完序后红色的1,2,3,4还在蓝色的1,2,3,4前面,那么则是稳定的,否则,就是不稳定的。

2.希尔排序

插入排序的第二类就是希尔排序,这个排序对于我们来说有点陌生,我们先介绍一下希尔排序:

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

 也就是,我们上面知道了直接插入排序是这一组数据越接近有序效率越高,那么我们这里就引进了希尔排序,也就是先进行预排序没然后再进行插入排序,如下图所示:

对于gap选择的几点解释

  • 如果gap越小,越接近有序,当gap == 1时排出来的就是有序的。
  • 如果gap越大,大的数据可以更快的到最后, 小的数据, 可以更快的到前面,但是它越不接近有序。
  • 上面图示gap的缩小进程每次是gap=gap/2,对于普遍的希尔排序算法还有gap=gap/3+1(+1是为确保gap最后是1)相对官方的一种写法是第二种,当然,两种都可以使用。

代码如下:

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//gap = gap / 2;
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}

特性总结:

  • 1. 希尔排序是对直接插入排序的优化。
  • 2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  • 3.稳定性:不稳定。
  • 4. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些例中给出的希尔排序的时间复杂度都不固定,我们最后引用两本书来解释:

 

  •  大概的时间复杂度就是:〇(N * logN)或者O(N^1.3)
  • 空间复杂度:O(1)

3.选择排序

选择排序我们也比较熟悉了,就是选取最小的元素放在第一个位置,然后在剩余的元素中再选取最小的元素放在第二个位置,以此类推。

 但是我们还可不可以改善算法呢?

  • 其实我们可以在找最小元素的时候同时寻找最大的元素,最小的放第一个位置,最大的放最后一个位置,再在剩余的元素中以此类推:

 下面我们给出代码:

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

	while (begin < end)
	{
		int maxi = begin;
		int mini = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		swap(&a[mini], &a[begin]);

		if (begin == maxi)
		{
			maxi = mini;
		}
		swap(&a[maxi], &a[end]);

		begin++;
		end--;
	}
}
特性总结:
  • 1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  • 2. 时间复杂度:O(N^2)
  • 3. 空间复杂度:O(1)
  • 4. 稳定性:不稳定

4.堆排序

堆排序我们在之前的二叉树不可中实现过,如果想完整了解堆的相关知识,请看下面传送门

传送门

这里我们基于堆的相关知识再讲解堆排序:

  • 基本思路如下:
  • 1、我们把这一组数放在一个堆中,升序则建立大堆,降序则建立小堆
  • (以大堆为例,我们取得整个数组,然后从第一个非叶子节点依次往前向下调整成大堆,然后依次交换头尾pop出去,得到了升序)
  • 2、我们每次把堆顶的元素和堆尾的元素交换,然后Pop掉堆尾的元素,然后调整堆,这里得到的就是堆中元素最大或者最小的元素,以此类推,最后就可以得到排序后的元素
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 确认child指向大的那个孩子
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}

		// 1、孩子大于父亲,交换,继续向下调整
		// 2、孩子小于父亲,则调整结束
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

// O(N*logN)
void HeapSort(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;
	}
}

特性总结:

  • 1. 堆排序使用堆来选数,效率就高了很多。
  • 2. 时间复杂度:O(N*logN)其中log以2为底
  • 3. 空间复杂度:O(1)
  • 4. 稳定性:不稳定

5.冒泡排序

冒泡排序在我们初始C语言时候想必大家都接触过,这里我们再回顾一下:

 基本过程大家已经非常清楚。

这里有一个改善时间效率的方法可以设置一个flag=0的变量,如果循环期间有数值交换则代表无序,则flag赋值为,如果一趟下来后flag的值还是为0没变。则已经有序,跳出循环即可。

// 冒泡排序
void BubbleSort(int* a, int n)
{
	int i = 0;
	int j = 0;
	for (j = 0; j < n; j++)
	{
		int flag = 0;
		for (i = 1; i < n - j; i++)
		{
			if (a[i - 1] > a[i])
				swap(&a[i - 1], &a[i]);
			flag = 1;
		}
		if (flag == 0)
			break;
	}
}

特性总结:

  • 1. 时间复杂度:O(N^2)
  • 2. 空间复杂度:O(1)
  • 3. 稳定性:稳定

6.快速排序

6.1.Partsort1----hoare版本

在我们讲解快排之前,我们先理解以下有序的概念:

有序序列:

 有序序列的特性:

  • 如图我们发现,对于一组升序有序序列中的每一个数字,该数字左面的数字都比该数字小,它右面的数字都比它大,那么我们称该数在序列中处于“正确位置”。
  • 快排就是利用这一特性,我们使得一组序列中的每个数字处于该数字所在的“正确位置”,那么这一组数据将会变成有序序列

下面我们讲解快排第一种版本---hoare(霍尔)版本。

示意图:

 要求:一躺要找到一个数字在它的“正确位置”——key,即key左边数字比他小,右边数字比他大

步骤:

  • 设置第一个数字为key;
  • 右边R先走,找到比key小的数字停下;
  • 左边L再走,找到比key大的数字停下;
  • L和R位置的数字进行交换,然后右边再走重复2,3,4过程;
  • L和R相遇,把相遇点和key原本所在的位置交换,交换后key左边的数字就比他小,右边的比他大,即key在他自己的“正确位置”。

常见疑问:

前续的操作把小的数堆积到了右边,大的数在左边,那么最后相遇位置的数一定不比key(第一个数)大吗?

答案是一定的。但注意若左边第一个数是key,那么必须右边先走,反过来也是一样。

分析:

  • 1.第一种情况是R先停下,然后L往右走与R相遇,此时R停下的位置一定比key小,L向右走遇到R,那么这个数一定比key小。
  • 2.第二种情况是L停下,那么R和L要进行交换,交换后L所在位置的数就比key小,然后R向前遇到L,那么这个数一定比key小。
  • 3.第三种情况是L一直往前走,如果R一直没有找到比key小的值,就一直向左找,直到找到key为止,相遇位置的值是等于key的, key和key交换。

总结相遇的位置一定不大于key所在位置的数。

一躺排序的代码如下:

int PartSort1(int* a, int begin, int end)
{
	

	int left = begin, right = end;
	int keyi = left;
	while (left < right)
	{
		// 右边先走,找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}

		// 左边再走,找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}

		swap(&a[left], &a[right]);
	}

	swap(&a[left], &a[keyi]);
	keyi = left;

	return keyi;
}

6.2.Partsort2----挖坑法

对比第一种霍尔版本,挖坑法是一种相似但更好理解的一种方法。

示意图:

在这里插入图片描述

 步骤:

  • 设置左边第一个数字所在位置为坑位,并保存第一个数字为key;
  • 右边R先走,遇到比key小的停下,并直接把该数字放在坑位中,然后R所在位置为新的坑位;
  • 左边L再走,遇到比key大的停下,并直接把该数字放在坑位中,然后L所在位置为新的坑位;
  • 重复上述操作直到L与R相遇,然后把key放在相遇的坑位中。

该步骤相比于霍尔版本少了交换的步骤,但是核心思路还是一样,较为容易理解。

一躺排序的代码如下:

int PartSort2(int* a, int begin, int end)
{
	
	int left = begin;
	int right = end;
	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;
	}
	a[hole] = key;
	return hole;

}




6.3.Partsort3----前后指针法

示意图:

在这里插入图片描述

步骤:

  • 设置两个指针分别为prev指向第一个数和cur指向第二个数,也就是keyi位置对应的数。
  • cur先走,如果比a[keyi]小就让cur和prev一前一后同时++一起走;如果cur遇到比a[keyi]大的就继续走,prev不走,直到cur遇到比a[keyi]小的数停下,然后prev++,然后prev和cur所在位置交换。
  • 然后cur再继续走,重复上面的操作。
  • 直到cur到了数组的末尾时候,再让prev和a[keyi]交换。
  • 这样比a[keyi]小的数据汇聚在前面,大的汇聚在后面。

一躺排序的代码如下:



int PartSort3(int* a, int begin, int end)
{
	
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	while (cur <= end)
	{
		if (a[cur] < a[keyi])
			// 找到比key小的值时,跟++prev位置交换,小的往前翻,大的往后翻
			//if (a[cur] < a[keyi] && ++prev != cur)
		{
			prev++;
			swap(&a[prev], &a[cur]);
		}

		cur++;
	}

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

6.4.*递归算法完整实现快速排序

通过上面的三种方法我们可以找到一个数的“正确位置”。

那么如何让他左右两边的数也处在“正确位置”呢?由于操作步骤一样,我们考虑使用递归算法

  •  如图我们找到的key处在了正确位置,然后分成了左右部分区间;
  • 我们递归算法再在左区间找到一个数的正确位置,然后以该数为key位置再递归左右区间再找到其他书所在的正确位置,当区间只剩下一个数或者没有数的时候返回。
  • 右区间也是如此......
  • 总结就是不断分小区间,找到每个数所在的正确位置返回即可。
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	if ((end - begin + 1) < 15)
	{
		// 小区间用直接插入替代,减少递归调用次数
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort1(a, begin, end);
        //或者int keyi = PartSort2(a, begin, end);
        //或者int keyi = PartSort3(a, begin, end);


		// [begin, keyi-1]  keyi [keyi+1, end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

递归执行的条件

  • 进入到PartSort函数的区间至少有两个值
  • 如果只有一个值, 或者没有值的话, 就会在进入递归函数之前就 return 了
  • 区间是左闭右闭类型的区间

6.5.*快速排序优化(附完整代码)

(1)优化1------三数取中

快排中如果每次key都在序列的一端,那么快排的时间效率会变成O(N^2),这样就达不到了优化算法的目的,所以在每次找到key位置之前,我们会取头,尾,中三个数中的中间值为初始的key,这样就避免了O(N^2)时间时间复杂度的产生。

(2)优化2------小区间非递归

  • 递归深度到一定程度,计算机开辟的空间会非常大,时间消耗也会随之增高,递归就会显得有点多余,但是我们此时的数据已经接近有序
  • 快速排序建立栈帧里面存放的是排序过程中要控制的区间
  • 接近有序时候我们使用插入排序的效率会非常高,为了避免过多的栈帧消耗,我们采用小区间优化。

比如下图,最后一行所开的空间比前两行都多,所以我们可以直接使用插入排序优化。

优化代码如下(以PartSort3为例):

//面对顺序排列,可以采用三数取中
// 三数取中
// begin  mid  end
int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[mid] > a[begin])
	{
		if (a[mid] < a[end])
			return mid;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else
	{
		if (a[mid] > a[end])
			return mid;
		else if (a[begin] < a[end])
			return begin;
		else
			return end;
	}
}
int PartSort3(int* a, int begin, int end)
{
	int mid = GetMidIndex(a, begin, end);
	swap(&a[begin], &a[mid]);
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	while (cur <= end)
	{
		if (a[cur] < a[keyi])
			// 找到比key小的值时,跟++prev位置交换,小的往前翻,大的往后翻
			//if (a[cur] < a[keyi] && ++prev != cur)
		{
			prev++;
			swap(&a[prev], &a[cur]);
		}

		cur++;
	}

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


void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	if ((end - begin + 1) < 15)//小区间优化
	{
		// 小区间用直接插入替代,减少递归调用次数
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort3(a, begin, end);

		// [begin, keyi-1]  keyi [keyi+1, end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

6.6.快速排序的复杂度

(1)时间复杂度

 假设每一个红色方格是key所在位置,递归到最深层过程中,这里是理想的key在中间左右位置时候,递归了log2N层,每一层历遍了N次(严格来说每一层并不是N,第一层是N,第二层是N - 1,第三层是N - 3,第四层是N - 7……),下面计算这里是理想值,真实值在下面区间

所以时间复杂度是O(N*log2N)~O(N^2)

ps:我们之前采用了三数取中,避免了key一直在一端的情况,所以不会出现O(N^2)的情况。

(2)空间复杂度

  • 因为空间的是可以重复使用的,函数结束调用之后会将创建的栈帧销毁
  • 根据快排代码的基本思路是先将key的左区间排完序,再去将key的右区间排有序
  • 那么根据代码思路它是一层一层递归,不断地选key,不断地将选出来的key的左区间缩小
  • 当左区间不能再分割时,递归就开始往回返,销毁栈帧,开始排右区间
  • 排右区间用的栈帧是刚刚左区间销毁的
  • 所以从宏观来看左区间的数排完栈帧全部销毁之后,右区间继续用之前销毁的空间
  • 所以空间复杂度就为高度次个

所以空间复杂度是〇(log2N)

6.7.非递归实现快排

非递归实现快排,我们联想到二叉树非递归的历遍,我们这里采用的数据结构来实现。

基本思路:

  • 我们首先完成一躺快排的代码,见上文;
  • 我们封装一趟的快排在函数中,下面我们就考虑在哪个区间找出该区间的key并放在正确位置;
  • 下面我们就要用到了栈:
  • 先把begin和end入栈,然后出栈得到了区间的右边界和左边界,带入函数得出keyi(即该数的正确位置);
  • 然后再让left和keyi-1入栈以及keyi+1和right入栈,重复上述操作即可。

该过程模拟的就是递归,参考二叉树章节。

下面请看代码,栈的实现请回顾之前的博文------>>栈的实现

//非递归版本
void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{

		int right = StackTop(&st);
		StackPop(&st);

		int left = StackTop(&st);
		StackPop(&st);

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

		if (keyi + 1 < right)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);

		}

		if (keyi - 1 > left)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);

		}

	}

	StackDestory(&st);

7.归并排序

7.1.递归版

基本思想:
归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法 Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
这里排序用的基本思路也是使得一段数据整体上逐渐接近于有序,使得最后排序效率高。
示意图:

 再看一个图片:

 

 通过上面两张图片,我们就大概理解了过程:

  • 将n个元素从中间切开,分成两部分。 
  • 将步骤1分成的两部分,再分别进行递归分解。 直到所有部分的元素个数都为1。
  • 从最底层开始逐步合并两个排好序的数列。
  • 上述排序过程使用递归。

ps:写代码中过程中,由于我们一边要对数据排序,一边还要存放数据,所以我们开辟新的与原来一样大小的数组存放排好序的数据,然后再拷贝回原来数组中。

代码实现:

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

	int mid = (begin + end) / 2;

	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	int begin1 = begin;
	int end1 = mid;

	int begin2 = mid + 1;
	int end2 = end ;

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

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

	while (begin2<= end2)
	{
		tmp[i++] = a[begin2++];
	}
	memcpy(a + begin, tmp + begin, sizeof(int)*(end - begin + 1));
}


void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

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

	free(tmp);
	tmp = NULL;
}

7.2.非递归版

我们参考上面的递归版本,我们给出下面思路:

我们从最小区间开始,然后两两排,然后再区间再翻倍排,直到区间包含了整个数组

示意图:

具体过程:

  • 直接从最小规模问题开始归, 没有借助递归进行分割
  • 两个区间两个区间的归并,1 - 1 归, 2 - 2 归, 4 - 4归
  • 只要控制好两层循环,内层循环控制好两两归并,外层循环控制好归并的区间

遇到的问题:

数组内数据不是2的倍数,归并时候可能会越界。

我们在代码中给出几组数据试验并打印区间如下(打印过程代码中附有)

 经过尝试,发现除了begin1不会越界,end1,begin2,end2都有越界的可能。所以我们在归并前用条件语句进行判断一下

代码如下:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	int rangeN = 1;

	while (rangeN < n)
	{
		for (int i = 0; i < n; i += 2 * rangeN)
		{
			int begin1 = i;
			int end1 = i + rangeN - 1;
			int begin2 = i + rangeN;
			int end2 = i + 2 * rangeN - 1;
			printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);

			int j = i;

			if (end1 >= n)//使区间不存在
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}

			else if (end2 >= n)
			{
				end2 = n - 1;
			}

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

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

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			
		}
		memcpy(a, tmp, sizeof(int) * (n));

		rangeN *= 2;

		}

	free(tmp);
	tmp = NULL;
	}




//void MergeSortNonR(int* a, int n)
//{
//	int* tmp = (int*)malloc(sizeof(int)*n);
//	if (tmp == NULL)
//	{
//		perror("malloc fail");
//		exit(-1);
//	}
//
//	// 归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
//	int rangeN = 1;
//	while (rangeN < n)
//	{
//		for (int i = 0; i < n; i += 2 * rangeN)
//		{
//			// [begin1,end1][begin2,end2] 归并
//			int begin1 = i, end1 = i + rangeN - 1;
//			int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
//			printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
//			int j = i;
//
//			// end1 begin2 end2 越界
//			if (end1 >= n)
//			{
//				break;
//			}
//			else if (begin2 >= n)
//			{
//				break;
//			}
//			else if (end2 >= n)
//			{
//				end2 = n - 1;
//			}
//
//			while (begin1 <= end1 && begin2 <= end2)
//			{
//				if (a[begin1] <= a[begin2])
//				{
//					tmp[j++] = a[begin1++];
//				}
//				else
//				{
//					tmp[j++] = a[begin2++];
//				}
//			}
//
//			while (begin1 <= end1)
//			{
//				tmp[j++] = a[begin1++];
//			}
//
//			while (begin2 <= end2)
//			{
//				tmp[j++] = a[begin2++];
//			}
//
//			// 归并一部分,拷贝一部分
//			memcpy(a + i, tmp + i, sizeof(int)*(end2 - i + 1));
//		}
//
//		rangeN *= 2;
//	}
//
//	free(tmp);
//	tmp = NULL;
//}

7.3.归并排序的复杂度

(1)时间复杂度

  • 归并排序时严格的二分,所以每次递归划分左右区间的时候都是二分划分的
  • 那么他的区间递归展开就是一棵满二叉树
  • 总的来看每一层都要执行N次,一共log2N层,可参考快排的时间复杂度
  • 时间复杂度:O(N*logN)。

(2)空间复杂度

  • 归并排序中我们开辟了和数组一样的额外空间,在这块空间排序,最后才拷贝回原数组。
  • 所以空间复杂度:O(N)。

8.计数排序

上面我们所介绍的排序均为比较排序,下面我们将介绍一种非比较排序------计数排序。

过程:

  • 我们历遍一遍找出这组数的最大值和最小值;
  • 我们在找最大值和最小值这之间开辟一个数组,初始数据都为0;
  • 我们再次历遍一次这组数据,计算这组数据中每个数出现的次数,然后在新开辟的数组中的相对位置进行映射;
  • 最后根据映射内容,拷贝回原数组。

示意图:

 注释:下面的数组存放的数据是对应下标的数字在第一个数组中出现的次数!

#include<stdio.h>

void CountSort(int* a, int n)
{
	int max = a[0];
	int min = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
		
	}
	int range = max - min + 1;

	int* countA = (int*)calloc(range, sizeof(int));
	if (countA == NULL)
	{
		printf("calloc fail\n");
		exit(-1);
	}

	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;
	}

	int k = 0;
	for (int j=0;j<range;j++)
	{
		while (countA[j]--)
		{
			a[k++] = j + min;
		}
	}
	
	free(countA);
}
int main()
{
	int a[] = { -3,1,2,3,2,8,4,5,-9,7,1 };
	CountSort(a, sizeof(a) / sizeof(int));

	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		printf("%d ", a[i]);
	}
	return 0;
}

不足之处:

计数排序只可以排正数负数(也就是int类型),其他类型不可以排。

特性总结:
  • 1、计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  • 2、空间复杂度:〇(range)
  • 3、时间复杂度:
  • 遍历原数组找出 最大/最小 值 时间复杂度:〇(N)
  • 遍历原数组计数,时间复杂度:〇(N)
  • 写会原数组,时间复杂度:〇(range + N)
  • 综上所述:
  • 精确时间复杂度:〇(range + 3N)
  • 粗略估计:时间复杂度:〇(range + N)

9.上述排序大总结

(1)总结表格

  • 总结:插入排序, 冒泡排序, 归并排序  - 稳定

(2)随机数据测试

两组数据量不同,但是得到的结果是差不多的——现实应用中,希尔排序,堆排序,快排,归并排序较为有效率。

谢谢大家的阅读,别忘了点赞关注支持一下作者哦!

猜你喜欢

转载自blog.csdn.net/m0_67821824/article/details/128553875