排序(5)——快速排序算法hoare

本文详细介绍了快速排序的基本思想、单趟和多趟排序方法,包括hoare版本、错误点及解决方案,同时讨论了性能优化策略如随机选择基准值和三数取中。
摘要由CSDN通过智能技术生成

快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。

基本思想

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

单趟基本思想(选取分界线位置keyi):

  • 任取待排序元素序列中的某元素(最左边/最右边的数)作为基准值a[keyi]
  • 按照该排序码将待排序集合分割成两子序列
  • 左子序列中所有元素均小于基准值
  • 右子序列中所有元素均大于基准值

整体基本思想:

  • 然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
  • 核心是区间

单趟基本思路有3中方式去实现:

  • hoare版本
  • 挖坑版本
  • 前后指针版

整体思路 hoare版本

单趟 

留下疑问:为什么相遇位置一定比key小? 

多趟 

左边有序+右边有序=整体有序

把它看作二叉树,递归。 

代码实现 

单趟

void QuickSort(int* a, int n)
{
	int left = 1, right = n - 1;
	int keyi = 0;

	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]);

}
易错点

我们可能会写成下面这样的代码。

void QuickSort(int* a, int n)
{
	int left = 0, right = n - 1;
	int key=a[left];

	while (left < right)
	{
		// 右边找小
		while (a[right] > key)
		{
			--right;
		}

		// 左边找大
		while (a[left] < key)
		{
			++left;
		}

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

	Swap(&a[left], &key);

}

【1】临界状态:left的值不变。

只有当a[left] < key的时候,left才会++,而left最开始是0,它的值等于key,所以left会一直不变。

  • 解决方法:将left的值改为1,并且key=a[0]。 

【2】相遇判断有问题。

while (a[right] > key)  while (a[left] < key)
这里只能保证找到符合条件的值然后++left或者--right,并不能保证什么时候停下。而我们停下的条件是两者相遇,所以要在两个while里都加上一个条件。

  • 解决办法:while (left < right && a[right] >= a[keyi])   while (left < right && a[left] <= a[keyi])

【3】与key交换出错。 

Swap(&a[left], &key);

key是局部变量,最后交换的是与局部变量交换的。而我们希望的是与key存储的位置进行交换,也就是数组里的那个位置。

  • 解决办法: int keyi = 0;Swap(&a[left], &a[keyi]);

多趟 

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

	int left = begin, right = end;
	int keyi = begin;

	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;

	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

分为 [begin, keyi-1]   keyi   [keyi+1, end] ,两边分别递归下去,最后会排成有序。

提问:if (begin >= end)代表的是只有一个值或者不存在该区间的情况。一个值很好理解,那么为什么会有不存在的情况呢?

例如我们这里的4 5,4的下标是3,5的下标是4,那么接下来keyi就是4,[keyi+1, end]这个区间就是不存在的。

 易错点

【1】写成 int left = begin+1;这样就会导致有序的情况下出错。

比如 4 5 6 7 ,left指向5,right指向7,begin和keyi指向4,right一直找不到小,直到找到5,left==right,跳出循环,此时4和5交换Swap(&a[left], &a[keyi]); 就会导致出错。

  • 解决办法:直接改为int left = begin;

【2】写成while (left < right && a[right] > a[keyi]) while (left < right && a[left] <a[keyi])死循环

比如一组数据是: 2 1 2 6 8 2  这样keyi指向2,而a[right] ==2永远也不可能>2,那么right就会一直不动,当left来到2的时候,a[left] <a[keyi]不再满足,所以left也停下了,然后就是交换2和2,接下来程序继续,一直都会卡在这里,死循环。

  • 解决办法: 加上等号判断
  • while (left < right && a[right] >= a[keyi]) while (left < right && a[left] <= a[keyi])

性能分析 

最理想的状态就是,每一次找都可以二分。时间复杂度是O(N*logN)

 

  • 当数据有序时,快排就会很吃力,这是为什么呢?

因为有序时,left要找小,right要找大,都找不到,就会一直从头找到尾,时间复杂度就会上升到O(N^2)

解决: 

【1】随机值选key

【2】三数取中,选不是最大也不是最小的那个数做key(与begin交换)。改成以下代码:

int GetMidi(int* a, int begin, int end)
{
	int midi = (begin + end) / 2;
	// begin end midi三个数选中位数
	if (a[begin] < a[midi])
	{
		if (a[midi] < a[end])
			return midi;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else
	{
		if (a[midi] > a[end])
			return midi;
		else if (a[begin] < a[end])
			return begin;
		else
			return end;
	}
}

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

	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);

	int left = begin, right = end;
	int keyi = begin;

	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;

	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi+1, end);
}

问题解决

上面我们留下疑问:为什么相遇位置一定比key小? (不考虑相遇位置是key)

因为右边先走!

相遇有两种情况,要不就是右边遇到左边,要不就是左边遇到右边

  • 右遇左:3 8 4 9 2 6 右边找小到2,左边找大找到8,交换后继续找,右边找不到小了,直接找到left处,二者相遇,此地时左边刚才找到的,且已经与2交换,所以此时这个位置一定比3小。
  • 左遇右: 5 3 2 4 7 右边找小找到4,左边找不到大,二者相遇。相遇位置一定比key小。

注意:我们这一篇写的是最左边作key,让right先走。相反的,如果让右边作key,是right先走。

  • 16
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值