【数据结构与算法初阶(c语言)】插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序-全梳理(万字详解,干货满满,建议三连收藏)

目录

1.排序的概念及其运用

1.1排序的概念

1.2排序运用

1.3常见的排序算法

2.插入排序 

2.1 原理演示:​编辑

2.2 算法实现 

2.3 算法的时间复杂度和空间复杂度分析 

 3.希尔排序

3.1算法思想

3.2原理演示

3.3代码实现

3.4希尔算法的时间复杂度

4.冒泡排序

4.1冒泡排序原理(图解)

4.2.冒泡排序实现

4.3冒泡排序封装为函数

4.4 冒泡排序代码效率改进。 

5.堆排序

5.1升序建大堆,降序建小堆

 5.2 建堆的时间复杂度

5.2.1 向下调整建堆

5.2.2向上调整建堆

5.3 排序实现

6.选择排序 

6.1基本思想:

6.2算法演示

6.3代码实现 

6.4时间复杂度分析

7.快速排序

7.1 hoare版本

7.1.1算法图解

7.1.2代码实现 

7.1.3 算法优化---三数取中

7.2挖坑法快排 

7.2.1算法图解

7.2.2 代码实现

7.3 前后指针法

 7.4快速排序进一步优化

7.5快速排序非递归写法

8.归并排序

9.计数排序

10.结语


深度优先遍历 dfs  : 先往深处走,走到没有了再往回,前序遍历 一般是递归

广度优先遍历  bfs  层序遍历  一般用队列 

1.排序的概念及其运用

1.1排序的概念

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

这种情况对应考试成绩相同确认排名有很大的好处。

 

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

1.2排序运用

1.3常见的排序算法

2.插入排序 

直接插入排序是一种简单的插入排序法

算法思想:

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

和我们玩扑克牌整理牌的时候是非常相像的

将我们的摸到的牌作为手里的牌的最后一张,从倒数第一张开始到第一张逐次比较,如果当前牌大于前一张就放置在当前位置,如果小于那就向前移动,最坏的情况就是每次摸到牌都要逐渐比较放到最前面。

2.1 原理演示:

2.2 算法实现 

由原理图我们知道这个算法的实现就是第二个先和1第一个比较,调整位置,第三个又和前两个进行比较调整位置。

第一次 调整前两个位置

第二次 调整前三个位置

第三次 调整前四个位置

。。。

n个元素,就调整n-1次

每一次调整,都是倒数一个与倒数第二个比较调整完,倒数第二个又和倒数第三个做比较,知道比较到倒数最后一个也就是最开始的一个。

void InserPort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n - 1; i++)//n个元素调整n-1次
	{
		int end = i;
		int tmp = a[end + 1];//最后一个元素值给tmp

		while (end >= 0)
		{
			if (tmp < a[end])
			{
				//交换
				a[end + 1] = a[end];
				a[end] = tmp;
			}
			else
			{
				//不动
				break;
			}
			//继续比较调整前两个
			end--;
		}
	}
}

2.3 算法的时间复杂度和空间复杂度分析 

上述代码就是最坏的情况:

时间复杂度为O(N^2)

空间复杂度为O(1)

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

 3.希尔排序

3.1算法思想

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当间距到达=1时,所有记录在统一组内排好序
通俗来讲:就是先进行与排序再直接插入排序
预排序的操作是将数据以同一个间隔的数据分为一组:使得更大的数更快的到一段,更小的数更快到一端。

3.2原理演示

3.3代码实现

ShellSort(int* a, int n)
{
	int i = 0;
	int gap = n;
	while (gap > 1)
	{
		gap /= 2;
		


		for (i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end>gap)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					a[end] = tmp;
				}
				else
				{
					break;
				}
				end -= gap;
			}

		}
	}
}

3.4希尔算法的时间复杂度

希尔排序的时间复杂度不好计算,因为 gap 的取值方法很多,导致很难去计算,因此在好些树中给出的 希尔排序的时间复杂度都不固定

《数据结构》--严蔚敏

 《数据结构-用面相对象方法与C++描述》--- 殷人昆

按照:

来计算

空间复杂度为O(1)

稳定性:不稳定

4.冒泡排序

 核心思想:两两相邻的元素相比较是一种交换排序

4.1冒泡排序原理(图解)

4.2.冒泡排序实现

通过图解原理我们可以发现规律(以10个元素排列举例)

1.每一趟比较完全完成就把这一趟数据的最大数放在最后,第一趟是10个数据中的最大值9在最后,第二趟是9个数据中的8在最后,那么我们10个元素就要循环9趟,那么n个数据,就要循环n - 1趟。

2.第一趟是10个数据,就有9对数据比较,那么第二趟,就有8对数据进行比较。如果一趟有n个数据就要比较n - 1对数据。

3.所以需要两层循环,外层控制趟数,内层控制每一趟要比较的次数。

看如下实现。

int main()
{
	int len[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		scanf("%d",&len[i]);
	}
	int n = sizeof(len) / sizeof(len[0]);
	//printf("%d", n);
	int x = 0;
	for (x = 0; x < n; x++)
	{
		int y = 0;
		{
			for (y = 0; y < n-1 - x; y++)
			{
				if (len[y] > len[y + 1])
				{
					int temp = 0;
					temp = len[y];
					len[y] = len[y + 1];
					len[y + 1] = temp;//交换
				}
			}
		}
	}
	
	int j = 0;
	for (j = 0; j < 10; j++)
	{
		printf(" %d ", len[j]);
	}
}

运行结果如图,大家也可以用源码自己体验一下效果: 

 

4.3冒泡排序封装为函数

现在已经实现了冒泡排序,为了以后的方便,我们来试着将这个冒泡排序模块功能封装为一个函数。让我们一起来看看自定义为函数有那些坑。

   按照上述排序规则和封装函数的方法,我们将数组作为一个函数参数,传递给冒泡排序函数来执行冒泡排序,我们来看一下: 

void BBurd_sort(int arr[10])
{
	int n= sizeof(arr) / sizeof(arr[0]);
	int x = 0;
	for (x = 0; x < n; x++)
	{
		int y = 0;
		{
			for (y = 0; y < n - 1 - x; y++)
			{
				if (arr[y] > arr[y + 1])
				{
					int temp = 0;
					temp = arr[y];
					arr[y] = arr[y + 1];
					arr[y + 1] = temp;//交换
				}
			}
		}
	}

}
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		scanf("%d", &arr[i]);
	}
	//int sz = sizeof(arr) / sizeof(arr[0]);
	BBurd_sort(arr);
	int j = 0;
	for (j = 0; j < 10; j++)
	{
		printf(" %d ", arr[j]);
	}
	return 0;
}

我们观察到没有发生排序,我们调试看一下:

问题所在:我们这里想要实现的功能是n 保存的是数组的大小,那么应该是10才对,为什么是2呢。

思考:sizeof(arr[0] 求的是一个整形数组元素的大小那么其大小就应该是4字节,

那么说明我们的sizeof(arr)就是8字节,那么只有可能我们这里的arr,并不是一整个数组,而是一个地址 (地址也就是指针,在32位操作系统下面大小是4个字节,64位操作系统下是8个字节),那就说明我们这里的arr实质上是一个指针,保存的是数组首元素的地址。

所以在函数里面是没有办法求出这个数组的大小,解决办法,把函数的大小作为一个函数的参数一起传递,看一下改进效果。

程序正常实现,附上源码。大家体验一下效果。 

void BBurd_sort(int arr[10], int n)
{
	int x = 0;
		for (x = 0; x < n; x++)
		{
			int y = 0;
			{
				for (y = 0; y < n-1 - x; y++)
				{
					if (arr[y] > arr[y + 1])
					{
						int temp = 0;
						temp = arr[y];
						arr[y] = arr[y + 1];
						arr[y + 1] = temp;//交换
					}
				}
			}
		}

}
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		scanf("%d", &arr[i]);
	}
	int sz = sizeof(arr) / sizeof(arr[0]);
	BBurd_sort(arr, sz);
		int j = 0;
	for (j = 0; j < 10; j++)
	{
		printf(" %d ", arr[j]);
	}
	return 0;
}

4.4 冒泡排序代码效率改进。 

思考:

对于这样一个有序数列,我们如果执行的话,我们的自定义函数就还是会跑一遍,那么这样实际效率并不高。所以想能不能改进一下我我们的代码。

再次浏览原理图,我们不难发现这样一个规律,只要数据是乱序一趟下来就会有元素在发生交换。

比如:

9 1 2 3 4 5 6 7 8

这样的数据,第一趟会有元素交换,第二趟就没有。

那么我们可以这样,只要某一趟没有交换,说明此时我们的数列已经有序了就跳出

只要发生交换就继续。

那么看一下改进代码: 

void BBurd_sort(int arr[10], int n)
{
	int x = 0;
		for (x = 0; x < n; x++)
		{
			int flag = 0;
			int y = 0;
			
				for (y = 0; y < n-1 - x; y++)
				{
					if (arr[y] > arr[y + 1])
					{
						int temp = 0;
						temp = arr[y];
						arr[y] = arr[y + 1];
						arr[y + 1] = temp;//交换
						flag = 1;//发生了交换就改变flag
					}
				}
				//执行完一趟就判断一下有没有交换,有交换就继续,没有交换就直接退出,不用排序了
				if (flag == 0)
				{
					break;
				}
			
		}

}
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		scanf("%d", &arr[i]);
	}
	int sz = sizeof(arr) / sizeof(arr[0]);
	BBurd_sort(arr, sz);
		int j = 0;
	for (j = 0; j < 10; j++)
	{
		printf(" %d ", arr[j]);
	}
	return 0;
}

5.堆排序

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

使用堆结构对一组数据进行排序,方便对数据进行处理。粗暴办法就是将原数组数据插入堆,再取堆数据覆盖,这种方法首先得有堆结构,其次插入数据就要额外开辟空间。

最好的方式就是直接将原数组或者原来的这组数据变成堆。将原数组直接看成一颗完全二叉树,一般都不是堆。那么就要将原数据之间调成堆----建堆

建堆不是插入数据,只是使用向上调整的思想。在原有数据上进行更改,调换。

5.1升序建大堆,降序建小堆

一般我们要利用堆结构将一组数据排成升序,就建立大堆

要利用堆结构将一组数据排成降序,就建立小堆。

void HeapSort(int* a, int n)
{
	//对数据进行建堆
	for (int i = 0; i < n; i++)
	{
		AdjustUp(a, 1);
	}
	//堆排序---向下调整的思想
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;//让n-1个数据调整成堆选出次小
	}

}

 5.2 建堆的时间复杂度

5.2.1 向下调整建堆

讨论最坏的时间复杂度

将下图进行建立一个大堆

实现:

void AdjustDown(int* a, int parent, int n)
{
	int child = parent * 2 + 1;
	while (child < n)
	{

		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;//找出左右孩子中大的一个1
		}
		if (a[child] >a[parent])
		{
			//交换
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;


		}
		else
		{
			break;
		}
	}

	

}
//堆排序

//首先将数据建堆
	//升序建大堆,降序建小堆
	//采用向下调整建堆的方式时间复杂度较小
	int child = n - 1;
	int parent = (child - 1) / 2;
	while (parent >=0)
	{
		AdjustDown(a, parent, n);
		parent--;
	}
	
	

假设有h层,这里测算的时间复杂度是最坏的情况,那么就相当于调整的是一个满二叉树的堆。

第h层  ,共有2^(h-1)个节点, 需要调整0次

第h-1层,共有2^(h-2)个节点 ,每个节点调整一次,调整:2^(h-2)*1次

第h-2层,共有2^(h-3)个节点,每个节点最坏向下调整两次,调整2^(h-3)*2次

        :

        :

        :

第3层,共有2^(2)个节点,每个节点向下调整h-3次,调整2^(2)*(h-3)次

第2层,共有2^(1)个节点,每个节点向下调整h-2次,调整2^(1)*(h-2)次

第1层,共有2^(0)个节点,每个节点向下调整h-1次,调整2^(0)*(h-1)次

时间复杂度为:

T(h) = 2^(0)*(h-1)+2^(1)*(h-2)+2^(2)*(h-3)+…………2^(h-3)*2+2^(h-2)*1①

2T(h) =  2^(1)*(h-1)+2^(2)*(h-2)+2^(3)*(h-3)+…………2^(h-2)*2+2^(h-1)*1

②-①得

T(h) = 2^(1)+2^(2)……+2^(h-2)+2^(h-1)+1-h

=2^0+2^(1)+2^(2)……+2^(h-2)+2^(h-1)-h

=2^h-1-h

由于h是树的层高,与节点个数的关系是:N = 2^h-1

h = log(n+1)

所以时间复杂度为:

O(N) = N-longN+1~O(N)

5.2.2向上调整建堆

假设有h层,这里测算的时间复杂度是最坏的情况,那么就相当于调整的是一个满二叉树的堆。

第2层,共有2^1个节点,每个节点向上调整1次

 第3层,共有2^2个节点,每个节点向上调整2次

        

        :

        :

        :

第h-2层,共有2^(h-3)个节点,每个节点向上调整h-3次

第h-1层,共有2^(h-2)个节点,每个节点向上调整h-2次

第h层  ,共有2^(h-1)个节点,每个节点向上调整h-1次

时间复杂度为:T(h) = 2^1*1+2^2*2+2^3*3……2^(h-3)*(h-3)+2^(h-2)*(h-2)+2^(h-1)*(h-1)

                       2 T(h) = 2^2*1+2^3*2+2^4*3……2^(h-2)*(h-3)+2^(h-1)*(h-2)+2^(h)*(h-1)

T(h) = -2-(2^2+2^3+2^4…………2^(h-1))+2^h(h-1)

= -(2^0+2^1+2^2…………2^(h-1))+2^h(h-1)+2^0

=2^h*(h-2)+2

由于h是树的层高,与节点个数的关系是:N = 2^h-1

h = log(n+1)

T(N) = (N+1)log(N+1)-2N-1

5.3 排序实现

排序和删除的原理是一样的,先找最大/最小然后次大/次小,每次选取数据后不会将后面数据覆盖上来,否则就会导致关系全乱,可能次大数据就要重新建堆,增加了工作量了。而是通过堆顶元素和最后一个数据交换位置过后,向下调整思想,将排除刚刚调整的尾部最大数据除外的剩下数据看成堆,循环排序。

最后发现:大堆这样处理的数据最大的数据在最后,最小的在最前,小堆相反。 

void AdjustDown(int* a, int parent, int n)
{
	int child = parent * 2 + 1;
	while (child < n)
	{

		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;//找出左右孩子中大的一个1
		}
		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 child = n - 1;
	int parent = (child - 1) / 2;
	while (parent >=0)
	{
		AdjustDown(a, parent, n);
		parent--;
	}
	
	
	//排序
	int end = n - 1;
	while (end >= 0)
	{

		Swap(&a[0], &a[end]);

		AdjustDown(a, 0, end);
		end--;
	}
	
	
}

6.选择排序 

6.1基本思想:

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

6.2算法演示

6.3代码实现 

//选择排序

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

	
	while (begin <end)
	{
		int min = begin;
		int max = begin;
		int i = 0;
		for (i = begin+1; i <= end; i++)
		{
			if (a[min] > a[i])
			{
				min = i;
				
				
			}
			if (a[max] < a[i])
			{
				max = i;
				
			}
		}
		Swap(&a[begin], &a[min]);
		
		if (begin == max)
		{
			max = min;
		}
		Swap(&a[end], &a[max]);
		end--;
		begin++;

	}
	


}

6.4时间复杂度分析

. 时间复杂度: O(N^2)
如果有10个数据: 10 9 8 7 6 5 4 3 2 1
第一次:比较2(n-1次)
第二次比较:2(n-3)次
最后一次比较:1次
总共比较:n/2次,时间复杂度为:1/4n^2
3. 空间复杂度:O(1)
4. 稳定性:不稳定

7.快速排序

算法思想:

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

7.1 hoare版本

单趟:

7.1.1算法图解

为什么相遇的位置一定是比k值小的值是因为右边先走,那么每次左边停下来的值经过交换一定是比k值大,此时r继续走,找到比k值大的数才会停下来,所以如果右边先走,停下来的地方值一定小于k,如果左边先走,停下来的地方的值一定大于k。 

将上述过程递归就可以得到一个有序的序列:

7.1.2代码实现 

int Partsort(int* a, int left, int right)
{


	int keyi = left;

	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;//如果左右两边同时是key,此时如果不取等号就回陷入死循环
			//再次判断left<key是因为值都比k大或者都比k小的极端情况出现越界问题,因为外层循环只判断了一次,里面的--++的循环没有判断
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

//快速排序
void QuickSort(int* a, int left,int right)
{
	if (left >= right)
	{
		//大于为空,等于只有一个值
		return;
	}
	
	int keyi = Partsort(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

7.1.3 算法优化---三数取中

为了保证我们快排的效率呢,我们试着调整一下我们选择key值的策略。

每次选取三个数中中间值的一个作为我们的key,并且将这个值换到数组的最左边:

int GetMid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mid > a[right]])
		{
			return mid;
		}
		else if (a[left] < a[right])//mid是最小的
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
int Partsort(int* a, int left, int right)
{
	int mid = GetMid(a, left, right);
	Swap(&a[left], &a[mid]);


	int keyi = left;

	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;//如果左右两边同时是key,此时如果不取等号就回陷入死循环
			//再次判断left<key是因为值都比k大或者都比k小的极端情况出现越界问题,因为外层循环只判断了一次,里面的--++的循环没有判断
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

//快速排序
void QuickSort(int* a, int left,int right)
{
	if (left >= right)
	{
		//大于为空,等于只有一个值
		return;
	}
	
	int keyi = Partsort(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

这样我们的逆序排序这种对于快排最坏的情况就变成最好的情况,十万个数据1毫秒就可以排好。

7.2挖坑法快排 

7.2.1算法图解

7.2.2 代码实现


//挖坑法
int Partsort2(int* a, int left, int right)
{

	int mid = GetMid(a, left, right);
	Swap(&a[left], &a[mid]);
	int ken = a[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= ken)
		{
			right--;
			

		}
		a[hole] = a[right];
		hole = right;
		while (left < right && a[left] <= a[ken])
		{
			left++;
		}
		a[hole] = a[left];
		hole= left;

	}
	a[hole] = ken;
	return hole;
	
}

7.3 前后指针法

逻辑:cur找比key值小的值,然后prev遇到比key值大的就停下来,直到cur遇到比key值小的值过后就交换。

prev:

在cur没有遇到比key值大的值的时候,prev紧紧的跟着cur

在cur遇到比key值大的值的时候,prev就停下来了,prev在比key大的一组值的前面。

//前后指针
int Partsort3(int* a, int left, int right)
{

	int mid = GetMid(a, left, right);
	Swap(&a[left], &a[mid]);
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi]&&prev++ != cur)//防止开始位置自己跟自己交换
		{
			Swap(&a[prev++], &a[cur]);
		}
		cur++;
		
	}
	Swap(&a[prev], &a[keyi]);
		return prev;



}

 7.4快速排序进一步优化

按照上面的方法实际主题来说,大思想就是递归,递归是有消耗的,而且,最后几层的递归往往是最大的,如果当递归区间的数值减少到一定程度,我们就不递归了,特别是到最后一两层的时候,那么排序的效率就会有提升。

//快速排序
void QuickSort(int* a, int left,int right)
{
	if (left >= right)
	{
		//大于为空,等于只有一个值
		return;
	}
	
	if ((right - left + 1) > 10)
	{
		int keyi = Partsort(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
	else
	{
		InserPort(a + left, right - left + 1);
	}
}

小区间优化,编译器对递归优化也比较好,小区间不在递归,降低递归次数

7.5快速排序非递归写法

使用栈保存每次划分的区间

void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
while (StackEmpty(&st) != 0)
{
 right = StackTop(&st);
 StackPop(&st);
 left = StackTop(&st);
 StackPop(&st);
 
 if(right - left <= 1)
 continue;
 int div = PartSort1(a, left, right);
 // 以基准值为分割点,形成左右两部分:[left, div) 和 [div+1, right)
 StackPush(&st, div+1);
 StackPush(&st, right);
 
 StackPush(&st, left);
 StackPush(&st, div);
}
 
 StackDestroy(&s);
}
快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定

 

8.归并排序

8.1排序思想:

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

8.2 算法图解 

void _MergeSort(int* a,  int* tmp,int begin,int end)
{
	if (end <= begin)
	{//递归到只有一个或者没有就返回
		return;
	}
	//分割
	int mid = (end +begin) / 2;
	//没有那么均分也不是很影响
	//然后对左右区间进行递归操作 左区间:[begin mid] [mid+1,end}
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);
	//归并到tmp数组里面,再拷贝回原数组
	int begin1 = begin, end1 = mid;//左区间
	int begin2 = mid + 1, end2 = end;//右区间
	int index = begin;//tmo数组中的对应位置
	while (begin <= end1 && begin2 <= end2)//需要同时将左右两边小的拿出来插入到tmp数组
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++ ];//往后继续找,直到一个区间遍历完

		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
	//出循环,有一边区间肯定遍历完了,但是不确定是那个区间
	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];//这种情况是左边区间没有遍历完,将后续数据放在这个区间的后面
	}
	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];//这种情况是右边区间没有遍历完,将后续数据放在这个区间的后面
	}
	//将tmp数组归并回原数组的对应位置,所以不一定是从0开始拷贝的
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
	//a+begin  tmp+begin表示当前区间的起始位置
	//end -begin+1 
}
//归并排序
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	//为归并区间开辟存储数据的数组,空间复杂度产生于此
	if (tmp == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	//区间分割
	//递归不能每次都开辟空间,所以这里分离出子函数
	_MergeSort(a,tmp, 0, n - 1);
	free(tmp);
	tmp = NULL;
}

8.3 归并排序非递归实现 

排序思想:

//归并排序非递归版本
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	//为归并区间开辟存储数据的数组,空间复杂度产生于此
	if (tmp == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	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;
			//[begin1,end][begin2,end2]归并
			int index = i;//tmo数组中的对应位置
			while (begin1 <= end1 && begin2 <= end2)//需要同时将左右两边小的拿出来插入到tmp数组
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];//往后继续找,直到一个区间遍历完

				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}
			//出循环,有一边区间肯定遍历完了,但是不确定是那个区间
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];//这种情况是左边区间没有遍历完,将后续数据放在这个区间的后面
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];//这种情况是右边区间没有遍历完,将后续数据放在这个区间的后面
			}
			//将tmp数组归并回原数组的对应位置,所以不一定是从0开始拷贝的
			memcpy(a + i, tmp + i, (2 * gap) * sizeof(int));
			//a+begin  tmp+begin表示当前区间的起始位置
			//end -begin+1 

		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
} 

这个代码当数据为偶数个的时候就会越界

 奇数个二二归的时候也会越界,需要修正。

①右边区间的右边界出界

②右边区间的右边界出界

③左区间的 右边界出界,

那么就要处理这种情况,越界,如果是左区间的右边界或者右区间的左边界出界了,那么越界的部分就不归并了,直接结束。如果是右区间的有边界越界,那么前面的数据都没有越界。只需要把区间调整回来。最后每次归并完拷贝回原数组的时候,数据不是2*gap个了,而是

end2-i+1个。

//归并排序非递归版本
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	//为归并区间开辟存储数据的数组,空间复杂度产生于此
	if (tmp == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	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;
			//[begin1,end][begin2,end2]归并
			//printf("[%d ,%d][%d,%d] ", begin1, end1, begin2, end2);
		
			if (end1 >= n || begin2 >= n)
			{
				break;//防止越界,同时也避免了第三种情况
			}
			if (end2 >= n)
			{
				//前面都没有越,把区间调整回来
				end2 = n - 1;
			}
			printf("[%d ,%d][%d,%d] ", begin1, end1, begin2, end2);
			int index = i;//tmo数组中的对应位置
			while (begin1 <= end1 && begin2 <= end2)//需要同时将左右两边小的拿出来插入到tmp数组
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];//往后继续找,直到一个区间遍历完

				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}
			//出循环,有一边区间肯定遍历完了,但是不确定是那个区间
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];//这种情况是左边区间没有遍历完,将后续数据放在这个区间的后面
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];//这种情况是右边区间没有遍历完,将后续数据放在这个区间的后面
			}
			//将tmp数组归并回原数组的对应位置,所以不一定是从0开始拷贝的
			memcpy(a + i, tmp + i, (end2-i+1) * sizeof(int));//修正了就没有2倍gap个了
			//a+begin  tmp+begin表示当前区间的起始位置
			//end -begin+1 

		}
		printf("\n");
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
} 

 

 

8.4归并排序的特性总结:

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

9.计数排序

9.1算法思想:

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

9.2 算法演示:

9.1.1 绝对映射

我们有n个数据就开n个空间

9.1.2 相对映射 

但是如果我们的数是100~ 200实际只有100个数,如果按照绝对映射的办法就要开辟200个数据空间就会造成浪费:解决办法就是:

大数-小数+1得到空间范围

然后所有数据都减去最小值再放到对应位置,拷贝回原数组的时候再加上Min就好。

 

代码:

 

//计数排序
void MergeSort(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];
		}
	}
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}
	memset(count, 0, sizeof(int) * range);
	//统计数据出现的个数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	//排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

9.3计数排序的特性总结:

  • 1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。只适用于整型。
  • 2. 时间复杂度:O(MAX(N,范围))
  • 3. 空间复杂度:O(范围)
  • 4. 稳定性:稳定

10.结语

以上就是本期所有内容,耗时半周,应该整理得比较全面,越往后面的排序代码实现就越难。知识含量蛮多,大家可以配合解释和原码运行理解。创作不易,大家如果觉得还可以的话,欢迎大家三连,有问题的地方欢迎大家指正,一起交流学习,一起成长,我是Nicn,正在c++方向前行的奋斗者,数据结构内容持续更新中,感谢大家的关注与喜欢。还有基数排序 桶排序

  • 44
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值