快速排序的基本思路及实现

基本了解

        首先,快排是一个基于二叉树结构的排序,理想情况下时间复杂度为N*logN,极端情况下为N^2,但实际使用中,通过一些优化(后文会说),基本不会出现时间复杂度为N^2的情况,所以可以将快排简单地看成是一个时间复杂度为N*logN的排序算法。快排是如今主流的排序算法。后文快排以升序为例。

排序思路

           假设有一串数组,     

        我们再取一个中间值5作为key,

        比key值小的放key左边,比key值大的放key右边,得到

        再基于key的位置,将数组分为两部分,key的左边,key的右边,然后重复以上的操作,直到分不了,即分出的部分没有值或只有一个值为止,如图,

        以上便是快排的基本思路。

        在理想情况下,key值为数组的中位数,数组的长度为N,递归深度为logN,因此时间复杂度为N*logN,而在极端情况下,比如数组是个有序数组,且我们一直取数组的第一个数作为key,这样一来递归的深度为N-1,时间复杂度为N^2。

        了解了基本思路后还剩下两个问题,1. 如何取key?2. 如何在原有数组中交换数据,使数组达到我们想要的效果?

如何取key

        从上文中我们可以看出key值的好坏决定了快排的效率,一旦key取得不好,调用堆栈过深,还会有栈溢出的风险。但给你一串数组,我们不知道一串数组中的所有值,我们无法像上文那样精确地找到中位数作为key值,因此我们只能尽量在数组的所有值中尽量找出接近中位数的key。

        常用的方法为:三数取中法。即取数组的头、中间、尾的数据进行比对,取出三数中中间的那个数,再与头一个数作交换。  

int GetMidIndex(int* a, int left, int right)
{
    //left和right为数组下标,返回中间值的数组下标,交给上层去做交换
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else // a[left] >= a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

   交换数据

        由于交换数据需要在数组内进行,无法另开空间,所以交换数据的方法反而是快排中最难的点。这里提供三种方法。

hoare法

        代码如下,

// 快速排序hoare版本
int PartSort1(int* a, int left, int right) {
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int keyi = left;
	while (left < right) {
		while (left < right && a[right] >= a[keyi]) {
			right--;
		}
		while (left < right && a[left] <= a[keyi]) {
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	int meeti = left;
	Swap(&a[meeti], &a[keyi]);
	return meeti;
}

        需要注意的是,这里一定要右边先走,因为我们需要保证当left=right的节点是小于key的。

挖坑法

        

        代码如下,

// 快速排序挖坑法
int PartSort2(int* a, int left, int right) {
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int key = a[left];
	int hole = left;
	while (left < right) {
		while (left < right && a[right] >= key) {
			right--;
		}
		a[hole] = a[right];
		hole = right;
		while (left < right && a[left] <= key) {
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}

         挖坑法也需要从右边找起,因为需要先找到比key小的数据与头位置的坑做交换。

前后指针法

        代码如下,

// 快速排序前后指针法
int PartSort3(int* a, int left, int right) {
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int keyi = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right) {
		if (a[cur] < a[keyi] && ++prev != cur) {
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

        以上三种方法没有明显差距,用哪个都可以。

 

优化策略 

         当快排递归到快结束时,比如当此递归只有8个数,8个数却至少需要三次递归,过于浪费栈空间,这时我们就可以使用其他排序,如插入排序来对这8个数进行排序。

        优化后的快排代码如下,

void QuickSort(int* a, int left, int right) {
	if (left >= right) {
		return;
	}
	if (right - left <= 8) {
		InsertSort(a + left, right - left + 1);
	}
	else {
		int keyi = PartSort3(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

快速排序的缺陷

      快速排序目前最大的缺点就是它的不稳定。通俗点来说,比如一串数组有两个1,一个在前一个在后,快速排序算法不能保证原本在前的那个1在排序结束后还是在前。ps: 大多数情况下我们可以忽略这个缺陷,但仍有不少场景需要算法稳定,这种时候就不会选择快排。  

拓展:快排的非递归写法 

         非递归写法主要应用在栈空间不够的情况下,基本思路是通过栈,储存数组下标,来模拟递归。

        代码如下,

void QuickSortNonR(int* a, int left, int right) {
	Stack st;
	StackInit(&st);
	StackPush(&st, left);
	StackPush(&st, right);
	while (!StackEmpty(&st)) {
		int right1 = StackTop(&st);
		StackPop(&st);
		int left1 = StackTop(&st);
		StackPop(&st);
		if (left1 >= right1) {
			continue;
		}
		int keyi = PartSort1(a, left1, right1);
		StackPush(&st, keyi + 1);
		StackPush(&st, right1);
		StackPush(&st, left1);
		StackPush(&st, keyi - 1);
	}
	StackDestroy(&st);
}

        栈是我用c手搓的,嫌麻烦的可以替换成c++STL库里的栈。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值