快速排序(三种思路及三数取中的优化)

目录

目录

1.快速排序

        1.1左右指针法

        1.2挖坑法

        1.3前后指针法

        1.4三数取中优化        


1.快速排序

        1.1左右指针法

步骤:

  1. 建立一个keyi保存最左或者最右的下标,我这边选择的是最左(切记不要选择中间不好控制,反而使问题更复杂了);
  2. 定义一个left和right,right从右向左找小,left从左向右找大(注:如果选择最左作为keyi,则需要right先走,left后走;选择最右同理);

                                             如果选择left先走和我们需要的结果不同 

 3.right从右向左找小,如果找到比a[keyi]小的数则停下,再让left从左向右找大,找到比a[keyi]大的left停下,交换a[left]和a[right];然后继续以上过程,直到right和left相遇,出循环交换a[keyi]和相遇位置(left和right都可以)(单趟排序);

4.此时a[keyi]左边的数都小于a[keyi],右边大于的数都a[keyi];

5.然后使用分治的思想,让keyi的左数列和右序列再次进行上面的单趟排序,反复此过程,直到左右数列只有一个数据或者不存在(使用递归);

如图所示:

 

代码如下:

int PartSort1(int* a, int left, int right)//左右指针法
{
	int keyi = left;
	while (right > left)
	{
		//一趟
		while (right > left && a[right] >= a[keyi])//right>left以免越界
		{
			//找小
			--right;
		}
		while (right > left && a[left] <= a[keyi])
		{
			//找大
			left++;
		}
		Swap(&a[left], &a[right]);//交换或者相遇了交换还是没变
	}
	Swap(&a[keyi], &a[left]);//把key交换到正确的位置
	return left;
}
void QuickSort(int* a, int begin, int end)//快速排序
{
	if (begin >= end)
	{
		return;
	}
	//左子树[begin,meet-1]
	//右子树[meet+1,end-1]
		int meet = PartSort1(a, begin, end);
		QuickSort(a, begin, meet - 1);
		QuickSort(a, meet + 1, end);	
}

 1.2挖坑法

步骤:

  1. 建立key保存最左或者最右,把此位置作为坑()
  2. 定义一个left和right,right从右向左找小,left从左向右找大(注:如果选择最左作为keyi,则需要right先走,left后走;选择最右同理
  3. right从右向左找小,如果找到比key小的数则停下,把a[right]放入坑中然后在a[right]位置形成新坑;再让left从左向右找大,找到比key大的left停下,把a[left]放入坑中然后在a[left]位置形成新坑;然后继续以上过程,直到right和left相遇,把key放入相遇的那个位置的坑(left和right都可以)(和左右指针法思路大体相同)(单趟排序);
  4. 此时key左边的数都小于key,右边大于的数都key;

  5. 然后使用分治的思想,让keyi的左数列和右序列再次进行上面的单趟排序,反复此过程,直到左右数列只有一个数据或者不存在(使用递归);

如图所示: 

 代码如图:

int PartSort2(int* a, int left, int right)//挖坑法
{
	int key = a[left];
	while (left < right)
	{
		//找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		//把数放在左边的坑,右边形成新坑
		a[left] = a[right];
		//找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		//把数放在右边的坑,左边形成新坑
		a[right] = a[left];
	}
	//相遇了,把key放在相遇的坑
	a[left] = key;
	return left;
}
void QuickSort(int* a, int begin, int end)//快速排序
{
	if (begin >= end)
	{
		return;
	}

	//左子树[begin,meet-1]
	//右子树[meet+1,end-1]
		int meet = PartSort2(a, begin, end);
		QuickSort(a, begin, meet - 1);
		QuickSort(a, meet + 1, end);

}

1.3前后指针法

步骤:

  1. 建立一个keyi保存最左或者最右的下标,我这边选择的是最左(切记不要选择中间不好控制,反而使问题更复杂了);
  2. 定义一个前后指针cur和prev,prev=left,cut=prev+1(注:请在单趟排序中也要保持一前一后);
  3. cur从左到右找比a[keyi]小的数据,找到让prev++后交换a[prev]和a[cur],交换完成后cur++,重复以上过程直到cur>right(最后一个元素的下标),出循环交换a[prev]和a[key](单趟排序);
  4. 此时a[keyi]左边的数都小于a[keyi],右边大于的数都a[keyi];
  5. 然后使用分治的思想,让keyi的左数列和右序列再次进行上面的单趟排序,反复此过程,直到左右数列只有一个数据或者不存在(使用递归);

如图所示:

 代码如图:

int PartSort3(int* a, int left, int right)//前后指针法
{

	int keyi = left;
	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		while(cur <= right && a[cur] >= a[keyi])
		{
			cur++;
		}
		if (cur > right)
			break;
		Swap(&a[++prev], &a[cur]);
		cur++;//保持perv和cur一前一后
		//if(a[cur] < a[keyi] && ++prev != cur)//这个方法更简洁
		//{
		//	Swap(&a[cur], &a[prev]);
		//}
		//cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSort(int* a, int begin, int end)//快速排序
{
	if (begin >= end)
	{
		return;
	}

	//左子树[begin,meet-1]
	//右子树[meet+1,end-1]
		int meet = PartSort3(a, begin, end);
		QuickSort(a, begin, meet - 1);
		QuickSort(a, meet + 1, end);

}

1.4三数取中优化

思想:

如果数组为一些极端情况(顺序),时间复杂度为O(n2),效率太低为了解决这种情况出现了三数取中,就是取头 ,尾,((头+尾)/2)的中位数,使快排的时间复杂度得到优化(三数取中很重要

如图所示:

 

使用代码如图:

int GetMidIndex(int* a, int left, int right)//三数取中
{
	int mid = (left + right) >> 1;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if(a[mid]>a[right])
		{
			if (a[left] > a[right])
			{
				return left;
			}
			else
			{
				return right;
			}
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[mid] < a[right])
		{
			if (a[right] > a[left])
			{
				return left;
			}
			else
			{
				return right;
			}
		}
	}
}
int PartSort1(int* a, int left, int right)//左右指针法
{
	int midIndex = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midIndex]);
	int keyi = left;
	while (right > left)
	{
		//一趟
		while (right > left && a[right] >= a[keyi])//right>left以免越界
		{
			//找小
			--right;
		}
		while (right > left && a[left] <= a[keyi])
		{
			//找大
			left++;
		}
		Swap(&a[left], &a[right]);//交换或者相遇了交换还是没变
	}
	Swap(&a[keyi], &a[left]);//把key交换到正确的位置
	return left;
}
<think>嗯,我现在要了解一下快速排序中的两种选基准值的方法:随机选三数取中。首先,我应该回顾一下快速排序的基本原理,这样有助于理解为什么需要这两种方法。 快速排序是通过分治的策略来排序数组的。基本步骤是选择一个基准值(pivot),然后将数组分成两部分,一部分比基准值小,另一部分比基准值大,再递归地对这两部分进行排序。这里的关键在于基准值的选择,如果选得不好,可能会导致分割不均匀,影响算法效率。最坏情况下,时间复杂度会退化到O(),比如当数组已经有序的时候,每次都选到最小或最大元素作为基准,这时候分割效率很低。 那为什么需要改进基准值的选择呢?原来的快速排序可能选第一个元素或者最后一个元素作为基准,这在某些情况下效率不高。所以人们提出了随机选三数取中的方法来优化基准的选择,避免最坏情况的发生。 先看随机选基准值的方法。这种方法就是在每次递归调用时,随机从当前子数组中选择一个元素作为基准。这样做的好处是理论上可以避免总是选到最坏情况的基准值,从而使得算法在平均情况下的时间复杂度保持O(n log n)。不过,随机数生成本身需要一定的计算时间,虽然对时间复杂度的影响不大,但可能会稍微增加常数因子。另外,这种方法虽然减少了最坏情况出现的概率,但并不能完全杜绝,只是在实际应用中,概率变得非常低。 然后是三数取中法。这个方法的基本思路是从当前子数组的首、中、尾三个位置出元素,然后这三个数的中位数作为基准值。这样做的好处是能够有效避免选到极端值(比如最大值或最小值),从而使得分割更均匀。特别是对于已经部分有序的数组,这种方法能够显著提高性能。不过,三数取中的实现稍微复杂一些,需要比较三个数并找出中位数,这会增加一定的比较次数,不过这些比较的时间开销相对整个排序过程来说是可以忽略的。 现在需要比较这两种方法的优缺点。随机法的优点是实现简单,能够有效避免恶意构造的数据导致的最坏情况,适用于一般情况。但它的缺点是无法完全避免最坏情况,只是概率低,而且随机数生成可能有一定开销。三数取中的优点是确定性,不需要随机数生成,对于部分有序的数组效果更好,能有效减少最坏情况的发生,特别是针对已经有序或接近有序的数据。缺点是实现稍微复杂,需要更多的比较操作,而且在某些特定情况下可能仍然无法完全避免不均匀的分割。 另外,可能需要考虑这两种方法在实际应用中的表现。比如,在标准库的实现中,很多会采用三数取中或者类似的策略,因为实际数据可能存在部分有序的情况,而随机法虽然理论保障强,但实际中的常数因子可能更高。不过,有些实现可能会结合两种方法,比如在三数取中的基础上再随机选择一个元素,以增加鲁棒性。 还需要考虑时间复杂度。两种方法的目标都是将平均时间复杂度保持在O(n log n),而最坏情况的时间复杂度在随机法中通过概率被降低到几乎不可能发生,而在三数取中法中,通过选择中位数作为基准,使得分割更均匀,从而减少最坏情况的可能性。不过,严格来说,三数取中并不能保证绝对的最坏情况避免,比如当数组中有很多重复元素或者特定模式时,可能仍然存在分割不均的情况。 总结一下,两种方法都是为了优化基准值的选择,从而提高快速排序的效率。选择哪一种决于具体的应用场景和数据特点。如果数据分布未知或者担心恶意输入,随机法可能更合适;如果数据通常部分有序,三数取中可能更有效。有时候,两种方法可以结合使用,以获得更好的效果。</think>快速排序中,基准值(pivot)的选直接影响算法效率。以下详细介绍两种常见优化方法:**随机选基准值**和**三数取中基准值**,并对比其优缺点。 --- ### 一、随机选基准值 #### 1. 方法 每次递归时,从当前子数组中**随机选择一个元素**作为基准值,再通过交换操作将其移动到子数组的起始或末尾位置,最后进行常规的分区操作。 #### 2. 实现步骤 - **步骤1**:随机生成一个下标 $i$,满足 $left \leq i \leq right$($left$ 和 $right$ 为当前子数组的边界)。 - **步骤2**:将 $arr[i]$ 与 $arr[left]$(或 $arr[right]$)交换,使随机值成为基准。 - **步骤3**:执行标准分区操作。 #### 3. 优点 - **避免最坏情况**:理论上,随机化使算法在概率上避免最坏时间复杂度 $O(n^2)$。 - **适用性广**:对输入数据分布无假设,尤其适合未知或可能包含恶意构造的数据。 #### 4. 缺点 - **额外开销**:随机数生成需少量时间,但影响可忽略。 - **概率性保障**:最坏情况仍存在,但概率极低。 --- ### 二、三数取中基准值 #### 1. 方法 从当前子数组的**首、中、尾**三个位置元素,选择三者的**中位数**作为基准值。 #### 2. 实现步骤 - **步骤1**:计算中点下标 $mid = left + \lfloor (right - left)/2 \rfloor$。 - **步骤2**:比较 $arr[left]$、$arr[mid]$、$arr[right]$,将中位数交换到 $arr[left]$ 作为基准。 - **步骤3**:执行标准分区操作。 #### 3. 优点 - **确定性优化**:无需随机数生成,直接避免极端值(如最小值或最大值)成为基准。 - **高效处理部分有序数据**:对已部分排序的数组(如升序、降序)表现更优。 #### 4. 缺点 - **无法完全避免最坏情况**:若数据有特殊模式(如重复元素过多),仍可能分割不均。 - **额外比较操作**:需三次比较确定中位数,但对整体性能影响极小。 --- ### 三、对比与总结 | **方法** | **时间复杂度** | **适用场景** | **核心优势** | |----------------|-------------------------|----------------------------------|----------------------------------| | 随机选基准值 | 平均 $O(n \log n)$ | 数据分布未知或需避免确定性最坏情况 | 概率性保障最坏情况几乎不出现 | | 三数取中 | 平均 $O(n \log n)$ | 数据部分有序或需确定性优化 | 减少极端值影响,实现简单稳定 | #### 选择建议 - 若数据可能存在**人为构造的最坏情况**(如攻击场景),优先选择**随机法**。 - 若数据**通常部分有序**(如实际业务场景),优先选择**三数取中**。 - 部分库(如C++ STL的`std::sort`)会结合两种策略,进一步优化性能。 --- ### 四、示例代码(三数取中法) ```python def quick_sort(arr, left, right): if left < right: pivot_index = partition(arr, left, right) quick_sort(arr, left, pivot_index - 1) quick_sort(arr, pivot_index + 1, right) def median_of_three(arr, left, right): mid = (left + right) // 2 # 比较三个数并返回中位数的下标 if arr[left] < arr[mid]: if arr[mid] < arr[right]: return mid elif arr[left] < arr[right]: return right else: return left else: if arr[left] < arr[right]: return left elif arr[mid] < arr[right]: return right else: return mid def partition(arr, left, right): # 三数取中并交换到 left 位置 pivot_idx = median_of_three(arr, left, right) arr[left], arr[pivot_idx] = arr[pivot_idx], arr[left] pivot = arr[left] # 标准分区操作 i = left + 1 j = right while True: while i <= j and arr[i] <= pivot: i += 1 while i <= j and arr[j] >= pivot: j -= 1 if i > j: break arr[i], arr[j] = arr[j], arr[i] arr[left], arr[j] = arr[j], arr[left] return j ``` --- 通过合理选择基准值,快速排序能在绝大多数场景下保持高效稳定的性能。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值