计数排序&&归并排序(递归版本&&非递归版本)

本文详细介绍了计数排序和归并排序的原理、逻辑、时间和空间复杂度,以及它们在不同场景下的适用性,特别讨论了计数排序的绝对映射与相对映射问题以及归并排序的递归与非递归实现及其优化。
摘要由CSDN通过智能技术生成

1.计数排序

        计数排序是一种非比较排序算法,其核心思想是通过统计每个元素出现的次数,然后根据统计结果将元素按照顺序放置在输出数组中。

以下是计数排序的逻辑思想(C语言版):

1. 首先,遍历待排序的数组,找到数组中的最大值max,确定计数数组的大小为max+1。

2. 创建一个大小为max+1的计数数组count,并初始化为0。

3. 遍历待排序的数组,将每个元素的值作为计数数组count的索引,并将对应索引位置的值

加1,表示该元素出现的次数。

4. 对计数数组count进行累加操作,使得每个索引位置的值表示小于等于该索引的元素个数。

5. 统计完次数之后就往原数组内覆盖回写

        计数排序是一种就地排序算法,这意味着它不需要任何额外的空间来对数据进行排序。它也是一个稳定的排序算法,这意味着输入中相等元素的顺序在输出中得以保留。

        计数排序是一种十分高效的排序,时间复杂度为O(n + k),其中n是列表中元素的数量,k是列表中任何元素的最大值。计数排序的空间复杂度为O(k)。

        计数排序是不适合分散的数据,更适合集中的数据;不适合浮点数,字符串,结构体数据排序,只适合整数。

eg:如果有一组数据是从1000开始的连续的数据,那么是不是就意味着前面1000个空间就浪费了,如何解决?

        我是数据是多少就映射到哪个位置上,这种映射叫做绝对映射,这可能会导致大量空间的浪费,大多情况下我们都是使用相对映射的方法。(相对最小值)我们在使用计数排序前,可以先找到改组数据的最小值和最大值,这就是该组数据的范围,这样就可以有效的节省空间了。

代码:

// 计数排序
// 时间:O(N+range)
// 空间:O(range)
void CountSort(int* a, int n)
{
	int min = a[0], max = 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* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		printf("calloc fail\n");
		return;
	}

	// 统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;//使得下标从0开始
	}

	// 排序
	int i = 0;
	for (int j = 0; j < range; j++)
	{
		while (count[j]--)
		{
			a[i++] = j + min;//回到原数组范围
		}
	}
}

2.归并排序 

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

它将待排序的数组递归地分成两个子数组,然后对这两个子数组分别进行排序,最后将两个有序的子数组合并成一个有序的数组。

递归版本的归并排序的具体步骤如下:

1. 将待排序的数组分成两个子数组,每个子数组的大小为 n/2。

2. 对这两个子数组分别进行递归排序。

3. 将两个有序的子数组合并成一个有序的数组。

合并两个有序的子数组可以通过以下步骤实现:

1. 创建一个新的数组,用于存储合并后的结果。

2. 将两个子数组中的元素依次比较,将较小的元素放入新的数组中。

3. 重复步骤 2,直到两个子数组中的所有元素都被放入新的数组中。

下面是归并排序的递归版本的算法步骤:

1. **分解**:将待排序的数组分成两个子数组,通过计算数组的中间位置来实现。可以使用递归将左右两个子数组继续分解,直到子数组的大小为1。

2. **排序**:对分解得到的子数组进行排序,可以通过递归调用归并排序函数来实现。

3. **合并**:将排序好的两个子数组合并成一个有序的数组。比较两个子数组的第一个元素,将较小的元素放入结果数组中,并将相应子数组的指针向后移动,重复这个过程直到其中一个子数组为空,然后将另一个子数组中剩余的元素直接放入结果数组中。

递归版本的归并排序的关键在于递归地将数组分解为较小的子数组,并在合并阶段将它们逐步合并成一个有序的数组。这个过程会一直递归下去,直到子数组的大小为1,即只有一个元素,此时可以认为它已经是有序的。

下面归并排序递归版本的实现代码:

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

	int mid = (begin + end) / 2;
	// [begin, mid][mid+1, end]
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

	// [begin, mid][mid+1, end]归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, 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)
	{
		perror("malloc fail");
		return;
	}

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

	free(tmp);
}

非递归版本

递归的思想是把数组分到一个数据认为它有序,然后才开始归并。非递归的思想就是可以一开始就是认为单个数据有序,开分组归并。

这里我们引出gap变量,gap表示每组的数据个数

我们还是通过划分区间的方法来理清逻辑,[begin1,end1] [begin2,end2]两组一归并。

end1为什么是i+gap-1?因为我们访问的是下标,所有我们要减去1

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

该如何控制gap?

gap的变化趋势是每次扩大2倍,gap*=2。只要gap<n,那么循环就继续。

该怎么去控制归并循环?

首先i+=2*gap,因为每次要跳过两组数据,那么i的结束条件就可以是i<n


注意:没归并完一组就将这组的数据拷贝回原数组,而不是将所有组归并完后,再将整个数组拷贝回原数组。(稍后解释原因)。

初步代码:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	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;

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

			memcpy(a + i, tmp + i, sizeof(int) * 2*gap);
		}

		printf("\n");

		gap *= 2;
	}


	free(tmp);
}
int main()
{
	int a[8] = { 8,7,6,5,4,3,2,1 };
	MergeSortNonR(a, 8);
	for (int i = 0; i < 8; i++)
	{
		printf("%d ", a[i] );
	}
}

运行结果:

但我们的数组个数不是2^n时,排序就出现错误了,这是为什么呢?

我们通过打印每组gap的分组情况发现gap为2,4,8时,都出现了越界情况,我们并没有那么多数据,因为我们都是按整数倍去算的,每次都会为我你们补齐,如何解决?

我们先看begin1,begin1是i,它不可能越界,其它的都可能越界。

如果end1越界, 那么证明后面就没有数据,就不需要越界了,接直接break。

如果begin2越界了,end2,那也可以证明后面没有数据了,直接break。

如果end2越界了,那么证明前面都是好的,end2是补齐到了整数倍所以导致越界了,那么只要将end2修正到n就好了。

这里的拷贝也是要进行修改的,因为拷贝也是按照整数倍去拷贝的,要改成(end2-i+1)左闭右闭,减完后要加1。

修改后的代码:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		//printf("gap:%2d->", gap);
		for (size_t i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			// [begin1, end1][begin2, end2] 归并
			//printf("[%2d,%2d][%2d, %2d] ", begin1, end1, begin2, end2);

			// 边界的处理
			if (end1 >= n || begin2 >= n)
			{
				break;
			}

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

			//printf("[%2d,%2d][%2d, %2d] ", 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++];
			}

			memcpy(a + i, tmp + i, sizeof(int) * (end2-i+1));
		}

		printf("\n");

		gap *= 2;
	}


	free(tmp);
}

运行结果:

填前面的坑:为什么要归并一组拷贝一组,而不是等拷贝完了,拷贝整个数组?

因为会出现越界的情况,当遇到越界的情况,因为只有一组已经是有序的了,直接break就好了,就不需要进入tmp数组了,如果break了,却又整组的进行迭代数据,那么在最后一组的位置进入tmp数组就有可能出现随机值,再拷贝回原数组那么就会把最后一组正确的数据给覆盖掉了。

  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
好的,我可以为您解答这个问题。 首先,堆排序、快速排序、归并排序计数排序都是常见的排序算法,它们的实现方式各不相同。下面分别介绍它们的C++实现。 1. 堆排序(Heap Sort) 堆排序是一种选择排序,它利用堆的性质进行排序。堆是一个完全二叉树,其中每个节点的值都大于或等于其子节点的值(最大堆),或者小于或等于其子节点的值(最小堆)。在堆排序中,我们将待排序数组构建成一个最大堆,然后将堆顶元素(即最大值)与数组最后一个元素交换,然后重新调整堆,重复执行此操作,直到整个数组有序。 下面是堆排序的C++实现: ```c++ void heapify(int arr[], int n, int i) { int largest = i; // 初始化最大值为根节点 int l = 2 * i + 1; // 左子节点索引 int r = 2 * i + 2; // 右子节点索引 // 如果左子节点比最大值大,则更新最大值索引 if (l < n && arr[l] > arr[largest]) largest = l; // 如果右子节点比最大值大,则更新最大值索引 if (r < n && arr[r] > arr[largest]) largest = r; // 如果最大值不是根节点,则交换根节点和最大值,然后递归调整堆 if (largest != i) { swap(arr[i], arr[largest]); heapify(arr, n, largest); } } void heapSort(int arr[], int n) { // 构建最大堆 for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i); // 依次取出堆顶元素,放到数组末尾,并重新调整堆 for (int i = n - 1; i >= 0; i--) { swap(arr[0], arr[i]); heapify(arr, i, 0); } } ``` 2. 快速排序(Quick Sort) 快速排序是一种分治排序,它利用分治的思想将大问题分解成小问题,然后递归地解决小问题。在快速排序中,我们选择一个枢轴元素(一般选择第一个元素),将数组分为两个部分,左边部分的元素均小于枢轴元素,右边部分的元素均大于枢轴元素。然后分别对左右两部分递归执行此操作,直到整个数组有序。 下面是快速排序的C++实现: ```c++ int partition(int arr[], int low, int high) { int pivot = arr[low]; // 枢轴元素 int i = low, j = high; while (i < j) { // 从右往左找第一个小于枢轴元素的位置 while (i < j && arr[j] >= pivot) j--; // 从左往右找第一个大于枢轴元素的位置 while (i < j && arr[i] <= pivot) i++; // 交换左右两个元素 if (i < j) swap(arr[i], arr[j]); } // 将枢轴元素放到正确的位置上 arr[low] = arr[i]; arr[i] = pivot; return i; } void quickSort(int arr[], int low, int high) { if (low < high) { int pivot = partition(arr, low, high); quickSort(arr, low, pivot - 1); quickSort(arr, pivot + 1, high); } } ``` 3. 归并排序(Merge Sort) 归并排序也是一种分治排序,它将待排序数组分成两部分,分别排序,然后合并成一个有序数组。在归并排序中,我们先将数组分为两半,分别对左半部分和右半部分递归执行归并排序,然后将两个有序子数组合并成一个有序数组。 下面是归并排序的C++实现: ```c++ void merge(int arr[], int l, int m, int r) { int n1 = m - l + 1; int n2 = r - m; // 创建临时数组 int L[n1], R[n2]; // 将左半部分复制到临时数组 L for (int i = 0; i < n1; i++) L[i] = arr[l + i]; // 将右半部分复制到临时数组 R for (int i = 0; i < n2; i++) R[i] = arr[m + 1 + i]; // 合并左右两个有序子数组 int i = 0, j = 0, k = l; while (i < n1 && j < n2) { if (L[i] <= R[j]) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } k++; } // 将剩余的元素复制到数组中 while (i < n1) { arr[k] = L[i]; i++; k++; } while (j < n2) { arr[k] = R[j]; j++; k++; } } void mergeSort(int arr[], int l, int r) { if (l < r) { int m = l + (r - l) / 2; mergeSort(arr, l, m); mergeSort(arr, m + 1, r); merge(arr, l, m, r); } } ``` 4. 计数排序(Counting Sort) 计数排序是一种非比较排序,它利用元素出现的次数来确定每个元素在有序数组中的位置。在计数排序中,我们先统计每个元素出现的次数,然后计算每个元素在有序数组中的位置,最后将元素放到正确的位置上。 下面是计数排序的C++实现: ```c++ void countingSort(int arr[], int n) { int max_val = 0; for (int i = 0; i < n; i++) { if (arr[i] > max_val) max_val = arr[i]; } // 创建计数数组 int count[max_val + 1] = {0}; // 统计每个元素出现的次数 for (int i = 0; i < n; i++) count[arr[i]]++; // 计算每个元素在有序数组中的位置 for (int i = 1; i <= max_val; i++) count[i] += count[i - 1]; // 将元素放到正确的位置上 int output[n]; for (int i = n - 1; i >= 0; i--) { output[count[arr[i]] - 1] = arr[i]; count[arr[i]]--; } // 将有序数组复制到原数组中 for (int i = 0; i < n; i++) arr[i] = output[i]; } ``` 以上就是堆排序、快速排序、归并排序计数排序的C++实现,希望能对您有所帮助!
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值