【数据结构】快速排序

从两种框架,三种思想来展开对快速排序的叙述

两种框架:递归与非递归

三种思想:左右交换法、挖坑法、前后指针法

但三种思想整体来说就是要找一个key值,把比key小的放在key左边,把比key大的放在key右边,然后再从[0,key-1][key+1,end]这两个区间排序,一直递归或者迭代,直到要排序的节点只有一个的时候,即为有序。

目录

一.递归

0.递归框架

1.左右交换法

2.挖坑法

3.前后指针法

二.非递归

0.非递归框架

三.时间复杂度/优化(避免最坏情况)/小区间优化/稳定性

1.时间复杂度

2.优化(避免最坏情况)

3.小区间优化

4.稳定性


一.递归

0.递归框架

递归框架可以类比成二叉树中的先序,先整体排一遍之后,递归左,再递归右... ...

//递归
void QuickSort(int* arr, int begin, int end)
{
    //只有一个节点
	if (begin >= end)
	{
		return;
	}
	int left = begin, right = end;
	//三种方法
	int key = //PartSort1(arr, left, right);//左右交换法
			  //PartSort2(arr, left, right);//挖坑法
				PartSort3(arr, left, right);//前后指针法
    //[begin,key-1]
	QuickSort(arr, begin, key - 1);
    //[key+1,end]
	QuickSort(arr, key + 1, end);
}

1.左右交换法

思想:默认选最左侧的一个数为key,先从右向左找比key小的,再从左向右找比key大的,交换两数。循环至左右指向同一个数时停止,这时将最左侧的key值与左右指针指向的数交换。

注意:这个思想有三处容易错的地方

1.为了保证最后一次交换一定是要比key小的值,比key值小才可以交换到key值左边,所以要先从右找比key小的。

2.不管是从右找小还是从左找大一定要在循环中加上left<right条件,否则可能发生越界

3.在从右找小或者从左找大的过程中,等于key的情况不能交换,否则可能发生死循环

int PartSort1(int* arr, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
        //从右找小
		while (left < right && arr[right] >= arr[keyi])
		{
			right--;
		}
        //从左找大
		while (left < right && arr[left] <= arr[keyi])
		{
			left++;
		}
        //交换
		Swap(&arr[right], &arr[left]);
	}
    //循环结束后,交换最后一次,交换最左侧的key值与左右指针同时指向的值
	Swap(&arr[left], &arr[keyi]);
	keyi = left;
    
	return keyi;
}

2.挖坑法

思想:把最左边的值看作坑,把值用pit保存起来。从右开始找比pit小的数,找到后把此位置的值放到坑中,把此位置看作是新的坑;从左开始找比pit大的数,找到后把此位置的值放到坑中,把此位置看做是新的坑。循环结束后,左右指针指向同一个位置,此位置必然是坑,把一开始最左边的数pit放到坑中。

int PartSort2(int* arr, int left, int right)
{
	int pit = arr[left], piti = left;
	while (left < right)
	{
        //从右找小
		while (left < right && arr[right] >= pit)
		{
			right--;
		}
        //放到坑中
		arr[piti] = arr[right];
        //把自己变成新的坑
		piti = right;
        //从左找大
		while (left < right && arr[left] <= pit)
		{
			left++;
		}
        //放到坑中
		arr[piti] = arr[left];
        //把自己变成新的坑
		piti = left;
	}
    
	arr[piti] = pit;
	return piti;
}

3.前后指针法

思想:定义两个指针,prev指向最左边,cur = prev+1;从cur位置出发,开始寻找比最左边的key值小的数,找到了就先++prev,如果prev!=cur再交换prev与cur位置的值,不管找没找到,都++cur,将数组每个数挨个遍历,最后再交换最左边的key值与prev。

int PartSort3(int* arr, int left, int end)
{
	int prev = left, cur = prev + 1;
	int keyi = left;
	while (cur <= end)
	{
		if (arr[cur] < arr[keyi] && ++prev != cur)
		{
			Swap(&arr[prev], &arr[cur]);
		}
		cur++;
	}
	Swap(&arr[keyi], &arr[prev]);
	return prev;
}

二.非递归

0.非递归框架

使用栈数据结构,来模拟二叉树的先序递归形式。

思想:(以栈数据结构举例,如果这里使用队列也可以,但是队列更像二叉树的层序遍历)

将要排序的空间首位成对入栈,需要排序时再成对出栈,每排完一次,如果符合条件,就将区间首位再次压入栈中,直到迭代至栈为空。

void QuickSort(int* arr, int begin, int end)
{
	Stack sk;
	StackInit(&sk);
    
	StackPush(&sk, end);
	StackPush(&sk, begin);

	while (!StackEmpty(&sk))
	{
		int left = StackTop(&sk);
		StackPop(&sk);
		int right = StackTop(&sk);
		StackPop(&sk);

		//三种方法
		int key = //PartSort1(arr, left, right);//左右交换法
				  //PartSort2(arr, left, right);//挖坑法
			        PartSort3(arr, left, right);//前后指针法
		if (key < right)
		{
			StackPush(&sk, right);
			StackPush(&sk, key + 1);
		}
		if (left < key)
		{
			StackPush(&sk, key - 1);
			StackPush(&sk, left);
		}
	}
}

三.时间复杂度/优化(避免最坏情况)/小区间优化/稳定性

1.时间复杂度

这是一张快速排序的逻辑图

时间复杂度:O(N*logN)

空间复杂度:O(logN)

讨论最坏情况:从此图可以看出每次排序好之后,接近二分,但如果是完全有序或者完全逆序

左右的情况就不再是n/2,拿完全有序来说,就是一个左1且右n-1的情况。这样排序下去就会有n层。

最坏情况:一组数据完全有序或者完全逆序,快排的时间复杂度是O(N^2)

2.优化(避免最坏情况)

为了尽可能的避免这种最坏情况的发生,在选key的时候,就不能默认从最左开始选了。

方法:可以采用三数取中,就是从最左,最右,中间,三个数中选出一个不大也不小的数当做key

把这个不大也不小的数的下标记录下来,与最左侧的数交换一下值。这样上面三种方法的代码不需

要很大的改动,仍然选取最左侧为key值,只不过我们把最左侧的这个数换为了一个最合理的数。

    int midi = GetMidIndex(arr, left, right);
    Swap(&arr[midi], &arr[left]);
int GetMidIndex(int* arr, int begin, int end)
{
	int mid = begin + (end - begin) / 2;
	if (arr[begin] > arr[end])
	{
		if (arr[mid] > arr[begin])
			return begin;
		else if (arr[end] > arr[mid])
			return end;
		else
			return mid;
	}
	else //arr[begin] < arr[end]
	{
		if (arr[mid] > arr[end])
			return end;
		else if (arr[begin] > arr[mid])
			return begin;
		else
			return mid;
	}
}

3.小区间优化

仍然使用这张图来解释一下,小区间优化问题。

我们解决了快排的最坏情况之后,快排时间复杂度为O(N*logN)

但是仔细思考一下,可不可以再进一步优化呢?让效率更优一点

递归的最大缺点就是有一个很强的限制性因素,当数据量太大时,递归很容易栈溢出,从而导致程序崩溃!

既然快排所使用的递归是一个类似于二叉树的前序遍历,那么我们从二叉树的角度出发来看一下!

例如:一个n层的二叉树,总结点个数是2^n-1,最后一层的节点个数是2^(n-1)

经过数学计算,2^n-1看作2^n,2^n = 2^(n-1) * 2,最后一层的节点个数就占了总结点个数的一半

当递归划分小区间,区间比较小的时候,就不再递归划分去排序这个小区间。可以考虑直接用其他排序对小区间的处理,比如使用插入排序,假设小区间小于10时,不在递归排序小区间。将会减少至少50%以上的递归次数

总体来说,小区间优化主要是来解决大量数据时递归的栈溢出问题,以及多次开辟栈帧的资源消耗问题。

4.稳定性

先说下结论:快排不稳定。

如果有一组数据中相同的元素在排序之后,更改他们的相对顺序,就认为这个排序不稳定。

在日常生活中,某些特定场合,使用的排序的稳定性及其重要。比如说两位同学分数相同,但是一个先交卷,一个后交卷,按照常理来说,先交卷的同学应该排在后交卷的同学之前,但如果经过排序后,先交卷的同学排到了后交卷同学之后,这就非常不合理了。

原因:

1.比如说有这样一组数据:4,5(1),5(2),5(3),8。

在三数取中时中间的5就会和4交换位置:5(2),5(1),4,5(3),8

2.或者这样一组数据:4,7(1),7(2),2,3,5

在第一次排序结束之后(左右交换法):2,3,4,7(2),7(1)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值