快排(算法思想、代码实现、优化)

快速排序是一种基于分治法的排序算法,平均时间复杂度为O(nlogn),最坏情况为O(n²)。文中介绍了快排的思想,包括如何找到居中的数(hoare方法和快慢指针法)以及如何递归地对区间进行操作。此外,还讨论了通过选择区间的中值来优化快排,以避免最坏情况的发生。
摘要由CSDN通过智能技术生成

        快排作为一个广泛应用的排序算法,顾名思义,时间复杂度较低。在c++标准库stdlib.h中提供了这个函数(qsort)。现在让我们对快排进行一些了解。

一、算法思想

快排采用分治法的思想。时间复杂度平均为O(nlogn),最坏情况下为O(n²)。

将快排分为两部分:

①:找到一个区间的数,将其居中。 O(n)

②:对若干区间重复一过程。            O(logn)~ O(n)

为什么要分为这两个部分呢?

以该数组为例:

 ​​​

        假设我们取最左边的数(4),将其居中:(先不考虑除4以外其它数之间的位置顺序,等一下会进行说明)(①操作)

 

         在一次居中后:4已经排列到它最终应该所在的位置。而此时,区间被分为两部分:比4大、比4小。(②操作)

我们再对两个区间执行①操作:

 

        2、4、5都排列到最终位置。依此类推,当区间长度为1时不可再分,且每个数字都已经排列到它最终所在的位置,排序完成。

        相信大家已经注意到了,我们以区间为单位进行划分(而不是以单个数字为单位O(n)),每次都将区间对半分,再对各个区间进行操作,这就是分治法的思想。

        那么,我们应该如何实现呢?

二、代码实现(以从小到大排序为例)

①:

在这里,我运用两种方法实现①操作

(1)hoare方法    也是快排发明者本人的方法

思想:

        将最左边的数记为key

        定义两个指针,分别指向头部(left)和尾部(right)。

        左指针向右移动,直到左指针大于key;右指针向左移动,直到右指针小于key。

        将左右指针指向的数互换。

        重复以上过程,直到左指针和右指针重合。

        最后将key和重合处互换即可。

图解:

代码实现:

void MySwap(int* num1, int* num2)
{
	int temp = *num1;
	*num1 = *num2;
	*num2 = temp;
}
int FindKey(int* nums, int left, int right)//hoare方法
{
	int key = left;
	
	while (left < right) {
		
		if (nums[key] <= nums[right]) {	//right指向的数大于等于key就--,直到right指向的数小于key
			right--;					
		}
		else if (nums[key] >= nums[left]) {			//left指向的数小于等于key就++,直到left指向的数大于key
			left++;						
		}
		else {									//当left指向的数大于key,right指向的数小于key时,交换位置
			MySwap(&nums[left], &nums[right]);
		}
	}
	//最后退出循环必然是left == right,且由于right判断在前,最后必然是right走向left,重合处为left指向的数(小于key)
	//因此,最后将key与left交换(必然小于key)	注:当key初始取left时,right判断在前;key初始取right时,left判断在前。
	//因为当key初始取left时,right判断在前的话,最后重合处为right指向的数(大于key),所以要和right - 1(小于key)交换
	//可能引发越界等问题。比如只有一个数时,一开始就重合,而key要和right - 1(-1)互换,导致越界。
	MySwap(&nums[key], &nums[left]);
	key = left;
	return key;
}

在使用hoare方法时,要注意是左指针先判断还是右指针先判断。
        因为当key初始取left时,right判断在前的话,最后重合处为right指向的数(大于key),所以要和right - 1(小于key)交换。
        而这时可能引发越界等问题。比如只有一个数时,一开始就重合,而key要和right - 1(-1)互换,导致越界。

 结论:当key初始取left时,right判断在前;key初始取right时,left判断在前。

 (2)快慢指针法

思想:

        以最左边为key。

        fast一直往右遍历,

        当fast指向的数小于key指向的数,将fast指向的数和slow交换,slow++。

        直到fast遍历完成。

        最终将key和slow - 1互换。

        这保证了slow - 1永远指向小于等于key的数,小于等于key的数都会被换到左边。

图解:

 

 

 

 

 

 代码:

int FindKey(int *nums,int left, int right)//快慢指针法
{
	int key = left;
	int fast = left + 1, slow = left + 1;			//快慢指针初始指向key的下一个

	while (fast <= right) {
		if (nums[key] > nums[fast]) {		
			MySwap(&nums[slow], &nums[fast]);		//慢指针指向的数小于等于key指向的数,
			slow++;									//保证了慢指针左边小于等于key,右边大于key
		}
		fast++;										//fast始终在移动
	}
	MySwap(&nums[slow - 1], &nums[key]);			//最后将key和慢指针互换,注意是slow - 1,因为slow++了一次
	key = slow - 1;
	return key;
}

 ②

void SortArray(int* nums,int left,int right)
{
	if (left >= right)return;					// >= 而不是 == ,因为有可能right < left
	int key = FindKey(nums, left, right);
	SortArray(nums, left, key - 1);				//key位置固定,不能再直接传入key,以免死循环
	SortArray(nums, key + 1, right);
}

 三、优化

刚刚我们提到,快排在最坏情况下时间复杂度为O(n²)。

①部分为O(n),②部分为O(logn)~O(n)

那么很显然,问题出在②。

②部分是对若干个区间进行操作,而区间的划分未必是二等分的。

在我们选取key值时,key的值未必是区间的中位数,这导致了区间的长度不一样。

而最坏的情况下,我们每次选取key值都为区间的最值。

设区间长度为n,那么每次划分的区间长度分别为n - 1和 1,而不是n/2和n/2,这样就相当于每次排列一个数字,排列n次,O(n)。

针对这种情况,我们不难想到,能不能每次取key时都找区间的中位数。答案是可以的。但是考虑到寻找中位数还要花费许多时间,我们退而求其次,每次比较区间的两端和中点,选取三者的中值(O(1)),也能达到很好的效果.

代码实现:

int GetMidNum(int* arr, int left, int right)//三者取中值
{
	int mid = (left + right) / 2;

	//  先取出 最左端、最右端、中间的数据
	//  比较出大小后返回的是下标
	if (arr[left] > arr[right])
	{
		if (arr[mid] < arr[right])
		{
			return right;
		}
		else if (arr[left] > arr[mid])
		{
			return mid;
		}
		else
		{
			return left;
		}
	}
	else
	{
		if (arr[left] > arr[mid])
		{
			return left;
		}
		else if (arr[right] > arr[mid])
		{
			return mid;
		}
		else
		{
			return right;
		}
	}
}
int FindKey(int *nums,int left, int right)//快慢指针法
{
	int Mid = GetMidNum(nums, left, right);
	MySwap(&nums[Mid], &nums[left]);

	int key = left;
	int fast = left + 1, slow = left + 1;			//快慢指针初始指向key的下一个

	while (fast <= right) {
		if (nums[key] > nums[fast]) {		
			MySwap(&nums[slow], &nums[fast]);		//慢指针指向的数小于等于key指向的数,
			slow++;									//保证了慢指针左边小于等于key,右边大于key
		}
		fast++;										//fast始终在移动
	}
	MySwap(&nums[slow - 1], &nums[key]);			//最后将key和慢指针互换,注意是slow - 1,因为slow++了一次
	key = slow - 1;
	return key;
}

这样就使时间复杂度平均在O(nlogn)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值