【算法】排序算法

排序的相关概念

:::block-1

  • 排序:使一串记录按照其中某个或某些关键字的大小,递增或递减排列起来的操作。
  • 排序算法的稳定性:经过排序之后,排序后的记录相对次序较原记录没变,例如:高考录取中的相同成绩语文成绩高的优先,我们这时可以先按照语文分数的高低进行排序,后按照总分进行排序,这样就达到我们的目的了(相同成绩,语文优先)。
  • 内部排序:数据元素全部放在内存中的排序。
  • 外部排序:数据元素过大,导致不能同时存放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。通常使用归并。
    :::
  • 排序算法的分类

:::block-1

  • 快速排序

  • 递归版本

  • 挖坑法

  • 这段程序是快速排序算法的实现,使用了"挖坑法"来进行元素的分区和交换。
  • 函数PartSort2是一个辅助函数,用于将数组中的元素分割成两个部分,并返回分割点的下标。它接受三个参数:数组 a、左边界 left 和右边界 right。首先,在数组中选择一个中间位置 mid,并将中间位置的值与左边界的值进行交换。接下来,使用两个指针 left 和 right 分别从左右两端开始向中间遍历。在遍历的过程中,通过与 key(初始为左边界的值)进行比较,找到第一个小于 key 的元素和第一个大于 key 的元素,然后将它们交换,并更新 hole 的位置。最后,将左边界的值 key 放入 hole 的位置,并返回 hole。
  • 函数QuickSort2是快速排序的主函数,用于递归地对数组进行排序。它接受三个参数:数组 a、左边界 left 和右边界 right。首先,检查边界条件,如果左边界大于等于右边界,则直接返回。否则,调用 PartSort2 函数获取分割点的位置 val,然后分别对左半部分和右半部分进行递归调用。具体地,对左半部分调用 QuickSort2(a, left, val - 1),对右半部分调用 QuickSort2(a, val + 1, right)。
  • 这样,通过递归地将数组不断分割成更小的部分,并根据分割点的位置进行排序,最终完成整个数组的排序过程。
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int mid = Select_middle(a, left, right);
	Swap(&a[mid], &a[left]);
	int hole = left;
	int key = a[left];


	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}
void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}


	int val = PartSort2(a, left, right);


	QuickSort2(a, left, val - 1);
	QuickSort2(a, val + 1, right);
}
  • 前后指针法

  • 这段程序是基于快速排序算法的另一种实现,使用了"前后指针法"来进行元素的分区和交换。
  • 函数PartSort3同样是用于将数组中的元素进行分割,并返回分割点的下标。它接受三个参数:数组 a、左边界 left 和右边界 right。首先,选择一个中间位置 mid,并将中间位置的值与左边界的值进行交换。接下来,使用两个指针 prev 和 cur 分别从左右两端开始向中间遍历。初始时,prev 指向左边界,cur 指向 left + 1,keyi 也指向左边界。在遍历的过程中,如果当前位置的值小于等于分割点的值 a[keyi],则将 prev 向前移动一位,然后将 a[cur] 与 a[prev] 进行交换。最后,将左边界的值 a[keyi] 放入 prev 的位置,并返回 keyi。
  • 函数QuickSort3是快速排序的主函数,同样采用递归地对数组进行排序。它接受三个参数:数组 a、左边界 left 和右边界 right。首先,检查边界条件,如果左边界大于等于右边界,则直接返回。否则,调用 PartSort3 函数获取分割点的位置 val,然后分别对左半部分和右半部分进行递归调用。具体地,对左半部分调用 QuickSort3(a, left, val - 1),对右半部分调用 QuickSort3(a, val + 1, right)。
  • 这种实现方式与"挖坑法"类似,通过使用两个指针来实现元素的分区和交换,从而完成整个数组的排序过程。
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int mid = Select_middle(a, left, right);
	Swap(&a[mid], &a[left]);
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] <= a[keyi])
		{
			prev++;
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}


	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

void QuickSort3(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}


	int val = PartSort3(a, left, right);


	QuickSort3(a, left, val - 1);
	QuickSort3(a, val + 1, right);
}
  • hoare版本

  • 这段程序是基于快速排序算法的另一种实现,采用了 Hoare 分区方案。
  • 函数 PartSort1 是用于将数组中的元素进行分割,并返回分割点的下标。它接受三个参数:数组 a、左边界 left 和右边界 right。首先,选择一个中间位置 mid,并将中间位置的值与左边界的值进行交换。接下来,使用两个指针 left 和 right 分别从左右两端开始向中间遍历。在每次遍历过程中,right 向左移动,直到找到一个小于分割点值 a[keyi] 的元素;left 向右移动,直到找到一个大于分割点值 a[keyi] 的元素。然后,交换 a[left] 和 a[right]。重复执行上述步骤,直到 left 和 right 相遇。最后,将左边界的值 a[keyi] 放入 left 的位置,并返回 left 作为分割点的位置。
  • 函数 QuickSort1 是快速排序的主函数,同样采用递归地对数组进行排序。它接受三个参数:数组 a、左边界 left 和右边界 right。首先,检查边界条件,如果左边界大于等于右边界,则直接返回。否则,调用 PartSort1 函数获取分割点的位置 val,然后分别对左半部分和右半部分进行递归调用。具体地,对左半部分调用 QuickSort1(a, left, val - 1),对右半部分调用 QuickSort1(a, val + 1, right)。
  • 这种实现方式在分区操作上与"前后指针法"相比略有不同,使用了两个指针同时从两端向中间遍历,并在满足条件时交换元素。它的主要思想是通过不断缩小分区的范围来完成整个数组的排序过程。
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int mid = Select_middle(a, left, right);
	Swap(&a[mid], &a[left]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}


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


		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	keyi = left;
	return keyi;
}

void QuickSort1(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}


	int val = PartSort1(a, left, right);


	QuickSort1(a, left, val - 1);
	QuickSort1(a, val + 1, right);
}
  • 非递归版本

  • 这段代码是快速排序的非递归实现。
  • 首先,创建一个栈 st 用于存储待处理的子区间。使用 InitStack 函数对栈进行初始化。
  • 然后,将初始的左右边界入栈,即 PushStack(&st, right) 和 PushStack(&st, left)。
  • 在进入循环之后,首先从栈顶取出左右边界并分别赋值给 left 和 right。如果 left 大于等于 right,则说明当前子区间已经有序,无需进行处理,直接进行下一次循环。
  • 接下来,调用 PartSort1 函数对当前子区间进行一次分割操作,获取分割点的位置 keyi。
  • 然后,按照以下顺序将四个新的子区间的边界入栈:右边子区间的右边界、右边子区间的左边界加1、左边子区间的右边界、左边子区间的左边界。注意,需要按照这个顺序入栈,以保证栈中的元素被处理的顺序符合快速排序的要求。
  • 最后,回到循环开始继续取出下一个子区间进行处理,直到栈为空。
  • 通过这种方式,可以模拟递归的过程,实现快速排序的非递归版本。
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	InitStack(&st);
	PushStack(&st,right);
	PushStack(&st, left);

	while (!EmptyStack(&st))
	{
		left = FrontStack(&st);
		PopStack(&st);
		right = FrontStack(&st);
		PopStack(&st);
		if (left >= right)
		{
			continue;
		}
		int keyi = PartSort1(a, left, right);
		PushStack(&st, right);
		PushStack(&st, keyi+1);
		PushStack(&st, keyi-1);
		PushStack(&st, left);
	}
}
  • 快速排序的优化方法

  1. 三数取中法
int Select_middle(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
  1. 当递归到小区间时用插入排序
  2. 随机数取中法
  • 希尔排序

  • 希尔排序是基于插入排序的一种排序算法,它通过将数组分成多个子数组来进行排序,并逐步减小子数组的长度。在代码中,使用变量 gap 来表示子数组的间隔,初始值为数组长度 n。然后,在每次迭代中,通过 gap = gap / 3 + 1 来缩小间隔,直到最后一次迭代 gap 变为 1。
  • 在每次迭代中,使用插入排序对每个子数组进行排序。对于从 0 到 n - gap 的每个元素 i,其中 i 表示子数组的起始位置,根据间隔 gap,依次比较并将当前元素 a[i] 插入到它应该处于的位置。通过将比当前元素大的元素向右移动 gap 个位置,直到找到合适的位置或者到达子数组的起始位置,然后将当前元素插入到找到的位置。
  • 通过不断缩小间隔和对子数组进行插入排序,希尔排序可以在一定程度上提高插入排序的效率。最终,当间隔 gap 缩小到 1 时,整个数组即完成了最后一次插入排序,排序过程结束。
  • 需要注意的是,在代码中使用断言 assert(a) 来确保输入的数组 a 不为空。
// 希尔排序
void ShellSort(int* a, int n)
{
	assert(a);
	int gap = n;
	int end = 0;
	int tmp = 0;
	while (gap>1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			end = i;
			tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp > a[end])
				{
					break;
				}
				else
				{
					a[end + gap] = a[end];
					end -= gap;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

  • 关于gap

  1. gap越大排的越快,但越不有序,越小则相反
  • 插入排序

  • 这段代码是插入排序的实现。
  • 插入排序是一种简单直观的排序算法,它将数组分为已排序和未排序两部分,每次从未排序部分取出一个元素插入到已排序部分的正确位置。
  • 在代码中,使用变量 end 来表示已排序部分的最后一个元素的索引,初始值为 0。然后,从数组的第二个元素开始遍历,即 i 的初始值为 1。对于每个 i,将 a[i] 存储到临时变量 tmp 中。
  • 然后,通过一个循环逐个比较已排序部分的元素,从 end 开始向前移动。如果找到一个已排序元素比 tmp 大,则将该元素向后移动一个位置,直到找到合适的位置或者到达已排序部分的起始位置。最后,将 tmp 插入到找到的位置,即 a[end+1] = tmp。、
  • 通过不断遍历未排序部分并插入到已排序部分的正确位置,最终可以完成整个数组的排序。
  • 需要注意的是,在代码中使用断言 assert(a) 来确保输入的数组 a 不为空。
void InsertSort(int* a, int n)
{
	assert(a);
	int end = 0;
	int tmp = 0;

	for (int i = 0; i < n - 1; i++)
	{
		end = i;
		tmp = a[end + 1];

		while (end >= 0)//每次的操作
		{
			if (tmp > a[end])
			{
				break;
			}
			else
			{
				a[end + 1] = a[end];
				end--;
			}
		}
		a[end+1] = tmp;
	}
}
  • 选择排序

  • 这段代码是选择排序的实现。
  • 选择排序是一种简单直观的排序算法,它通过每次从待排序的元素中选择最小(或最大)的元素并放置到已排序部分的末尾,逐步构建有序序列。
  • 在代码中,使用变量 begin 和 end 来表示待排序部分的起始和结束位置,初始值分别为 0 和 n-1。然后,使用变量 mini 和 maxi 分别表示待排序部分中最小和最大元素的索引,初始化为 begin。
  • 通过一个循环,不断缩小待排序部分的范围。在每次循环中,遍历待排序部分的剩余元素,找到其中的最小值和最大值的索引,分别存储在 mini 和 maxi 中。
  • 然后,将找到的最小值与待排序部分的起始位置进行交换,即 Swap(&a[mini], &a[begin])。**如果起始位置和最大值的索引相同,说明最大值已经被移动到最小值位置,此时需要将 maxi 更新为最小值的索引。**接着,将找到的最大值与待排序部分的结束位置进行交换,即 Swap(&a[maxi], &a[end])。
  • 每经过一次循环,待排序部分的范围缩小一位,即 begin++ 和 end–。通过不断缩小范围,并将最小(或最大)的元素放到已排序部分的末尾,最终可以完成整个数组的排序。
  • 需要注意的是,在代码中使用断言 assert(a) 来确保输入的数组 a 不为空,并且函数调用了一个名为 Swap 的交换函数来交换两个元素的值。
void SelectSort(int* a, int n)
{
	assert(a);
	int begin = 0;
	int end = n - 1;
	int maxi = 0;
	int mini = 0;

	while (begin < end)
	{
		maxi = end;
		mini = begin;
		for (int i = begin; i <= end; i++)//遍历整个数组,找出最小、最大值下标
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}

			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		Swap(&a[mini], &a[begin]);
		if (begin == maxi)
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);
		begin++;
		end--;
	}
}
  • 堆排序

  • 这段代码是堆排序的实现。
  • 堆排序是一种基于完全二叉堆的排序算法,它利用了堆的性质:父节点的值总是小于(或大于)其子节点的值。在堆排序中,首先将待排序的数组构建为一个最小堆或最大堆,然后依次将堆顶元素与最后一个元素交换,并重新调整堆,最终得到有序序列。
  • 在代码中,首先定义了一个函数 AdjustDwon,用于将指定位置的元素向下调整,以满足堆的性质。函数接受三个参数:数组 a、数组长度 n 和父节点的索引 parent。根据父节点的索引,计算出其左子节点的索引 child,然后进入一个循环,判断是否存在左子节点(即 child < n)。
  • 如果存在左子节点,再判断是否存在右子节点,并找出左右子节点中较小(或较大)的那个节点。如果父节点的值大于子节点的值,则交换父节点和子节点的值,并更新父节点和子节点的索引。然后,继续向下调整,直到满足堆的性质,或者到达叶子节点。
  • 接下来,定义了函数 HeapSort,用于进行堆排序。首先,从最后一个非叶子节点开始,调用 AdjustDwon 函数,将数组构建为一个最小堆或最大堆。然后,通过一个循环,每次将堆顶元素与最后一个元素交换,并将待排序部分的长度减一。接着,再次调用 AdjustDwon 函数进行堆的调整,直到待排序部分长度为 0,即完成整个数组的排序。
  • 需要注意的是,在代码中使用断言 assert(a) 来确保输入的数组 a 不为空,并且函数调用了一个名为 Swap 的交换函数来交换两个元素的值。
// 堆排序
void AdjustDwon(int* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;
 
	while (child < n)
	{
		if (child + 1 < n && a[child] > a[child + 1])
		{
			child++;
		}

		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* a, int n)
{
	assert(a);
	for (int i = (n - 1 - 1)/2; i >= 0; i--)
	{
		AdjustDwon(a, n, i);
	}
	//排序
	while (n)
	{
		Swap(&a[0], &a[n - 1]);
		n--;
		AdjustDwon(a, n, 0);
	}
  • 冒泡排序

  • 这段代码是冒泡排序的实现。
  • 冒泡排序是一种简单的排序算法,它重复地比较相邻的两个元素,如果它们的顺序不正确就进行交换,直到整个数组排序完成。
  • 在代码中,定义了一个函数 BubbleSort,用于进行冒泡排序。函数接受两个参数:数组 a 和数组长度 n。通过两层循环,外层循环控制比较轮数,内层循环在每一轮中逐个比较相邻的元素。
  • 在内层循环中,使用变量 j 来表示当前比较的元素索引,比较 a[j] 和 a[j + 1] 的值。如果前者大于后者,则交换它们的位置,即调用 Swap(&a[j], &a[j + 1])。
  • 通过每一轮循环,最大的元素都会被交换到待排序部分的末尾,同时待排序部分的长度减一。经过 n-1 轮循环后,整个数组就会按照升序(或降序)排列。
  • 需要注意的是,在代码中调用了一个名为 Swap 的交换函数来交换两个元素的值。
// 冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}
  • 归并排序

  • 归并排序递归版本

  • 这段代码是归并排序的实现。

  • 归并排序是一种分治算法,它将待排序的数组不断地划分为两个子数组,直到每个子数组只包含一个元素,然后将这些子数组逐个合并成更大的有序子数组。

  • 在代码中,定义了两个函数 _merge_sort 和 merge_sort。

  • 函数 _merge_sort 是归并排序的核心递归函数。它接受五个参数:原始数组 a、临时数组 tmp、当前待排序部分的起始索引 left,以及当前待排序部分的结束索引 right。首先判断如果 left >= right,则表示当前待排序部分只有一个元素或没有元素,直接返回。否则,计算出当前待排序部分的中间索引 mid,并将数组划分为两个子数组:begin1 到 end1 和 begin2 到 end2。

  • 然后,递归调用 _merge_sort 函数对两个子数组进行排序。再创建一个变量 k 来表示合并后的有序子数组的索引。通过一个循环,比较两个子数组中的元素,将较小的元素放入临时数组 tmp 中,并将相应的索引递增。当其中一个子数组遍历完后,将剩下的子数组中的元素直接放入 tmp 中。

  • 最后,通过一个循环,将临时数组 tmp 中的有序元素依次复制回原始数组 a 的相应位置。

  • 函数 merge_sort 是对外的接口函数,它接受两个参数:原始数组 a 和数组长度 n。在函数中,首先动态分配一个临时数组 tmp,大小与原始数组相同。然后调用 _merge_sort 函数对整个数组进行归并排序,并在排序完成后释放临时数组的内存。

  • 需要注意的是,在代码中使用了动态内存分配函数 malloc 和 free 来创建和释放临时数组的内存空间。

void _merge_sort(int* a, int* tmp, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int mid = (left + right) / 2;
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	_merge_sort(a,tmp,begin1, end1);
	_merge_sort(a,tmp, begin2, end2);

	int k = 0;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[k++] = a[begin1++];
		}
		else
		{
			tmp[k++] = a[begin2++];
		}
	}

	while (begin1 <= end1)
	{
		tmp[k++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[k++] = a[begin2++];
	}

	for (int i = 0; i < k; i++)
	{
		a[left++] = tmp[i];
	}
}
void merge_sort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_merge_sort(a,tmp,0,n-1);
	free(tmp);
}

  • 归并排序非递归版本

  • 这段代码实现了非递归版本的归并排序。
  • 函数 MergeSortNonR 接受两个参数:原始数组 a 和数组长度 n。在函数中,首先使用动态内存分配函数 malloc 分配了一个临时数组 tmp,大小为 n 个整数大小的内存空间。
  • 然后,定义了一个变量 gap,初始值为 1。通过循环,当 gap 小于 n 时,进行以下操作:
  • 在每一轮循环中,以步长 gap 遍历数组 a。定义了四个索引:begin1、end1、begin2 和 end2,用来表示两个子数组的起始和结束位置。初始时,begin1 是当前子数组的起始位置(即 i),end1 是 begin1 + gap,begin2 是 end1 + 1,end2 是 begin2 + gap。
  • 判断如果其中一个子数组的结束位置超出了数组的范围,或者第二个子数组的起始位置超出了数组的范围,则跳出循环。
  • 如果第二个子数组的结束为止超出数组范围,则调整结束位置:end2=n-1
  • 如果第一个子数组的最后一个元素大于第二个子数组的第一个元素,调用 Swap 函数交换这两个元素的位置。
  • 针对两个子数组的长度为 1 的情况,如果第一个子数组的唯一元素大于第二个子数组的唯一元素,同样调用 Swap 函数交换这两个元素的位置。
  • 使用一个循环,将两个子数组中较小的元素依次放入临时数组 tmp 中,并递增 k 的值。
  • 使用 memcpy 函数将临时数组 tmp 中的有序元素复制回原始数组 a 对应的位置。
  • 将步长 gap 乘以 2,进行下一轮的排序。
  • 最后,排序完成后,释放临时数组 tmp 的内存。
  • 需要注意的是,代码中使用了 Swap 函数来交换两个元素的位置,但是该函数的具体实现并未在提供的代码中给出。你可以自行实现一个交换函数来替代。
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap)
		{
			int begin1 = i, end1 = i + gap;
			int begin2 = end1 + 1, end2 = begin2 + gap;
			int k = 0;
			if (end1 >= n || begin2 >=n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			if (a[end1] < a[begin1])
			{
				Swap(&a[end1], &a[begin1]);
			}
			if (end1 - begin1 == 1)
			{
				if (a[end1] < a[begin1])
				{
					Swap(&a[end1], &a[begin1]);
				}
			}

			if (end2 - begin2 == 1)
			{
				if (a[end2] < a[begin2])
				{
					Swap(&a[end2], &a[begin2]);
				}
			}

			while (begin1 <= end1 && begin2 <= end2)//ºÏ²¢
			{
				if (a[begin1] < a[begin2])
				{
					tmp[k++] = a[begin1++];
				}
				else
				{
					tmp[k++] = a[begin2++];
				}
			}
			memcpy(a + i, tmp, k);
		}
		gap *= 2;
	}
}
  • 计数排序

  • 这段代码实现了计数排序(Counting Sort)算法。
  • 函数 CountSort 接受两个参数:原始数组 a 和数组长度 n。在函数中,首先找到数组中的最大值和最小值,分别赋给变量 max 和 min。
  • 然后,根据最大值和最小值计算出需要开辟的临时数组 tmp 的大小,大小为 max - min + 1。
  • 接着,使用动态内存分配函数 malloc 分配了临时数组 tmp 的内存空间,并使用 memset 函数将该数组的所有元素初始化为 0。
  • 接下来,遍历原始数组 a,统计每个元素出现的次数,将统计结果存储在临时数组 tmp 对应的位置上。假设 a[i] - min 为第 i 个元素在临时数组中的索引,那么执行 tmp[a[i] - min]++ 就表示对应索引位置的计数加 1。
  • 然后,定义变量 k,用于记录排序后的数组 a 的索引位置。再次遍历临时数组 tmp,通过一个循环,将临时数组 tmp 中的计数值转化为排序后的元素,并存放到原始数组 a 中。
  • 具体实现是通过第二个循环,在每次循环中,当临时数组 tmp 中的计数值大于 0 时,将当前索引 i 加上最小值 min,得到当前元素的值,然后将该元素赋给原始数组 a 的相应位置,并递增变量 k。
  • 最后,排序完成后,释放临时数组 tmp 的内存。
  • 需要注意的是,代码中使用了动态内存分配函数 malloc 分配了临时数组 tmp 的内存空间,但在实际使用过程中,应该对分配结果进行检查,确保分配成功并释放相应的内存。
void  CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	for (int i = 0; i < n; i++)
	{
		if (max < a[i])
		{
			max = a[i];
		}
		if (min > a[i])
		{
			min = a[i];
		}
	}
	int lenth = max - min + 1;
	int* tmp = (int*)malloc(sizeof(int) * lenth);
	memset(tmp, 0, sizeof(int)*lenth);
	for (int i = 0; i < n; i++)
	{
		tmp[a[i] - min]++;
	}
	int k = 0;
	for (int i = 0; i < lenth; i++)
	{
		while (tmp[i]--)
		{
			a[k++] = i + min;
		}
	}
}
  • 计数排序的局限

  1. 适用于数据范围集中时,否则会浪费很多空间
  2. 只能用来排整型
    :::

各个排序的时间复杂度及其稳定性

:::block-1

冒泡排序

  1. 平均情况:O(N^2)
  2. 最好情况:O(N)
  3. 最坏情况:O(N^2)
  4. 空间复杂度:O(1)
  5. 稳定性:稳定

选择排序

  1. 平均情况:O(N^2)
  2. 最好情况:O(N^2)
  3. 最坏情况:O(N^2)
  4. 空间复杂度:O(1)
  5. 稳定性:不稳定

插入排序

  1. 平均情况:O(N^2)
  2. 最好情况:O(N)
  3. 最坏情况:O(N^2)
  4. 空间复杂度:O(1)
  5. 稳定性:稳定

希尔排序

  1. 平均情况:O(NlogN)~O(N^2)
  2. 最好情况:O(N^1.3)
  3. 最坏情况:O(N^2)
  4. 空间复杂度:O(1)
  5. 稳定性:不稳定

堆排序

  1. 平均情况:O(NlogN)
  2. 最好情况:O(NlogN)
  3. 最坏情况:O(NlogN)
  4. 空间复杂度:O(1)
  5. 稳定性:不稳定

归并排序

  1. 平均情况:O(NlogN)
  2. 最好情况:O(NlogN)
  3. 最坏情况:O(NlogN)
  4. 空间复杂度:O(N)
  5. 稳定性:稳定

快速排序

  1. 平均情况:O(NlogN)
  2. 最好情况:O(NlogN)
  3. 最坏情况:O(N^2)
  4. 空间复杂度:O(logN)~O(N)
  5. 稳定性:不稳定
    :::

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