[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序

前言

作者小蜗牛向前冲

名言我可以接受失败,但我不能接受放弃

[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第1张图片

  如果觉的博主的文章还不错的话,还请点赞,收藏,关注支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。

目录

 一 归并排序

1 归并排序的递归形式

 2 非递归实现归并排序

3 归并排序的总结

二 非比较排序 

三 排序大总结  


这里我们将继续为大家分享归并排序和非比较排序,我相信大家在上期中对冒泡排序和快速排序有了比较深刻的理解。

 一 归并排序

基本思想:

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

[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第2张图片

 归并排序动态演示

1 归并排序的递归形式

[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第3张图片

对于归并来说,其实就是将二个有序序列和并未一个有序序列,如果说快速排序像是二叉树的前序遍历的话,归并排序就像二叉树的后序遍历。归并排序的思路就是不断取小数尾插就可以了,这里我们就要用到递归的思想了,要不断将要排序的序列分为左右二个子区间,直到每个区间中只剩余一个元素(一个元素相当有序),就停止向下递归,开始返回归并(取小的尾插),注意这里我们要借助一个临时数组。

我们拿上面这个例子来理解一下,首先将序列分为左右二个区间,左区间(10 6 7 1),右区(3 9 4 2)

,我们在以左区间来举例,又可以分解为左右区间,左区间(10 6),右区间(7 1),直到分解为一个元素时就不在分解,取小的尾插到新数组tmp中,完后在拷贝回原数组,经过层层递归和返回,就可以形成二个有序序列(1 6 7 10)和(2 3 4 9)在归并就可以形成一个有序序列。

下面我们用代码实现他:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	//左右区间不可在分,递归停止
	if (begin >= end)
		return;
	//分区间[begin,mid].[mid+1,end]
	int mid = begin + (end - begin) / 2;
	//递归左区间
	_MergeSort(a,begin, mid, tmp);
	//递归右区间
	_MergeSort(a, mid + 1, end, tmp);
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1,end2 = end;
	//标记交换空间tmp的下标
	int i = begin;
	//归并二个有序序列,取小的尾插
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	//取大的数组,插入到tmp数组后
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	//将归并好的元素,拷贝到原数组中
	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");
			exit(-1);
	}
	//调用归并函数
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}

注意:

1 这里我们要注意在归并的时候区分好各个区间,因为我们还要将tmp中的元素拷贝回原数组,所以memcpy的传的目标指针应该是a+begin,源头指针是tmp+bgin。

2 我们在归并二组序列后,要记得把大的序列剩余的元素直接尾插到归并的新的序列中。

这里我们可以测试一下。

 2 非递归实现归并排序

上面我们用递归实现了归并排序,我们可以很容易的看出归并排序是一个完全二叉树,当数据为10亿的时候递归深度才30,也就是说归并排序几乎不会因为递归而栈溢出,但我们为了提高自己的思维能力,我们不妨试试用非递归实现归并排序。[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第4张图片

我们这里的非递归形式并不适合用栈来实现,为什么这么说呢?因为我们在进行序列归并时,区间的begin或者end可能使用多次,像斐波那契数列那样通过前面区间求后面区间。那么我们这里还是借用开辟一个动态的数组来解决。这么我们还是要回到递归中说的思路,在二个有序序列取小数尾插到新的序列中;所以我们有个变量gap来标明进行排序的每次组的元素个数,然后定义循环,让二组数据进行归并,归并完成后我们在跟新gap*=2;让其进行跟大的数组进行归并,直到gap

//非递归实现归并排序
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (NULL == tmp)
	{
		perror("malloc fail");
		return;
	}
	int gap = 1;//定义gap来标记要排序的组中有多少个元素
	while (gap < n)
	{
		//gap个数据进行归并
		for (int j = 0;j < n;j += 2 * gap)
		{
			//归并取小数进行归并
			int begin1 = j, end1 = j + gap - 1;
			int begin2 = j + gap, end2 = j + 2 * gap - 1;
			int i = j;
			//归并二个有序序列,取小的尾插
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[i++] = a[begin1++];
				}
				else
				{
					tmp[i++] = a[begin2++];
				}
			}
			//取大的数组,插入到tmp数组后
			while (begin1 <= end1)
			{
				tmp[i++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[i++] = a[begin2++];
			}
			//将归并好的元素,拷贝到原数组中
			memcpy(a + j, tmp + j, (end2 - j + 1) * sizeof(int));
		}
		gap *= 2;
	}
}

[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第5张图片

我们这种方式只适合归并数据个数为2^n,但数组数据个数不满足这个条件数组就会发生越界的情况:

[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第6张图片

当n为奇数的时候

[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第7张图片

 当n为偶数的时候

[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第8张图片

[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第9张图片

那么我们就要根据这些越界情况经行修正:

可能发生越界的三种情况:

1 第一组越界,这里我们只要考虑end1越界的情况,为什么这里说呢?因为begin1是不可能越界的(j

2 第二组全部越界,即是第二组的begin2越界了,也就是说要只剩余一组,这里我们不归并直接break返回.

3 第二组部分越界,即end2越界了,这时候我们只有修正一下end2就行了end2 = n-1;

这里我们根据越界的情况去修正我们的代码:

//非递归实现归并排序
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (NULL == tmp)
	{
		perror("malloc fail");
		return;
	}
	int gap = 1;//定义gap来标记要排序的组中有多少个元素
	while (gap < n)
	{
		//gap个数据进行归并
		for (int j = 0;j < n;j += 2 * gap)
		{
			//归并取小数进行归并
			int begin1 = j, end1 = j + gap - 1;
			int begin2 = j + gap, end2 = j + 2 * gap - 1;
			//第一组越界
			if (end1 >= n)
			{
				break;//直接返回
			}
			//第二组部分越界
			if (begin2 >= n)
			{
				break;//直接返回
			}
			//第二组部分越界
			if (end2 >= n)
			{
				//修正end2
				end2 = n - 1;
			}
			int i = j;
			//归并二个有序序列,取小的尾插
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[i++] = a[begin1++];
				}
				else
				{
					tmp[i++] = a[begin2++];
				}
			}
			//取大的数组,插入到tmp数组后
			while (begin1 <= end1)
			{
				tmp[i++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[i++] = a[begin2++];
			}
			//将归并好的元素,拷贝到原数组中
			memcpy(a + j, tmp + j, (end2 - j + 1) * sizeof(int));
		}
		gap *= 2;
	}
}

3 归并排序的总结

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

二 非比较排序 

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤

1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中 

 对于计数排序的思路来说,思路是非常简单的,我们采用相对映射的思路,首先遍历数组,找到最大值max和最小值min,开辟一个数组CountArr大小为max-min+1。然后在遍历一边原数组,但是我们这边要稍微处理一下,将原数组的值-min,这样求到的值就可以于CountArr的下标对于,统计好下标出现的次数。完成后,我们就可以根据下标进行排序:从CountArr中依次取出数据到原数组中,这里我们要注意加上min恢复数据。

[数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第10张图片

 思路图

 动图演示

代码实现:

// 时间复杂度:O(N+range)
// 空间复杂度:O(range)
// 适合数据范围集中,也就是range小
// 只适合整数,不适合浮点数、字符串等
void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	int i = 0;
	//找最大数和最小数
	for (i = 0;i < n;i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}
	int range = max - min + 1;//求出新数组要开辟的大小
	//这里我们要注意,一定要把开辟的数组都初始化为0
	int* CountArr = (int*)calloc(rang, sizeof(int));
	if (NULL == CountArr)
	{
		perror("calloc fail");
		exit(-1);
	}
	//将原数组中的值相对映射到CountArr中,并在各下标出现的次数

	for (i = 0;i < n;i++)
	{
		CountArr[a[i] - min]++;
	}
	int j = 0;
	//CountArr数组进行排序
	for (i = 0;i < rang;i++)
	{
		while (CountArr[i]--)
		{
			a[j] = i + min;//加上min恢复原数据
			++j;
		}
	}
	free(CountArr);
	CountArr = NULL;
}

注意点:

1 计数排序只能排序整形,而不能排序浮点型和字符串

2 计数排序适合排数据相对集中的数据

计数排序的特性总结:


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

三 排序大总结  

 [数据结构]-玩转八大排序(三)&&归并排序&&非比较排序_第11张图片

在这里我们重点总结一下各个排序的时间复杂度和稳定性:

直接插入排序

时间复杂度在数组中的元素全部是逆序时的情况最差O(n²),而最好的情况便是顺序了O(n),只要遍历就可以。

 该排序是非常稳定的,二个相同的数据的相对位置不会因为排序而改变,所以他是稳定的。

希尔排序

时间复杂度,该排序的时间复杂度是非常难求的,最坏情况无疑是全部逆序排序O(n²),但对于最好的情况我们姑且认为是O(n^1.3).

该排序是非常不稳定的,为什么这么说,因为该排序每次都要让gap步调相同的元素进行预排序,这极大可能是会打断相同排序的相对位置。

直接选择排序

这个排序就没事好说了总之就是稳的一批。

堆排序

这个排序的效率还是挺高的时间复杂度为O(n*logn),但我们要注意的是建堆就破坏了他的稳定性,所以说他是不稳定的。

冒泡排序

这个也是稳的很,不过我们在进行优化后,在最好的情况,数组是有序时,时间复杂度为O(n),但是他的平均情况基本就是O(n²)

快速排序

这个排序不得不说厉害,当数组都是有序的情况他们的复杂度是最坏的O(n²)在正常情况下都是O(n*logn),我们可以认为遇事不决就可以用快速排序。这里因为快速排序实现的三种方法都要改变相对位置,所以说他是不稳定的。

归并排序

该排序时间复杂度就为O(n*logn),但是我们要注意该排序要开额外的空间,所以他的空间复杂为O(logn)~O(n),这也使得他排序使用场景仅限于内存中,在磁盘中就显的用起来很不方便了,有点挺好的就是非常稳定

排序方法

平均情况

最好情况

最坏情况

空间复杂度

稳定性

直接插入排序

O(n²)

O(n)

O(n²)

O(1)

稳定

希尔排序

O(n*logn)~O(n²)

O(n^1.3)

O(n²)

O(1)

不稳定

直接选择排序

O(n²)

O(n²)

O(n²)

O(1)

不稳定

堆排序

O(n*logn)

O(n*logn)

O(n*logn)

O(1)

不稳定

冒泡排序

O(n²)

O(n)

O(n²)

O(1)

稳定

快速排序

O(n*logn)

O(n*logn)

O(n²)

O(logn)~O(n)

不稳定

归并排序

O(n*logn)

O(n*logn)

O(n*logn)

O(n)

稳定

其实排序还有其他的,像基数排序,外排序, 桶排序等,但这些排序不常用感兴趣的小伙伴可以自己去了解一下。

你可能感兴趣的:(数据结构,数据结构,排序算法,算法)