浅谈快速排序,以及快速排序的实现

快速排序      是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值(key),按照该基准值将待排序集合分割成两个子序列,左子序列中所有元素小于基准值,右子序列中所有元素据大于基准值,然后最左右子序列反复重复该过程,直到所有元素都排列在相应位置为止。

首先,我们需要明白的一点是,先写一个单趟排序,再把单趟排序重复多次,这样来写排序会更加方便快捷不易出错

所以,从快速排序的单趟实现,开始讲起

现有一数组       6        1        2        7        9        3        ​​​​​​​4        ​​​​​​​5        ​​​​​​​10        8

                        begin                                                                                end

                        key

                       pivot

我们一般选择,数组第一个元素作为我们的基准值 key,在此为6

我们单趟操作要实现的目标是,将6送到中间,同时6的左边都是比6小的值,6的右边都是比6大的值,假设我们完成了目标

​​​​​​​           得到了一个这样的数组     5        1        ​​​​​​​2        ​​​​​​​4        ​​​​​​​3        ​​​​​​​6        ​​​​​​​9        ​​​​​​​7        ​​​​​​​10        8

                                                我们会发现6是排名第6小的,那么也就是说6已经处于它在升序排序中的位置了,这里可能会有点绕,我们可以拿有序数组来对比,

1        2        ​​​​​​​3        ​​​​​​​4        ​​​​​​​5        ​​​​​​​6        ​​​​​​​7        ​​​​​​​8        ​​​​​​​9        10,有序数组中6也排在第六位

那,如何让6的左边都小于它本身,右边都大于它本身

就要用到以下的挖坑法

                        6        1        ​​​​​​​2        ​​​​​​​7        ​​​​​​​9        ​​​​​​​3        ​​​​​​​4        ​​​​​​​5        ​​​​​​​10        8

                        begin                                                                                end

                        key

                       pivot

begin前端,end尾端,pivot坑,key基准值,只有key存的是数值,其余存的都是下标

首先把 key赋上6

然后将pivot的值赋为0

也就是6的位置是第一个坑,这个坑可以开始接受小于key的数据了

end先开始工作,开始从尾部找小于key的数据,找到后将该数据抛到坑里

然后end的位置就变成了新的坑

begin再开始工作,开始从头部找大于key的值,然后丢到坑里

以begin为下标的地方又变成新的坑,依次操作

当begin=end时交换停止,说白了就是左右两边数据按照key值左右交换

写完单趟就要用分治思想,来实现整体排序

把大问题划分成一个个不可分割的子问题

例如,举一个最小的情况  两个数中一个数为key,让其右边比key大,或左边比key小,这两个数就有序了

以下是代码实现

void QuickSort(int* a, int left,int right)
{   
	if (left >= right)//left>right表示没有左区间存在,left=right表示只有一个值
	{
		return;
	}                 一轮递归的结束条件
	int begin = left, end = right;
	int pivot = begin;
	int key = a[begin];
	while (begin < end)
	{
		//选择从左端开始做坑
		//右边找小放到左边
		while (begin < end && a[end] >= key)
		{
			end--;//end持续--可能会导致end到达到begin前面发生交错,把已经处于正确位置的数,又扔到后面的坑中所以while条件里要加一条begin<end
		}
		//选出小的放到左边的坑中,自己就变成了新的坑位
		a[pivot] = a[end];
		pivot = end;
		//key的左边目标是全是小于key,所以从左边找大,放到右边
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[pivot] = a[begin];
		pivot = begin;//我成为新的坑
	}
	pivot = begin;
	a[pivot] = key;
	//left区间           坑  pivot         right区间
	//[left,pivot-1]       pivot           [pivot+1,right]
	//左子区间和右子区间有序,我们就有序了,如何让他们有序呢?  分治递归  二叉树前序遍历,根,左子树,右子树
	QuickSort(a, left, pivot - 1);
	QuickSort(a, pivot+1, right);
}
void TestQuickSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	QuickSort(a, 0,sizeof(a) / sizeof(int)-1);
	PrintfArry(a, sizeof(a) / sizeof(int));

}

int main()
{
	TestQuickSort();
}

关于快速插入排序的时间复杂度

首先看单趟,我们从左边不断找大的向右边抛,从右边不断找小的向左边抛,最差的情况是,来回总共抛了N次

总体看,我们拿一个具体的理想情况来估算,假如我们每次选的key,它的顺序大小刚好在正中间则可以将它视为一个二叉树,N个数的话,总共有log2N层,但是,第一层往下,每一层都会分成左右两部分,则进行操作的次数为 2*n/2+2*n/2=n............依次类推,每一层进行的操作次数还是n次

然后总有,N层,所以时间复杂度为 N*logN

快排什么情况下最坏?时间复杂度又是多少?

有序的情况下最坏

假如是顺序,我们的算法并不能检测已经是顺序,所以,每次只会排最左边的值,把它排有序,然后,右边的值从最后遍历到最开头,第一次遍历N,第二次遍历N-1,.....................直到1

 

 快速排序,有着致命的缺陷,假如是有序,则它的时间复杂度为O(N^2),会变得非常非常慢

官方用三数取中法来选取key,不再单纯的选最左边,或最右边来作为key,而是选用三个数

中间那个数来做为key,这样就避免了选用最大或最小值来做key

以下是添加了,三数取中函数GetMidIndex(),后的代码

//三数取中,三个数中取中间那个
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) >> 1;//下标取平均,这样写效率更高一点,循环右移说白了,就是二进制中每一位的权重除以2,加和成十进制数就还是整体除以2
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return  mid;
		}
		else if (a[right] > a[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else//a[left]>a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;

		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
void QuickSort(int* a, int left,int right)
{   
	if (left >= right)//left>right表示没有左区间存在,left=right表示只有一个值
	{
		return;
	}
	int index= GetMidIndex(a, left, right);//找出中间值的下标
	//为了不打乱下面的代码结构,我们选择将,中间值下标找出来,并将该处存放的数值与开头存放的数值互换
	Swap(&a[left], &a[index]);
	int begin = left, end = right;
	int pivot = begin;
	int key = a[begin];
	while (begin < end)
	{
		//选择从左端开始做坑
		//右边找小放到左边
		while (begin < end && a[end] >= key)
		{
			end--;//end持续--可能会导致end到达到begin前面发生交错,把已经处于正确位置的数,又扔到后面的坑中所以while条件里要加一条begin<end
		}
		//选出小的放到左边的坑中,自己就变成了新的坑位
		a[pivot] = a[end];
		pivot = end;
		//key的左边目标是全是小于key,所以从左边找大,放到右边
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}		

		a[pivot] = a[begin];
		pivot = begin;//我成为新的坑
	}
	pivot = begin;
	a[pivot] = key;
	//left区间           坑  pivot         right区间
	//[left,pivot-1]       pivot           [pivot+1,right]
	//左子区间和右子区间有序,我们就有序了,如何让他们有序呢?  分治递归  二叉树前序遍历,根,左子树,右子树
	QuickSort(a, left, pivot - 1);
	QuickSort(a, pivot+1, right);
}
void TestQuickSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	QuickSort(a, 0,sizeof(a) / sizeof(int)-1);
	PrintfArry(a, sizeof(a) / sizeof(int));

}

int main()
{
	TestQuickSort();
}








实测的话,快速插入排序和堆排序速度差不多

快速插入排序的特性总结

1.快速插入排序综合性能强,使用场景多

2.时间复杂度只有O(N*logN)

官方c语言库中,对数组的排序选择的是快排

如果,我们把最后的几层递归调用消除掉,即进行小区间优化,则效率会更加高

假如,我们有一百万个数,最后两层,就会调用80万次左右的递归,那不如在划分成很小的区间后,直接用其他排序来完成小区间的排序,我选择用插入排序,下面是部分需要修改的地方,

​​​​​​​至于插入排序的实现,我之前的博客有写过可以直接copy过来用。


void QuickSort(int* a, int left,int right)
{   
	if (left >= right)//left>right表示没有左区间存在,left=right表示只有一个值
	{
		return;
	}
	int index= GetMidIndex(a, left, right);//找出中间值的下标
	//为了不打乱下面的代码结构,我们选择将,中间值下标找出来,并将该处存放的数值与开头存放的数值互换
	Swap(&a[left], &a[index]);
	int begin = left, end = right;
	int pivot = begin;
	int key = a[begin];
	while (begin < end)
	{
		//选择从左端开始做坑
		//右边找小放到左边
		while (begin < end && a[end] >= key)
		{
			end--;//end持续--可能会导致end到达到begin前面发生交错,把已经处于正确位置的数,又扔到后面的坑中所以while条件里要加一条begin<end
		}
		//选出小的放到左边的坑中,自己就变成了新的坑位
		a[pivot] = a[end];
		pivot = end;
		//key的左边目标是全是小于key,所以从左边找大,放到右边
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}		

		a[pivot] = a[begin];
		pivot = begin;//我成为新的坑
	}
	pivot = begin;
	a[pivot] = key;
	//left区间           坑  pivot         right区间
	//[left,pivot-1]       pivot           [pivot+1,right]
	//左子区间和右子区间有序,我们就有序了,如何让他们有序呢?  分治递归  二叉树前序遍历,根,左子树,右子树
	if (pivot - 1 - left > 10)
	{
		QuickSort(a, left, pivot - 1);
	}
	else
	{
		InsertSort(a + left, pivot - 1 - left + 1);//InsertSort函数要求传入,数组的地址和元素个数
	}
	if (right - (pivot + 1) > 10)
	{
		QuickSort(a, pivot+1,right);
	}
	else
	{
		InsertSort(a + pivot + 1, right - (pivot + 1) + 1);
	}
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值