【排序详解】快排、归并、希尔

目录

一、快速排序

1、GetMidi函数

2、Hoare快排

3、挖坑法快排

4、非递归的快排

5、快排的qsort实现

二、归并排序

1、递归的归并

2、非递归的归并

三、希尔排序


一、快速排序

        快排的思想是任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。快排是非稳定的排序算法。

        快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

1、GetMidi函数


int GetMidi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	// left mid right
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])  // mid是最大值
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right]) // mid是最小
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

        该函数通常用于快速排序算法中的分割步骤,目的是选择一个合适的基准值(pivot),将数组分成左右两个子数组。GetMidi函数根据数组元素的大小关系,确定基准值的位置。

在具体实现中,GetMidi函数接受一个整型数组a、左边界begin和右边界end作为参数。它首先计算出中间索引mid,并根据条件判断来确定返回的值。

需要注意的是,GetMidi函数并不进行任何交换或排序操作,它只是用来辅助快速排序算法中的分割过程。在使用,GetMidi函数的返回值被用来确定基准值的位置,进而将数组划分成左右两个子数组,并递归地对子数组进行排序操作。

2、Hoare快排

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int midi = GetMidi(a, begin, end);
    //从数组a中得到位置处于begin、end和(begin+end)/2中最小的数
	Swap(&a[begin], &a[midi]);
	int keyi = begin;
	int left = begin, right = end;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[right], &a[left]);
	}
	Swap(&a[keyi], &a[left]);
	QuickSort(a, begin, left - 1);
	QuickSort(a, left+1,end);
}

这段代码采用了hoare大佬的方法。基于左右两边指针的快速排序算法。

首先,函数会检查起始下标是否大于等于结束下标,如果是,则直接返回。

接着,通过调用 GetMidi 函数获取中间位置 midi 的值,并将该位置和起始位置的元素进行交换。

然后,定义一个 keyi 来表示基准值的位置,并同时定义 left 和 right 分别为左侧和右侧的指针。

在接下来的循环中,首先从右侧开始,找到第一个小于基准值的元素的位置 right。然后从左侧开始,找到第一个大于基准值的元素的位置 left。如果 left 小于 right,则交换 a[left] 和 a[right]。

当 left 和 right 相遇时,将基准值和 left 指向的元素互换位置,并且此时基准值左侧都是小于它的元素,右侧都是大于它的元素。

最后,通过递归方式对基准值左侧和右侧的子序列进行快速排序,即调用 QuickSort(a, begin, left - 1) 和 QuickSort(a, left + 1, end)。

3、挖坑法快排

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int midi = GetMidi(a, begin, end);
	Swap(&a[begin], &a[midi]);
	int hole = begin;
	int key = a[hole];
	int left = begin, right = end;
	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;
	QuickSort(a, begin, left - 1);
	QuickSort(a, left + 1, end);
}

        这段代码实现了快速排序算法。与经典的快速排序算法不同的是,这个实现中将基准值选为第一个元素,并使用“挖坑填数”法来进行分割操作。

        快速排序算法的核心思想是分治法。在这个实现中,首先找到基准值的位置,然后将数组划分成左右两个子数组。具体过程如下:

  1. 首先调用函数 GetMidi() 来确定基准值的位置 midi,并将 a[begin] 和 a[midi] 交换位置,将基准值放在最左边。
  2. 然后从数组的两端开始向中间扫描。从右边开始,找到第一个小于基准值的元素 a[right],将其赋值到 hole 所在的位置 a[hole],并将 hole 移动到 a[right] 的位置。
  3. 再从左边开始,找到第一个大于基准值的元素 a[left],将其赋值到 hole 所在的位置 a[hole],并将 hole 移动到 a[left] 的位置。
  4. 重复步骤2和3,直到 left = right。
  5. 将基准值 a[begin] 放回 hole 所在的位置。
  6. 对左右两个子数组分别递归调用 QuickSort() 函数进行排序。

        需要注意的是,在该实现中,使用了“挖坑填数”法来进行分割,而不是交换法。这种方法的优点在于可以减少数据交换的次数,提高算法效率。

4、非递归的快排

void QuickSortNonR(int* a, int begin, int end)
{
	Stack 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 midi = GetMidi(a, left, right);
		Swap(&a[left], &a[midi]);

		int l = left;
		int r = right;
		int keyi = l;

		while (l < r)
		{
			while (l < r && a[r] >= a[keyi])
			{
				r--;
			}
			while (l < r && a[l] <= a[keyi])
			{
				l++;
			}
			Swap(&a[l], &a[r]);
		}
		Swap(&a[keyi], &a[l]);
		if (l + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, l + 1);
		}
		if (l - 1 > left)
		{
			StackPush(&st, l - 1);
			StackPush(&st, left);
		}

	}
	StackDestroy(&st);
}

此段使用栈来模拟递归的过程。下面对代码进行分析:

  1. 首先,创建一个栈st,并将begin和end分别压入栈中,表示待排序区间的起始和结束位置。
  2. 进入循环,当栈不为空时执行以下操作:
  3. 弹出栈顶的begin和end值,表示当前待排序区间。
  4. 调用GetMidi函数获取基准值的索引midi,并通过调用Swap函数将基准值移到待排序区间的最左边。
  5. 初始化两个指针l和r,分别指向待排序区间的左边界和右边界。
  6. 初始化一个指针keyi,表示基准值的初始位置。
  7. 进入内层循环,当l小于r时执行以下操作:
  8. 从右向左遍历,找到第一个小于基准值的元素的位置,记为r。 从左向右遍历,找到第一个大于基准值的元素的位置,记为l。
  9. 交换a[l]和a[r]的值。
  10. 交换基准值a[keyi]和a[l]的值,将基准值放回正确的位置。
  11. 如果l + 1 < right,说明基准值右边还有元素需要排序,将右半部分的起始和结束位置压入栈中。如果l - 1 > left,说明基准值左边还有元素需要排序,将左半部分的起始和结束位置压入栈中。

该算法通过使用栈,避免了递归调用带来的额外开销,提高了效率。同时,非递归版本的快速排序也更适合大规模数据的排序。

 注:栈的实现在数据结构中简单的栈和队列-CSDN博客

5、快排的qsort实现

qsort函数是C/C++标准库中的一个排序函数,它可以将一个数组按照指定的排序方式进行排序。

需要注意的是,使用qsort函数排序时需要保证被排序的元素能够作为参数传递给排序函数,同时要注意元素的数量和大小,以免发生越界等问题。

void qsort(void* base,
	size_t num,//无符号整型
	size_t size,
	int (*cmp)(const void*, const void*));

函数指针类型  -这个函数指针指向的函数,能够比较base指向数组中的两个元素

void*的指针(无具体类型的指针) ,不能直接解引用操作,不能直接进行指针运算。void*类型的指针可以接受任意类型的地址。

我使用冒泡排序的思想,实现类似qsort的函数。

void Swap(char* buf1, char* buf2, int size) {
	for (int i = 0; i < size; i++) {
		char tmp;
		tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
void bubble_sort(void* base, size_t num, size_t size, int(*cmp)(const void*, const void*)) {
	for (int i = 0; i < num - 1; i++) {
		for (int j = 0; j < num - 1 - i; j++) {
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0) {
				Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}

int cmp(const void* a, const void* b) {
	return *(int*)a - *(int*)b;
}

二、归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

在此我借用流传较广的图片来给大家理解。

1、递归的归并

定义了一个辅助函数 _MergeSort,该函数用于实现递归的归并排序。

  1. 如果起始位置 begin 大于等于结束位置 end,表示数组只有一个元素或为空,直接返回。
  2. 计算中点位置 mid,将数组分为两个子数组。
  3. 递归调用 _MergeSort 对左半部分子数组进行排序,即调用 _MergeSort(a, tmp, begin, mid)。
  4. 递归调用 _MergeSort 对右半部分子数组进行排序,即调用 _MergeSort(a, tmp, mid + 1, end)。
  5. 声明变量 index 表示当前待合并的位置,以及对四个指针进行赋值,begin1 = begin、end1 = mid、begin2 = mid + 1、end2 = end。
  6. 在循环中比较两个子数组的元素,将较小的元素放入临时数组 tmp 中,并更新相应指针和 index。
  7. 将剩余未处理的元素依次放入 tmp 数组中。
  8. 使用 memcpy 函数将排序后的临时数组 tmp 的数据复制回原数组 a。

归并排序是一种稳定的排序算法。它通过不断将数组划分为两个子数组,然后对子数组进行排序合并,最终得到有序的数组。这种算法的空间复杂度为 O(n),因为需要额外的临时数组来辅助排序过程。

2、非递归的归并

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (NULL == tmp)
	{
		perror("malloc fail");
		return;
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int index = i;
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			printf("before : [%d,%d] [%d,%d]", begin1, end1, begin2, end2);

			if (begin2 >= n)
			{
				printf(" break");
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
		
			printf(" adjust : [%d,%d] [%d,%d]\n", begin1, end1, begin2, end2);

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

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}

			memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
		}
		gap = gap * 2;
		printf("\n");
	}
}

这是一个非递归的归并排序算法实现,使用了循环和迭代的方式来进行归并排序。算法首先申请了一个临时数组tmp用于存放排好序的数值,然后通过不断增大gap的方式来进行归并排序,每次循环中将相邻的两个区间进行合并,直到最终将整个序列全部排序完成。

具体实现中,每次循环时,对于当前的gap,从左到右将相邻的两个长度为gap的区间进行合并,如果右边区间不足长gap,则将右边区间长度调整为剩余元素个数。合并两个区间时,将对应位置上较小的元素复制到tmp数组中,直到某个区间元素全部复制完毕。最后将排好序的tmp数组复制回原数组中对应位置。

三、希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

在此我借用流传较广的图片来给大家理解。

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;

				}
				else
				{
					break;
				}
			}
			a[end+gap] = tmp;
		}
	}
}

具体代码逻辑如下:

  1. 初始化间隔 gap 为数组长度 n。
  2. 当 gap 大于 1 时,进行排序循环。
  3. 更新 gap 的值为 gap 除以 3 后再加 1(这是希尔排序中推荐的一个常用间隔序列)。
  4. 遍历数组,从索引 0 到索引 n - gap - 1。
  5. 在每个遍历位置,记录当前索引为 end,同时记录 end + gap 处的元素值为 tmp。
  6. 在当前位置进行比较和移动操作,如果当前位置的元素大于 tmp,则将当前元素后移 gap 个位置,并将 end 减去 gap。
  7. 如果当前位置的元素小于等于 tmp,则跳出内层循环。
  8. 将 tmp 插入到最终位置 end + gap 处。
  9. 循环结束后,数组中的元素按照指定的间隔有序排列。
  10. 重复步骤 2-9,直到 gap 缩小至 1,完成最后一轮的插入排序。

        总体来说,希尔排序通过先对间隔较大的元素进行排序,逐渐减小间隔,最终完成整个数组的排序。这种排序算法的时间复杂度与具体的间隔序列有关。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无敌岩雀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值