zoukankan      html  css  js  c++  java
  • 交换排序高速分拣

    高速排序(Quick Sort)也是一种交换排序,它在排序中採取了分治策略。

    高速排序的主要思想

    1. 从待排序列中选取一元素作为轴值(也叫主元)。
    2. 将序列中的剩余元素以该轴值为基准,分为左右两部分。左部分元素不大于轴值,右部分元素不小于轴值。轴值终于位于两部分的切割处。
    3. 对左右两部分反复进行这种切割,直至无可切割。
    从高速排序的算法思想能够看出,这是一递归的过程。

    两个问题:

    要想彻底弄懂高速排序,得解决两个问题:
    1. 怎样选择轴值?(轴值不同,对排序有影响吗?)
    2. 怎样切割?
    问题一:轴值的选取?
    轴值的重要性在于:经过切割应使序列尽量分为长度相等的两个部分,这样分治法才会起作用。若是轴值正好为序列的最值,切割后,元素统统跑到一边儿去了,分治法就无效了。算法效率无法提高。-看别人写快排的时候,注意他轴值的选取哦。

    问题二:怎样切割?
    这涉及到详细的技巧和策略。在稍后的代码中我们一一介绍。

    高速排序版本号一

    直接选取第一个元素或最后一个元素为轴值。这也是国内众多教材中的写法。
    举个样例:
    原序列   4   8   12   1   9   6
    下标     0   1   2    3   4   5   轴值 pivot=4
    初始化   i                    j
             i            j           i不动,移动j,while(i<j && a[j]>=pivot)j--; 
    移动元素 1   8   12   1   9   6
                 i        j           j不动,移动i,while(i<j && a[i]<=pivot)i++;
    移动元素 1   8   12   8   9   6
                i,j                   再次移动j,i和j相遇,结束
    最后一步 1   4   12   8   9   6   pivot归位
    轴值4的左右两部分接着切割……

    我想你一定看懂了,而且这轴值4,真的没选好,由于切割后左部分仅仅有一个元素。

    有人称上面的做法是:挖坑填数。这样的描写叙述真的非常形象。简单解释下:首先取首元素为轴值,用变量pivot存储轴值,这就是挖了一个坑。此时,a[0]就是一个坑。接着移动j,把合适位置的j填入a[0],于是a[j]成了新的坑。旧的坑被填上,新的坑就出现。直到i和j相遇,这最后一个坑,被pivot填上。至此完毕了第一趟切割……
    看懂了,就动手写程序吧!
    void QuickSort(int a[], int n)  //高速排序,版本号一
    {
    	if (a && n > 1)
    	{
    		int i, j, pivot;  //pivot轴值
    		i=0, j = n - 1;
    		pivot = a[0];   //第一个元素为轴值
    		while (i < j)
    		{
    			while (i < j && a[j] >= pivot)
    			j--;
    			if (i < j)
    			a[i++] = a[j];
    			while (i < j && a[i] <= pivot)
    			i++;
    			if (i < j)
    			a[j--] = a[i];
    		}
    		a[i] = pivot;   //把轴值放到切割处
    		QuickSort(a, i);
    		QuickSort(a + i + 1, n - i -1);
    	}
    } 

    如今想想以最后一个元素为轴值的代码了,先别急着看,先动动手哦!代码例如以下:
    void QuickSort(int a[], int n)
    {
    	if (a && n > 1)
    	{
    		int i, j, pivot;  //pivot轴值
    		i = 0, j = n - 1;
    		pivot = a[j];   //最后一个元素为轴值
    		while (i < j)
    		{
    			while (i < j && a[i] <= pivot)
    				i++;
    			if (i < j)
    				a[j--] = a[i];
    			while (i < j && a[j] >= pivot)
    				j--;
    			if (i < j)
    				a[i++] = a[j];
    		}
    		a[i] = pivot;   //把轴值放到切割处
    		QuickSort(a, i);
    		QuickSort(a + i + 1, n - i - 1);
    	}
    }

    轴值选取策略

    为了让轴值pivot不至于无效(不让pivot出现最值的情况)。我们能够使用一些策略来改进pivot的选取。

    策略一:

    随机选取序列中一元素为轴值。 

    int SelectPivot(int a[], int low, int high)
    {
    	int size = high - low + 1;
    	srand((unsigned)time(0));
    	return a[low + rand()%size];
    }
    选取首尾元素不就是该策略的一种特例!

    策略二:

    随机选取三数,取中位数。  
    int SelectPivot(int a[], int low, int high)
    {
    	int size = high - low + 1;
    	int p1, p2, p3;
    	srand((unsigned)time(0));
    	p1 = low + rand()%size;
    	p2 = low + rand()%size;
    	p3 = low + rand()%size;
    	/*
    	*  以下的交换不好理解:
    	*  经过前两次的交换,p1指向最小的,
    	*  所以最后两个最大的比較,把次最大的交换到 p2  
    	*/
    	if(a[p1] > a[p2])
    		swap(p1, p2);
    	if(a[p1] > a[p3])
    		swap(p1, p3);
    	if(a[p2] > a[p3])
    		swap(p2, p3);
    	return a[p2];
    }
    它的一种特例就是,选取原序列首、尾、中间三数,取它们的中位数。

    眼下看来基本经常使用的就这两种策略。只是我得吐槽一句:假设原序列中的元素本身就是随机存放的,也就是说,各个元素出如今各个位置的概率一样。那么特别地选取首尾元素和随机选取又有什么差别呢?不知大家怎么看?
    还得补充一句:随机选取轴值后,记得要把它和首或尾的元素交换哦。至于为什么?你懂的!

    高速排序版本号二

    这也是《算法导论》上的版本号。它的普遍做法是选取尾元素为pivot。重点是使用了一个切割函数:partition()。
    伪代码与例如以下:
    PARTITION(A, low, high)
    1. pivot <- A[high]    //选取尾元素为轴值
    2. i <- low-1          //把low-1赋值给i,下同
    3. for j <- low to high-1    //j的变化范围[low, high-1]
    4.      do if A[j] <= pivot
    5.            then i <- i+1
    6.            exchange A[i]<->A[j]
    7. exchange A[i+1} <-> A[high]
    8. return i+1;    //返回的是切割的位置
    然后,对整个数组进行递归排序:
    QUICKSORT(A, low, high)
    1  if low < high
    2  then q <- PARTITION(A, low, high)  //对元素进行切割就在这里
    3  QUICKSORT(A, low, q - 1)
    4  QUICKSORT(A, q + 1, high)

    假设你不习惯于看伪代码,我来举个样例:(还是上面的序列)
    原序列   4   8   12   1   9   6
    下标  -1 0   1   2    3   4   5   轴值pivot是6
    初始化 i j                        a[j]=a[0]=4<6,下一步先 i++;再swap(a[i],a[j]);随后j++;
    交换     4   8   12   1   9   6
             i   j                    接着移动j
             i            j           a[j]=a[3]=1<6,下一步…
    交换     4   1   12   8   9   6
                 i            j       
                 i                j   
    交换     4   1   6    8   9   12  最后一步 swap(a[i+1], a[high]);或者是 swap(a[i+1], a[j]);
    所以最后返回的是 i+1
    用大白话讲讲上面的排序过程:用两个指针i,j,它们初始化为i=-1;j=0,接着让j不断地自增,遇到a[j]>pivot就与i交换,直到j指向末尾。
    更直白的话:从头開始遍历原序列,遇到小于轴值的就交换到序列前面。

    看懂了,就写代码了…
    int partition(int a[], int low, int high)
    {
    	int i, j;
    	i = low - 1;
    	j = low;
    	while (j < high)
    	{
    		if (a[j] < a[high])
    		swap(a[++i], a[j]);
    		j++;
    	}
    	swap(a[++i], a[high]);    //主元归位 
    	return i;  //上面一步已经 ++i,所以这里不用 i+1 
    }
    void quicksort(int a[], int low, int high)
    {
    	if (low < high)  //至少两个元素,才进行排序 
    	{
    		int i = partition(a, low, high);
    		quicksort(a, low, i - 1);
    		quicksort(a, i + 1, high);
    	}
    }
    void QuickSort(int a[], int n)
    {
    	if (a && n>1)
    		quicksort(a, 0, n - 1);	
    }

    题外话:看到有的Api设计是这种:QuickSort(int a[], int low, int high)。竟然让用户多写一个0!如此不为用户考虑。应越简洁越好。排序仅仅给数组名和数组大小,就可以。
    对上面的流程再思考:看到初始化i=-1;你不认为奇怪吗?为什么i一定要从-1開始,细致了解了i的作用,你会发现i本能够从0開始。这种做法的partition()方法是这种:
    int partition(int a[], int low, int high)
    {
    	int i, j;
    	i = low;  //这里与上一种的做法不同哦!
    	j = low;
    	while(j < high)
    	{
    		if (a[j] < a[high])
    		swap(a[i++], a[j]);
    		j++;
    	}
    	swap(a[i], a[high]);    //主元归位 
    	return i;  
    }

    再思考:为什么j不能指向high?若是更改if(a[j]<a[high])为if(a[j]<=a[high),最后直接把a[high]交换到前面了,也就是说在while循环里面就完毕了最后“主元归位”这一步。大家想想是不是?
    此时的partition()是这种:
    int partition(int a[], int low, int high)
    {
    	int i, j;
    	i = low;
    	j = low;
    	while (j <= high)
    	{
    		if (a[j] <= a[high])
    		swap(a[i++], a[j]);
    		j++;
    	}
    	return i-1;   //这里为什么是i-1,得想明确?
    }

    至于有时候把quicksort()和partition()写成一个函数,那是再简单只是的事情,你肯定会的。

    高速排序版本号三:

    上面用的都是递归的方法,把递归转化非递归总是不简单的,也总让人兴奋。这个版本号就是高速排序的非递归写法;
    void QuickSort(int a[], int low, int high)
    {
    	if (low < high)
    	{
    		stack<int> s;   //使用STL中的栈 
    		int l,mid,h;
    		mid = partition(a, low, high);
    		/*
    		首先存储第一次切割后的 [low, mid-1]和 [mid+1, high] 
    		注意:这是成对存储的,取的时候注意顺序 
    		*/ 
    		if (low < mid-1)
    		{
    			s.push(low);
    			s.push(mid - 1);
    		}
    		if (mid + 1 < high)
    		{
    			s.push(mid + 1);
    			s.push(high);
    		}
    		//仅仅要栈不为空,说明仍有可切割的部分 
    		while(!s.empty())
    		{
    			h=s.top();
    			s.pop();
    			l=s.top();
    			s.pop();
    			mid = partition(a, l, h);
    			if (l < mid - 1)
    			{
    				s.push(l);
    				s.push(mid - 1);
    			}
    			if (mid + 1 < h)
    			{
    				s.push(mid + 1);
    				s.push(h);
    			}	
    		}
    	}
    }

    这个非递归的写法是非常有意思的,非常须要技巧。细致想想,你能明确的。
    提示:用栈保存每个待排序子序列的首尾元素下标,下一次while循环时取出这个范围,对这段子序列进行partition操作。

    小结:

    高速排序号称高速搞定,时间复杂度是O(nlogn)。基本上是最优的排序方法。它的写法不外乎以上三种,大同小异。看到这里。你一定彻底了解了它。以上写法,都经过了本人測试,不知道你的測试是否和我一样?



    若是有所帮助,顶一个哦!


    本专栏的文件夹
    全部内容的文件夹


  • 相关阅读:
    < java.util >-- Set接口
    Codeforces 627 A. XOR Equation (数学)
    Codeforces 161 B. Discounts (贪心)
    Codeforces 161 D. Distance in Tree (树dp)
    HDU 5534 Partial Tree (完全背包变形)
    HDU 5927 Auxiliary Set (dfs)
    Codeforces 27E. Number With The Given Amount Of Divisors (暴力)
    lght oj 1257
    Codeforces 219D. Choosing Capital for Treeland (树dp)
    Codeforces 479E. Riding in a Lift (dp + 前缀和优化)
  • 原文地址:https://www.cnblogs.com/zfyouxi/p/4556432.html
Copyright © 2011-2022 走看看