数据结构——排序(2)

上一篇文章我们讲解了插入排序,选择排序以及交换排序,此篇文章着重于交换排序中的快速排序的优化以及归并排序、计数排序的讲解。

正文开始:

1. 快速排序的优化

1.1 三数取中

上一篇文章我们了解到,快速排序每次都会取第一个元素作为key,但是当数组接近有序的时候,此时快速排序的时间复杂度会退化成 O(N^2) ,因此,为了解决这个问题,我们可以运用三数取中的方法来优化,代码如下:

int GetMidNum(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[end])
	{
		if (a[end] < a[mid])
		{
			return end;
		}
		else if (a[mid] < a[begin])
		{
			return begin;
		}
		else
		{
			return mid;
		}
	}
	// end > begin
	else
	{
		if (a[begin] > a[mid])
		{
			return begin;
		}
		else if (a[end] < a[mid])
		{
			return end;
		}
		else
		{
			return mid;
		}
	}
}

这时每次快排都会把数组分为两组数量几乎相等的两部分,时间复杂度就会变成O(N*logN) 

1.2 挖坑法

我们了解到,Hoare的快速排序需要注意很多细节,处理起来比较繁琐,因此还有其他方法来实现快速排序,其中之一就是挖坑法:

  • 首先将首元素作为key,并将key所在位置的值放在tmp中,此时“坑”就相当于是首元素;
  • 从右向左找比tmp小的值,找到就将此位置的值填到坑里,此位置就变为“坑”;
  • 从左向右找比tmp大的值,找到就将此位置的值填到坑里,此位置就变为“坑”;
  • 一直到左右相遇,把tmp放到坑中,就完成第一次快排,之后继续递归。

动图如下:

代码如下:

//挖坑
int PartSort2(int* a, int begin, int end)
{
	int mid = GetMidNum(a, begin, end);
	Swap(&a[begin], &a[mid]);
	int holei = begin;
	int tmp = a[begin];
	while (begin < end)
	{
		while (begin < end && a[end] >= tmp)
		{
			--end;
		}
		a[holei] = a[end];
		holei = end;
		while (begin < end && a[begin] <= tmp)
		{
			++begin;
		}
		a[holei] = a[begin];
		holei = begin;
	}
	a[holei] = tmp;
	int keyi = holei;
	return keyi;
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort2(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

1.2 双指针法

另一种方法就是双指针法:

  • 将首元素的下标当作key;
  • 定义两个下标cur 和 prev,cur先走,找比key小的值;
  • 找到之后prev再走一步,交换cur和prev所在位置的值;
  • 直到cur超过数组的范围n,就停止,此时prev所在的位置就当成key,并进行递归。

动图如下:

代码如下:

//双指针
int PartSort3(int* a, int begin, int end)
{
	int mid = GetMidNum(a, begin, end);
	Swap(&a[begin], &a[mid]);
	int keyi = begin;
	int prev = begin;
	int cur = prev + 1;
	while (cur <= end)
	{
		if (a[cur] < a[keyi] && (++prev) != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	return keyi;
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort3(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

运行截图如下:

1.3 非递归版本的快排

我们可以用栈来模拟非递归版本的快排:

  • 首先将最后一个元素和第一个元素的下标入栈(注意顺序);
  • 然后取两次Top(一个作为begin,另一个作为end),并Pop掉,进行第一次快排;
  • 之后,将end、keyi + 1、keyi - 1、begin 依次入栈,再进行接下来的快排。

代码如下:

void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	STInit(&st);
	STPush(&st, begin);
	STPush(&st, end);
	while (!STEmpty(&st))
	{

		int right = STTop(&st);
		STPop(&st);
		int left = STTop(&st);
		STPop(&st);
		int keyi = PartSort3(a, left, right);
		
		if (keyi + 1 < right)
		{
			STPush(&st, keyi + 1);
			STPush(&st, right);
		}

		if (left < keyi - 1)
		{
			STPush(&st, left);
			STPush(&st, keyi - 1);
		}
		
	}
	
	STDestroy(&st);
}

运行结果:

注意以上的方法只是对Hoare的方法进行了优化,对于时间复杂度和空间复杂度没有什么质的飞跃,平均时间复杂度依旧是 O(N*logN),平均空间复杂度依旧是 O(logN)


2. 归并排序

归并排序是一种经典的分治算法,它的基本思想是将一个大的数组划分为两个相等大小的子数组,然后递归地对子数组进行排序,最后将已经排好序的子数组合并成一个有序的数组。

以下是归并排序的基本步骤:

  1. 分割: 将原始数组分成两个子数组,这个过程是递归的,直到每个子数组只包含一个元素。

  2. 排序: 递归地对每个子数组进行排序。

  3. 合并: 合并两个已排序的子数组,生成一个新的有序数组。

  4. 递归终止条件: 当数组的大小为1时,不再继续递归,直接返回。

归并排序的关键在于合并两个已排序的子数组。合并过程中,需要比较两个子数组中的元素,并按照顺序将它们合并到一个临时数组中。最后,将临时数组中的元素拷贝回原始数组的相应位置,完成合并。

动图如下:

2.1 归并排序的递归版本 

代码如下:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
    // 分成两组,最后会变成一个一个比较
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin1;
	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 begin, int end)
{

	int* tmp = (int*)malloc(sizeof(int) * (end - begin + 1));
	if (tmp == NULL)
	{
		perror("malloc fail!");
		exit(-1);
	}
	_MergeSort(a, begin, end, tmp);
}

运行结果:

2.2 归并排序的非递归版本

 代码如下:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail!");
		exit(-1);
	}
	int gap = 1;
	while (gap < n)
	{
		int i = 0;
		for (i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
            // end1 和 begin2 越界,不需要处理
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
            // end2 越界, 需要拷贝
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			int j = i;
			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));
		}
		gap *= 2;
	}
}

运行如下:

归并排序的特性总结:

  • 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(N)

3. 计数排序

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

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

代码如下:

void CountSort(int* a, int n)
{
	int max = a[0];
	int min = a[0];
	for (int i = 0; i < n; ++i)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	int range = max - min + 1; // 注意这里的加1
	int* count = (int*)calloc(range,sizeof(int));
	if (count == NULL)
	{
		perror("calloc error");
		exit(-1);
	}
	for (int j = 0; j < n; j++)
	{
		count[a[j] - min]++;
	}
	for (int k = 0; k < range; k++)
	{
		while (count[k]--)
		{
			printf("%d ", k + min);
		}
	}
}

运行结果:

计数排序的特性总结:

  • 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  • 时间复杂度:O(MAX(N,Range))
  • 空间复杂度:O(Range)
  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值