排序算法第四辑——归并排序与计数排序

排序算法第四辑——归并排序与计数排序_第1张图片

 

目录

一,归并排序算法

二,归并排序的非递归版本

三,计数排序


 

一,归并排序算法

归并排序算法是一个特别经典的算法了。这个算法采用的思想就是一个分治的思想,也就是将大问题化为子问题的思想。这个思想其实我们经常见到了,递归用的就是这个思想。那在归并排序上我们该如何用这个思想来解决问题呢?先举一个例子:

比如我们要排序下列数组:

排序算法第四辑——归并排序与计数排序_第2张图片

 现在我们要将这个数组内的数据排成升序该怎么搞呢?关键步骤有三步:

1.将数据的最左边与最右边的下标找到记作:begin  end。

2.找到数组的中间下标mid将数组分成[begin,mid],[mid+1,end]两组数据。

3.先将左右区间排好序最后再合并成一个数组完成排序。

总结起来就是:分解,排序,归并

以数组[5,1,10,3,0,7]为例,排序的过程就是这样的:

排序算法第四辑——归并排序与计数排序_第3张图片

将这个过程写成代码便实现了归并排序算法。

代码如下:

代码:

/归并排序递归版本
void MergeSort_(int* a, int begin, int end,int* tmp)
{
	//递归结束条件
	if (begin == end)
	{
		return;
	}
	//分解
	int midi = (begin+end) / 2;

	int begin1 = begin, end1 = midi;
	int begin2 = midi + 1, end2 = end;

	MergeSort_(a, begin1,end1,tmp);
	MergeSort_(a, begin2, end2,tmp);

	//合并
	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, (end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	MergeSort_(a, 0, n - 1, tmp);
	free(tmp);
}

 注意的点:

1.数组中的数据个数是最右边的下标-最左边的下标+1.

 (end - begin + 1)*sizeof(int)

2.合并数组的时候因为是先从左边开始合并的,在左边合并了以后。右边的合并就要从a+begin开始。临时的数组tmp也要从tmp+begin开始。这个begin可以看成已拷贝的元素个数。 

二,归并排序的非递归版本

 归并排序的算法有递归版本就一定会有非递归版本。归并排序的非递归版本的思想还是和递归版本的思想一样,都是先分割然后再将其归并排序拷贝回原数组中。拷贝的方式也有两种:

1.边归并边拷贝

2.归并完了以后再整体拷贝回原数组中。

这两种归并方法由于其拷贝的方式不同,所以在处理边界的方式上也会有所不同。现在来看看这两种不同拷贝方法的归并排序代码:

代码1:边归并边拷贝

void MergeSortNonR(int* a, int n)
{
	//首先创建一个数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}
    //循环分解并排序
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
        
		for (int i = 0;i < n;i+=2*gap)
		{   //利用i来控制两个要归并排序的数组的边界
			int begin1 = i, end1 = begin1 + gap - 1;
			int begin2 = i + gap, end2 = begin2 + gap - 1;
            //修正
			if (end1 >= n||begin1>=n )
			{
				break;
			}
			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++];
				}
			}
            //处理尾巴,这个必须先处理[begin1,end1]
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <=end2)
			{
				tmp[j++] = a[begin2++];
			}
            //归并一组拷贝一组,注意两个数组拷贝位置的变化
			memcpy(a+i, tmp+i, (end2 - i+1) * sizeof(int));
		}
        //归并下一组
		gap= 2*gap;
	}
}

过程演示:

比如我要排序数组[5,1,4,6,9,4,6]这组数据按照的归并排序过程大概就是这样的:

排序算法第四辑——归并排序与计数排序_第4张图片

在归并排序中最容易混淆的就是边界问题

在定义begin1,end1,begin2,end2时我们是这样定义的:

int begin1 = i, end1 = begin1 + gap - 1;
int begin2 = i + gap, end2 = begin2 + gap - 1;

其中i的范围是0~n-1:

for (int i = 0;i < n;i+=2*gap)

所以begin是不可能越界的,越界的只可能是end1,begin2,end2。越界的情况就分三种:

1.end1,begin2,end2全部越界。

2.begin2,end2越界。

3.end2越界。

越界的情况如下图所示:

排序算法第四辑——归并排序与计数排序_第5张图片

因为归并排序需要首先将数据放到不同的区间排序,所以每次分成的区间个数是2的倍数,

但是当end1与begin2越界时有效的区间的个数是奇数,当end2越界时有第二个区间有一部分数据有效。所以在这里就这样子处理:

if (end1 >= n||begin1>=n )//当有效区间数为奇数时就不需要归并最后一个区间
			{
				break;
			}
			if (end2 >= n)//当有效区间数是偶数但是只有一部分区间数据有效时只归并有效的区间
			{
				end2 = n - 1;
			}

 这样就可以解决区间越界的问题了。

代码2:整组拷贝

void MergeSortNonR2(int* a, int n)
{
	//首先创建一个数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}
	//循环分解
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		for (int i = 0;i < n;i += 2 * gap)
		{
			int begin1 = i, end1 = begin1 + gap - 1;
			int begin2 = i + gap, end2 = begin2 + gap - 1;
			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, n * sizeof(int));
		gap = 2 * gap;
		
	}
	free(tmp);
}

 

整组拷贝的代码与边归并边排序的代码大同小异,但是有几个关键点不同。

1.整组拷贝是没有不拷贝到tmp中的情况的。所以就不能再break。

所以这里的越界修正和边归并边拷贝不一样。整组拷贝的修正条件是这样的:

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;
			}

不存在的区间就改成不存在的情况。

三,计数排序

 计数排序是一种非比较排序。这个算法运用到的思想是映射思想。为了理解起来更加的方便,现在我们先把计数排序算法实现。代码如下:

代码:

//计数排序
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];
		}
	}
    //计算出最大和最小值之间的范围并创建数组count
	int range = max - min+1;
	int* count = (int*)malloc(sizeof(int)*range);
	if (count == NULL)
	{
		perror("malloc fail\n");
		return;
	}
    //将数组count里的数据都设置为0
	memset(count, 0, range * sizeof(int));
    //用count记录a[i]中每一个数据出现的次数,count的下标是a[i]的映射
	for (int i = 0;i < n;i++)
	{
		count[a[i] - min]++;
	}
    //根据每个数据出现的次数依次放回到数组a中,记住要加上min
	int k = 0;
	for (int i = 0;i < range;i++)
	{
		while (count[i]--)
		{
			a[k++] = i + min;
		}
	}
    //释放掉count
	free(count);
}

 图解原理:

 比如我要用计数排序来排数组[9,3,4,6,1,7,0,5,1,2]

 首先求出最大最小值:min = 0,max= 9.所以count数组的空间就开10个。

映射关系如下图所示:

排序算法第四辑——归并排序与计数排序_第6张图片

 

然后根据count数组内的每一个数据都对应着下标+min这个数出现的次数。按照下标依次放回到原数组中,便可以得到排序好的原数组。

#注意#:这个排序只能排序整型数据,因为数组下标必须是整型值。 

你可能感兴趣的:(数据结构初阶,排序算法,算法,数据结构,学习笔记,c语言)