数据结构 — 快速排序

快速排序




基本思想

                                                                                                                               

快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法. 分治法的基本步骤为:

1.先从数据当中找出一个数据作为参照数

2.然后开始分区操作,大于参照数的就放到参照数右边,小于参照数的就放到参照数的左边.

3.然后对左右分区继续进行该操作,知道区间里面只有一个数据的是时候,快排完成.



1>>挖坑法快速排序


但是快排运用的可不仅仅只有分治法,说出来你可能不信快排还使用到了填坑法,其实也就是挖坑+填坑.


具体过程:
数据结构 — 快速排序_第1张图片


所以我总结一下上面整个挖坑填数操作的步骤:

1.i =L; j = R; 将参照数挖出形成第一个坑a[i]。

2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。

3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。

4.再重复执行2,3二步,直到i==j,将参照数填入a[i]中。


我们很容易写出这个时间复杂度为O(N)的代码:


int partSort1(int s[], int l, int r) //返回调整后参照数的位置  
{  
    int i = l, j = r;  
    int x = s[l]; //s[l]即s[i]就是第一个坑  
    while (i < j)  
    {  
        // 从右向左找小于x的数来填s[i]  
        while(i < j && s[j] >= x)   
            j--;    
        if(i < j)   
        {  
            s[i] = s[j]; //将s[j]填到s[i]中,s[j]就形成了一个新的坑  
            i++;  
        }  
  
        // 从左向右找大于或等于x的数来填s[j]  
        while(i < j && s[i] < x)  
            i++;    
        if(i < j)   
        {  
            s[j] = s[i]; //将s[i]填到s[j]中,s[i]就形成了一个新的坑  
            j--;  
        }  
    }  
    //退出时,i等于j。将x填到这个坑中。  
    s[i] = x;  
  
    return i;  
}  
void quick_sort(int s[], int l, int r)  
{  
    if (l < r)  
    {  
        Swap(s[l], s[(l + r) / 2]);   
        int i = l, j = r, x = s[l];  
        while (i < j)  
        {  
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数  
                j--;    
            if(i < j)   
                s[i++] = s[j];  
              
            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数  
                i++;    
            if(i < j)   
                s[j--] = s[i];  
        }  
        s[i] = x;  
        quick_sort(s, l, i - 1); // 递归调用   
        quick_sort(s, i + 1, r);  
    }  
}  



接下来我们再使用分治法的思想使用递归,让每个区间的元素只有一个那么我们就已经达到了目的.


2>>左右指针法快速排序



我们开始介绍第二种方法左右指针法,那么何为左右指针法?? 有一个数组a[N],做指针left指向待排序列的最左边. 指针right指向

向待排序列的最右边.然后选取我们的key这里假设选的是a[right]的Key,然后让left先走,遇到比key的数字停下来.然后让right往左

走,当遇到比key小的数的时候,停下来. 然后将a[left]和a[right]交换.当left >= right的时候说明单次快排结束了. 最后让left的

与key的值交换即可. 为什么选择right的时候需要让Left先走? 这里下面会有答案. 

那么我们先来了解一下整个左右指针法的排序思想吧:


数据结构 — 快速排序_第2张图片


相信这个图你只要仔仔细细的看完,你就可以着手写代码了. 当你明白了思想,大的框架你就已经知道怎么写了.但是在这个时候尤其的

注意我们要注重细节!! 什么begin越界啊 还有哪里没有交换上. 书写代码!  这里我就直接添加代码了.

//左右指针法.
int partSort(int* a, int begin, int end)
{
	int key = a[end];

	int left = begin;
	int right = end;

	//key选择的是最左边,那么最右边就先走.
	while (left < right)
	{
		while (left < right && a[left] <= key)
		{
			++left;
		}

		while (left < right && a[right] >= key)
		{
			--right;
		}

		if (left < right)
		{
			swap(a[right], a[left]);
		}

	}
	swap(a[end], a[left]);

	return left;
}

void quick_Sort(int *a, int begin,int end)
{

	if (begin >= end)
		return;

	int pos = partSort(a, begin, end);

	quick_Sort(a, begin, pos-1);

	quick_Sort(a, pos+1, end);
}


3>>前后指针法快速排序



惊不惊喜,意不意外. 快速排序还有别的方法. 他叫做前后指针法! 这个是我觉得快排里面最不容易写错的方法了. 因为没有多少的特

殊情况. 细节的东西很少. 也就是说这个算法逻辑很严密. 但是不太好理解! 好了我们来认识它吧~ 首先我们使用一个first来记录

begin.然后用second 记录begin前一个的位置.然后first来寻找小于key的值. 如果不小于key那么first一直++. 而second每次只走一步.

如果first和second相等则不 用交换,如果不同则需要交换. 是不是听起来很绕. 好吧 我的表达能力有问题! 但是我会画图啊!简而

之就是first寻找小于key的值,second 找大于key的值然后他们交换.但是过程和左右指针,挖坑有较大的区别.

数据结构 — 快速排序_第3张图片
认真看图,你一定会理解它的. 这种方法我表达起来有点困难. 而且方法的逻辑思维很紧密,基本也没有什么特殊情况和细节. 

所以我直接贴代码:

int partSort2(int* a, int begin, int end)
{
	int first = begin;
	int second = begin - 1;
	int key = a[end];

	while (first < end)
	{
		if (a[first] < key && ++second != first)
		{
			swap(a[second], a[first]);
		}

		++first;
	}

	swap(a[first], a[++second]);

	return second;
}


void quick_Sort(int *a, int begin,int end)
{

	if (begin >= end)
		return;

	int pos = partSort2(a, begin, end);

	quick_Sort(a, begin, pos-1);

	quick_Sort(a, pos+1, end);
}

好了,那么快速排序也就差不多了讲了一半了,是不是觉得有点慌哈哈. 其实快排基本没有其他方法可以实现了,但是但是快排还可以

优化啊.我们 思考一下,什么时候快速排序的时间复杂度为O(N^2). 那就是每次取key的时候取到了待排序列中最大的或者最小的. 也就

是序列有序的时候.所以 在选择key的时候是可以优化的.这里主要有两种方法,一种是随机数法.一种是三数区中法. 随机数法你听起来

就很随机很随意.我不就不用解释了. 大家都懂.


重点都放到这个三数取中法. 其实三数取中法及其高效和简单,也就是它每次取出序列中 第一个,最中间的那个,还有最后一个,三个

数当中找到 那个不大不小的数字.(中间的值),然后使用这个值作为key.有木有觉得很简单啊.这个就是一种优化方法.

int GetMid(int *a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[left]>a[right])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}


还有一种优化它叫做小区间优化法,提到这个优化方法我们首先要明白快速排序的时间复杂度怎么计算. 因为它是一个递归的算法,每

一次的 partSort是一个O(N)的算法.但是O(NlogN)是怎么来的呢?? 来,我们来看一下. 首先你要能够思考来快排整个栈帧样子。

数据结构 — 快速排序_第4张图片

这里我们有没有发现越到最后几层栈帧的个数越来越多??  举个例子如果栈帧的层数大于8的时候,栈帧个数就多起来了.这个时候我

们开辟出来这么 多栈帧显然不划算. 如果层数再大你的运行效率肯定大打折扣因为开辟的栈帧实在是太多了.这里我们就可以当快排的

栈帧次数大于8的时候,让这些 数据不要再递归了,直接使用插入排序. 这样岂不是美滋滋. 伪代码实现:

int GetMid(int *a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[left]>a[right])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

int partSort2(int* a, int begin, int end)
{
	int first = begin;
	int second = begin - 1;
	swap(a[end], a[GetMid(a, begin, end)]);

	int key = a[end];

	while (first < end)
	{
		if (a[first] < key && ++second != first)
		{
			swap(a[second], a[first]);
		}

		++first;
	}

	swap(a[first], a[++second]);

	return second;
}



void quick_Sort(int *a, int begin,int end)
{

	if (begin >= end)
		return;

	if (//在这里想办法判断层数)
	{
		Insert_sort();
	}

	int pos = partSort2(a, begin, end);

	quick_Sort(a, begin, pos-1);

	quick_Sort(a, pos+1, end);
}

非递归实现:



其实快排的栈帧很浪费空间但是呢,快速排序也是有非递归算法的也就是没有繁琐的栈帧,使用一个栈来存储每一个小待排数据的

最左下标和最右下标 ,然后在使用一套循环控制整个程序. 好了用左右指针举个例子吧:

//左右指针快速排序 非递归

void Quick_Sort(int* a,int size)
{
	int left = 0;
	int right = size - 1;
	stack q;

	q.push(right);
	q.push(left);

	while (!q.empty())
	{	
		left = q.top();
		q.pop();

		right = q.top();
		q.pop();

		int end = right;
		int begin = left;

		if (left < right)
		{
			swap(a[right], a[GetMid(a, left, right)]);
			int key = a[right];

			while (left < right)
			{
				while (left < right && a[left] <= key)
				{
					++left;
				}

				a[right] = a[left];


				while (left < right && a[right] >= key)
				{
					--right;
				}

				a[left] = a[right];
			}
			a[right] = key;

			q.push(end);
			q.push(right + 1);

			q.push(right - 1);
			q.push(begin);
		}
	}
}




总结

                                                                                                                                                           

快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。最坏情况是每次划分选取的基准都

是当前无序区中关字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非

空的子区间中记录数目,仅仅比划分无序区中记录个数减少一个。时间复杂度为O(NlgN)在最好情况下,每次划分所取的基准

是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:O(NlgN)尽管

快速排序的最坏时间为O(n2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。

它的平均时间复杂度为O(NlgN)。


算法名称  最差时间复杂度  平均时间复杂度  最优时间复杂度  空间复杂度  稳定性

快速排序    O(n2)          O(NlgN)                   O(NlgN)       O(n)        不稳定


快排和归并还是有各自的相似之处,总的来说出来小部分数据的话归并算法优于快排,但是归并排序需要耗费空间而快排不用,

数据的基数越来越多的时候,快排就会优 于归 并,这就是为什么快速排序使用的比较多的原因 .


你可能感兴趣的:(数据结构)