数据结构与算法初阶9:常见排序算法实现知识精讲

        今天这一讲主要为大家详细介绍《数据结构与算法》中常见的排序算法,这些排序算法在今后的学习中起着至关重要的的作用,下面跟着博主一起学习吧!!! 

目录

1、排序的基本概念及分类

1.1 基本概念

1.2 分类

2、插入排序算法实现

2.1 直接插入排序

2.1.1 基本思想

2.1.2 实现思路

 2.1.3 特性总结

2.2 希尔排序

2.2.1 基本思想

2.2.2 实现思路

2.2.3 特性总结

3、选择排序算法实现

3.1 直接选择排序

3.1.1基本思想

3.1.2实现思路

3.1.3 特性总结

3.2 堆排序

3.2.1 基本思想及实现思路  

3.2.2 特性总结

4、交换排序

4.1 冒泡排序

4.1.1 基本思想及实现思路  

4.1.2 特性总结

4.2 快速排序

4.2.1 hoare方法

4.2.2 挖坑法

4.2.3 前后指针法

4.2.4 快速排序综合优化方法

4.2.5 快速排序递归修改非递归方式

4.2.6 特性总结

5、归并排序

5.1 基本思想

 5.2 具体实现思路

5.2.1 递归实现

5.2.2 非递归实现(循环方式实现)

 5.3 特性总结

6、非比较排序(补充)

6.1 基本思想

6.2 实现思路

6.3 特性总结

7、总结


1、排序的基本概念及分类

1.1 基本概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求在内外存之间移动数据的排序。

1.2 分类

2、插入排序算法实现

2.1 直接插入排序

2.1.1 基本思想

        直接插入排序是一种简单的插入排序思想:把待排序的记录其按照其关键码值的大小逐个插入到一个已经拍好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。(可以从平时玩的扑克牌时依照顺序调整牌面的方式为例参考着理解)。

2.1.2 实现思路

直接插入排序实现思路

实现思路说明:

        当插入第i(i>=1)个元素时,前面的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++)
	{
		//[0,end]有序,把end+1的位置的值插入,保持有序
		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;
	}
	
}

 2.1.3 特性总结

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

2.2 希尔排序

2.2.1 基本思想

        先选定一个整数gap,然后以该数作为间隔,将待排序中相同间隔的数据设置在一个组,并对该组内的元素进行排序,假设数据可以分为n个组,则分别对各个组内的元素先排序,这称之为第一趟排序,后面缩小gap的值,然后采用同样的方法,细化分组,再排序,最后令gap=1,进行最后一次排序,此时的排序就是直接插入排序希尔排序也叫做缩小增量排序。

 注意:从上述实例中可以看出,希尔排序的缩小增量特性非常明显,在不同间隔的排序中,,所采用的思路都是插入排序思想。(预排序+排序思想)。

2.2.2 实现思路

//希尔排序改进
void ShellSort(int* a, int n)
{

	int gap = n;
	while (gap > 1)
	{
		//利用下述实现预排序和排序
		gap = gap / 3 + 1;//当gap等于1就是直接插入排序,gap>1时是预排序
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
	
}

2.2.3 特性总结

  • 1、希尔排序适用于数据量较大的情况,其是对直接插入排序的优化;
  • 2、当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
  • 3、希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,一般根据提供的书籍中的参考资料而言,时间复杂度一般取O(N^1.3)。
  • 4、稳定性:不稳定。

2.2 4 直接插入排序和希尔排序算法比较

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<time.h>
#include<string.h>

void TestOP()
{
	srand(time(0));
    //生成的随机数的个数
	const int N = 10000;
	int* a1 = (int*)malloc(sizeof(int)*N);
	int* a2 = (int*)malloc(sizeof(int)*N);

	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand(); //N个随机数
		a2[i] = a1[i];
	}

	int begin1 = clock(); //记录程序运行到这里的时间
	InsertSort(a1, N);
	int end1 = clock();    //记录程序运行到这里的时间

	int begin2 = clock();    //记录程序运行到这里的时间
	ShellSort(a2, N);
	int end2 = clock();    //记录程序运行到这里的时间

    //比较排序同样的数据运行的时间
	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);

	free(a1);
	free(a2);
}

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

        从上图比较中可以看出:直接插入排序在待排序数据量比较少的情况下,优势是很明显的,但当待排序的数据量较大时,希尔排序的优势就非常明显了,极大的缩短了待排序的时间,比如上图中当待排序数据是10万个时,直接插入排序需要7秒,而希尔排序只需要0.02秒左右,所以选择希尔还是直接插入排序可以根据实际情况判断比较。

3、选择排序算法实现

3.1 直接选择排序

3.1.1基本思想

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

3.1.2实现思路

 实现思路说明:

  • 1、在元素集合array[i]—array[n-1]中选择最大(小)的数据元素;
  • 2、若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换;
  • 3、在剩余的array[i]—array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素;

        简而言之就是每次遍历数组把最大的元素放最右边,最小的元素放最左边,然后缩短遍历数组长度,按照同样的方式接着遍历(以排升序为例)。

程序代码实现:

void Swap(int*p1, int*p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;

}

//直接选择排序:同时比较最大和最小,将最大的放右边,最小的放左边
//时间复杂度最好:O(N^2),最坏:O(N^2)
void SelectSort(int*a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin<end)
	{
		//用下标记录最小值和最大值
		int min = begin;
		int max = begin;
		for (int i = begin+1; i <=end; i++)
		{
			if (a[i] < a[min])
				min = i;
			if (a[i] > a[max])
				max = i;
		}
		Swap(&a[begin], &a[min]);

		//如果begin和max重叠
        //(也就是begin里面包含有最大值时,经过前面begin与min交换,begin中此时包含了最小值,
        //此时min下标中包含有最大值),所以需要修正一下max的位置
		if (begin == max)
		{
			max = min;
		}
		Swap(&a[end], &a[max]);
		--end;
		++begin;
	}

}

3.1.3 特性总结

  • 1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用;
  • 2. 时间复杂度:O(N^2),最好的情况也就是数据本身就是有序的情况下,时间复杂度同样是O(N^2)。
  • 3. 空间复杂度:O(1);
  • 4. 稳定性:不稳定;

3.2 堆排序

3.2.1 基本思想及实现思路  

堆排序的思想及使用方法在上第7讲中为大家详细讲述过,这里就不再赘述,需要特别注意一点就是:排升序是需要建大堆;排降序是需要建小堆。数据结构与算法初阶7:基于二叉树的堆和堆排序知识精讲_King_lm_Guard的博客-CSDN博客https://blog.csdn.net/King_lm_Guard/article/details/125906250

3.2.2 特性总结

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

4、交换排序

        基本思想:所谓交换,就是根据序列中两个记录数值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将数值较大的记录向序列的尾部移动,数值较小的记录向序列的前部移动。(这里以排升序为例)

4.1 冒泡排序

4.1.1 基本思想及实现思路  

        冒泡排序是我们学习C语言最早接触的一种排序算法,其思想就是每一趟排序将最大的数放到最后,然后重复进行该操作,可以参考如下动图。

程序代码实现:

//冒泡排序
//时间复杂度:O(N^2),最好的情况,即已经是有序的,但任然需要遍历一遍:0(N)
void BubbleSort(int*a, int n)
{
	assert(a);
	for (int i = 0; i < n - 1; i++)
	{
		int exchang = 0; //用来记录数组中的元素是否已经有序,如果有序则直接退出,提高排序效率
		//一趟冒泡排序
		for (int j = 0; j < n - i-1; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], a[j + 1]);
				exchang = 1;
			}
		}
		if (exchang == 0)
		{
			break;
		}
	}
}

4.1.2 特性总结

  • 1. 冒泡排序是一种非常容易理解的排序;
  • 2. 时间复杂度:O(N^2) ,最好的情况下时间复杂度为O(N);
  • 3. 空间复杂度:O(1);
  • 4. 稳定性:稳定;

4.2 快速排序

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

4.2.1 hoare方法

1、实现思路

        选出一个基准值key,在具体实验过程中,一般是最左边或者最右边的值;单趟排完以后,保证左边的所有值比key小,右边的值比key大(以升序为例),然后在左半部分中选取基准值,采用同样的方式将左半部分分为左子半部分和右子半部分,按照此方式,采用递归思想,先排左半部分,在排右半部分。(其实这里的排序思路可以参考着二叉树中的前序遍历规则,前序遍历是先根结点,然后遍历左子树,再遍历右子树),详细实现思路如下:​​​​​​​ 

2、代码实现思路 

//快速排序[begin,end]
//霍尔排序
void QuickSort(int*a, int begin,int end)
{
	if (begin >= end) //当区间不存在或者只有一个值时则不需要再处理
	{
		return;
	}
	int left = begin;
	int right = end;
	int keyi = left; //左边做key时,让右边先走
	while (left < right)
	{
		//右边先走,找小于keyi的值
		while (left < right && a[right] >= a[keyi]) //这里的left < right是为了防止越界
		{
			--right;
		}
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
	//分区间递归
	//[begin,keyi-1],keyi,[keyi+1,end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);

}

注意:

1、为了保证相遇位置时的值,比key小或者就是key的值,所以当左边做key时,要让右边先走。

2、假设R先走,R先停下来,然后L遇到R:此时相遇的位置就是R停下来的位置,R停的位置就是比key要小的位置。
3、假设R先走,R没有停下来(R没有找到比key小的值),然后R遇到L:此时相遇的位置就是L上一轮停下来的位置,L停的位置就是比key要小的位置或者就是key的位置。

4.2.2 挖坑法

1、实现思路

2、具体代码实现 

//挖坑法:
int PartSort(int*a, int begin, int end)
{
	if (begin >= end) //当区间不存在或者只有一个值时则不需要再处理
	{
		return;
	}
    int keyi = a[begin];
	int piti = begin;
	while (begin < end)
	{
		//右边找小,填到左边的坑piti里面去,然后这个位置为新的坑
		while (begin < end && a[end] >= keyi)
		{
			--end;
		}
		a[piti] = a[end];
		piti = end;
		//左边找大,填到右边的坑piti里面去,然后这个位置为新的坑
		while (begin < end && a[begin]<= keyi)
		{
			++begin;
		}
		a[piti] = a[begin];
		piti = begin;
	}
	//当左右相遇时
	a[piti] = keyi;
	keyi = piti;
	//分区间递归
	//[begin,keyi-1],keyi,[keyi+1,end]
	PartSortt(a, begin, keyi - 1);
	PartSort(a, keyi + 1, end);
}

4.2.3 前后指针法

1、实现思路

2、代码实现 

//前后指针版本1
//前后指针法
int PartSort(int*a, int begin, int end)
{
    if (begin >= end) //当区间不存在或者只有一个值时则不需要再处理
	{
		return;
	}
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	
	while (cur <= end)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	//当cur与prev相遇时
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	//[begin,keyi-1],keyi,[keyi+1,end]
	PartSortt(a, begin, keyi - 1);
	PartSort(a, keyi + 1, end);
}

//前后指针改进版本2

int GetMidIndex(int *a, int begin, int end)
{
    if (begin >= end) //当区间不存在或者只有一个值时则不需要再处理
	{
		return;
	}
	int midi = (begin + end) / 2;
	if (a[begin] < a[midi])
	{
		if (a[midi] < a[end])
		{
			return midi;
		}
		else if (a[begin] < a[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
	else
	{
		if (a[midi] > a[end])
		{
			return midi;
		}
		else if(a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}

//前后指针法:改进版本3
int PartSort(int*a, int begin, int end)
{
    if (begin >= end) //当区间不存在或者只有一个值时则不需要再处理
	{
		return;
	}
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	//加入三数取中的优化
	int midi = GetMidIndex(a, begin, end);
	Swap(&a[keyi], &a[midi]);

	while (cur <= end)
	{
		//当cur的值比keyi小,则cur和prev均向后走一步
		if (a[cur] < a[keyi] && ++prev != cur) //这里的!=是为了防止prev和cur相等时交换,此时的交换没有意义,但不写也可以的
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	//当cur与prev相遇时
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	//[begin,keyi-1],keyi,[keyi+1,end]
	PartSortt(a, begin, keyi - 1);
	PartSort(a, keyi + 1, end);
}

4.2.4 快速排序综合优化方法

//前后指针版本1
//前后指针法
int PartSort(int*a, int begin, int end)
{

	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	
	while (cur <= end)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	//当cur与prev相遇时
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
    return keyi;

}

//改进版本:快速排序[begin,end]
void QuickSort(int*a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if (end - begin > 10)
	{
		//利用前面的霍尔方法/挖坑法/前后指针法
		int keyi = PartSort(a, begin, end); //这里记得将前面的方法的返回值设置返回keyi,然后再这里递归,如提供的程序所示
		//[begin,keyi-1],keyi,[keyi+1,end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	else
	{
		//小区间优化

		//当数量比较小时,尽量少用递归,可用其他排序方式,如:插入排序,这样时间复杂度虽然没有降低太多,但可以降低空间复杂度,减少栈空间的使用
		//注意在用插入排序时,排序的首位置不一定就是数组首元素,而是a+begin;
		InsertSort(a + begin, end - begin + 1);
	}

}

4.2.5 快速排序递归修改非递归方式

//递归改非递归
//递归大问题,极端场景下,如果深度太深,会出现栈溢出
//1、直接改循环,比如斐波那契数列,归并排序
//2、用数据结构实现栈模拟递归过程

void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	//先将数组下标入栈
	StackPush(&st, end);
	StackPush(&st, begin);

	while (!StackEmpty(&st))
	{
		//取出栈中元素也就是记录数组元素的下标位置
		int left = StackTop(&st);
		StackPop(&st);
		int right = StackTop(&st);
		StackPop(&st);

		//采用快速排序的方法进行单排,比如利用前后指针法
		int keyi = PartSort3(a, left, right);

		//单排一次后,将左右部分区间分别入栈
		//根据栈的后进先出的特点,先入右区间
		if (keyi+1< right )
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);
		}
		//根据栈的后进先出特点,后入左区间
		if (left  < keyi-1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, left);
		}
	}

	StackDestory(&st);

}

注意:这里利用了栈的后进先出特性代替递归方式,程序中用到的栈相关函数请移步博主下面这一讲:​​​​​​​ 数据结构与算法初阶5:栈和队列知识精讲_King_lm_Guard的博客-CSDN博客https://blog.csdn.net/King_lm_Guard/article/details/125738989

4.2.6 特性总结

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序;
2. 时间复杂度:O(N*logN);
3. 空间复杂度:O(logN);(在递归和非递归中,会产生压栈的行为)
4. 稳定性:不稳定;

5、归并排序

5.1 基本思想

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

        在程序实现时,需要注意:归并排序需要额外开辟相同大小的数组空间,作为临时存放已经排好序的子区间,然后将每一段子区间拷贝回原来的数组中,从而实现原数组子区间有序,通过分治递归思想,最终实现整个数组的有序。

 5.2 具体实现思路

5.2.1 递归实现

//归并排序(递归方式)
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	//递归结束标志
	if (begin >= end)
	{
		return;
	}

	//分治思想
	int mid = (end + begin) / 2;

	//[begin,mid][mid+1,end]分治递归,分别让子区间有序
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

	//递归彻底以后开始排序
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	//注意i不可从0开始,因为是在递归过程中,所以需要通过begin1作为起始
	int i = begin1;
	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++];
	}
	//当数组a中的数据全部在tmp中有序时,将tmp中的数据拷贝到a中
	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)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//调用递归
	_MergeSort(a, 0, n - 1, tmp);

	//开辟空间要记得释放
	free(tmp);
}

实现逻辑:

5.2.2 非递归实现(循环方式实现)

1、循环方式实现1

        实现思路:不同于快速排序可以采用栈来实现排序,因为当我们从栈中拿去部分区间的数据时,从栈中取数据排好序,此时我们必须把刚取得的数据在栈中销毁,才能取下一子区间的数据,然后排序,但因为归并排序是需要两个子区间相互比较,不同于快速排序中采用一个基准,只要保证基准值的左边比基准值小,右边比基准值大就可以,在归并排序中,已经排好序的两段子区间之间还需要相互排序,但因为栈的特性,我们不能两段子区间的值都能拿到,因为有一段已经被销毁了,所以在这里,我们只能考虑采用循环的方式非递归实现归并排序。

//归并排序:采用循环的方式表示
//方法1
void MergeSortNonR_1(int*a, int n)
{
	//动态开辟空间
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//利用循环方式实现:即以间距为1比较,然后以间距为2比较,然后以间距为4比较,然后以间距为8比较,以此这样
	int gap = 1;
	while (gap < n)
	{
		printf("gap=%d-> ", gap);
		for (int i = 0; i < n; i += 2 * gap)
		{
			//划分区间思想:[i,i+gap-1][i+gap,i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			//利用循环的方式会存在越界情况,需要判断边界条件
			//越界-边界修正
			if (end1 >= n)
			{
				end1 = n - 1;
				//[begin2,end2]修改为不存在的区间,这样就不会进入下面的while循环
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				//[begin2,end2]修改为不存在的区间,这样就不会进入下面的while循环
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			printf("[%d,%d][%d,%d]->", begin1, end1, begin2, end2);

			int j = begin1;
			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++];
			}

		}
		printf("\n");
		//当数组a中的数据全部在tmp中有序时,将tmp中的数据拷贝到a中
		memcpy(a, tmp, n * sizeof(int));
		gap *= 2;

	}

	//开辟空间要记得释放
	free(tmp);
}

注意:

 2、循环方式实现2

//归并排序:采用循环的方式表示
//方法2
void MergeSortNonR_2(int*a, int n)
{
	//动态开辟空间
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//利用循环方式实现:即以间距为1比较,然后以间距为2比较,然后以间距为4比较,然后以间距为8比较,以此这样
	int gap = 1;
	
	while (gap < n)
	{
		printf("gap=%d-> ", gap);
		for (int i = 0; i < n; i += 2 * gap)
		{
			//划分区间思想:[i,i+gap-1][i+gap,i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			//利用循环的方式会存在越界情况,需要判断边界条件
			//越界-边界修正
			if (end1 >= n || begin2>=n)
			{
				break;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			printf("[%d,%d][%d,%d]->", begin1, end1, begin2, end2);
			int m = end2 - begin1 + 1;
			int j = begin1;
			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++];
			}
			//当数组a中的数据全部在tmp中有序时,将tmp中的数据拷贝到a中
			memcpy(a+i, tmp+i, m* sizeof(int));
		}
		printf("\n");

		gap *= 2;

	}

	//开辟空间要记得释放
	free(tmp);
}

3、循环方式1和方式2比较

 5.3 特性总结

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

6、非比较排序(补充)

6.1 基本思想

        计数排序又叫做鸽巢原理,是对哈希直接定址法的变形应用。但也有一定的局限性:该方法一般只针对int类型的数据,对浮点数和字符串就不适合;另外就是如果数据范围很大,会导致空间复杂度很高,同时也可能会造成空间的浪费,相对不适合。

6.2 实现思路

1、统计相同元素出现的次数;

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

 程序实现:

//计数排序

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

	//开辟统计次数的数组
	//确定数据分布范围range
	int range = max - min + 1;

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

	//将开辟的数组内容先置为0;
	memset(count, 0, sizeof(int)*range);

	//统计每个数字在数组中出现的次数
	for (int i = 0; i < n; i++)
	{
		//记录数字出现次数,出现一次加1次
		count[a[i]-min]++;
	}

	//回写-排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		//出现几次就回写几个i+min
		//conut[i]表示对应的相应待排序数字出现的次数
		while (count[i]--)
		{
			//恢复待排序的数字,并按照出现次数填入原数组中
			a[j++] = i + min;
		}
	}
}

6.3 特性总结

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

7、总结

排序算法时间复杂度(最坏)时间复杂度(最好)空间复杂度稳定性
插入排序直接插入排序O(N^2)O(N)O(1)稳定
希尔排序平均O(N^1.3)O(1)不稳定
选择排序选择排序O(N^2)O(N^2)O(1)不稳定
堆排序O(N*logN)O(N*logN)O(1)不稳定
快速排序冒泡排序O(N^2)O(N)O(1)稳定
快速排序O(N*2)O(N*logN)O(logN)不稳定
归并排序归并排序O(N*logN)O(N*logN)O(N)稳定
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值