划分是快速排序的一个根本机制,在介绍快速排序之前,先了解一下划分。
划分数据就是把数据分为两组,使所有关键字大于特定值的数据项在一组,使所有关键字小于特定值的数据项在另一组。
很容易想象划分数据的情况。比如可以将支援记录分为两组:家住办公地点15公里以内的雇员和住在15公里以外的雇员。或者学校管理者想要把学生分成年级平均成绩高于60分和低于60分的两组,一次来判定哪些学生应该在系主任掌握的名单里,等等。
在完成了划分之后,数据还不能称为有序:这只是把数据简单的分成了两组。但是,数据还是比没有划分之前更接近有序了。注意划分是不稳定的。这也就是说,每一组中的数据项 并不是按照它原来的顺序排列的。事实上,划分往往会颠倒组中一些数据的顺序。
划分的Java程序:
public int pattitionIt(int left, int right, long pivot)
{
int leftPtr=left -1; //right of first elem
int rightPtr=right+1; //left of pivot
while(true)
{
while(leftPtr<right && //find bigger item
theArray[++leftPtr]<pivot) // theArray是需要进行划分的数组
; //(nop)
while(rightPtr>left && //find smaller item
theArray[--rightPtr]>pivot)
; //(nop)
if(leftPtr>=rightPtr) //if pointers cross,
break; //partition done
else
swap(leftPtr,rightPtr); //swap elements
} //end while(true)
return ledtPtr;
} // end partitionIt()
public void swap(int dex1, int dex2) //swap two elements
{
long temp;
temp=theArray[dex1];
theArray[dex1]=theArray[dex2];
theArray[dex1]=temp;
}
划分的过程不一定要把数组分成大小小相同的两半;这取决于枢纽以及数据的关键字的值。有可能一组中的数据项个数多于另一组中的数据项个数。
划分算法:
划分算法由两个指针开始工作,两个指针分别指向数组的两头(此指针非彼指针)。在昨天的指针,leftPtr,向右推移,而在右边的指针,rightPtr,向左推移。
实际上,leftPtr初始化时是在第一个数据项的左一位,rightPtr是在最后一个数据项的右边一位,这是因为在它们工作之前,它们都要分别的加一和减一。
停止和交换
当leftPtr遇到比枢纽小的数据项时,它继续右移,因为这个数据项的位置已经处在数组的正确一边了。但是,当遇到比枢纽大的数据项时,它就停下来。类似的,当rightPtr遇到大于枢纽的数据项时,它继续左移,但是当发现比枢纽小的数据项时,它也挺下来,两个内层的while循环,第一个应用与leftPtr,第二个应用于rightPtr,控制这个扫描过程。因为指针退出了while循环,所以它停止移动。下面是一段扫描不在适当位置上的数据项的简化代码:
while(theArray[++leftPtr]<pivot) //find bigger item
;
while(theArray[--right>piovt]) //find smaller item
;
swap(leftPtr,rightPtr);
第一个while 循环在发现比枢纽大的数据项时退出;第二个循环在发现比枢纽小的数据项时退出,当这两个循环都退出之后,leftPtr和rightPtr都指着在数组错误一方位置上的数据项,所以交换这两个数据项。
交换之后,继续移动两个指针,当指向的数据项在数组的错误一方时,再次停止,然后交换数据项。所有的这些操作都包含在一个外部循环中。当两个指针最终相遇的时候,划分过程结束,并且退出这个外层while循环。
处理异常数据
如果肯定数组最右端的数据项小于枢纽,并且数组最左端的数据项大于枢纽,那么前面看到的简化的while循环会很好的执行。遗憾的是,可能用算法来划分的那些数据并没有排列得这么好。
例如,如果所有的的数据项都小于枢纽,leftPtr变量将会遍历整个数组,徒劳地寻找大于枢纽的数据项,然后跑出数组的最右端,产生数组越界的异常。当所有的数据都大于枢纽的时候,类似的糟糕结果也会发生在rightPtr上。
为了避免这些问题,必须在while循环中增加数组边界的检测:在第一个循环中增加leftPtr<right,第二个循环中增加rightPtr>left。
精巧的代码
这个while循环中的代码相当精巧。举例来说,想要从内部循环条件中出去加1操作符,并且用这个加1操作符代替空操作指令语句。例如,可以把如下的代码:
while(leftPtr<right && //find bigger item
theArray[++leftPtr]<pivot) // theArray是需要进行划分的数组
;
改为
while(leftPtr<right && //find bigger item
theArray[leftPtr]<pivot) // theArray是需要进行划分的数组
++ leftPtr ;
对于另一个内部while循环的改变是相似的。这些改变使指针的初始值分别设为left和right成为可能,这比设为left-1和right+1要更为清晰。
但是,这些改变导致只有在满足条件的情况下指针才会加1。而算法要求指针在任何情况下都必须移动,所以需要在外层的while循环中增加两个附加的语句强迫指针变换。空操作指令是最有效的解决办法。
相等的关键字
下面是要在partitionIt()方法中做的另一个细微的改变。如果想要对所有与枢纽相等的数据项运行partitionIt()方法,将发现每一次比较都会产生一次交换。交换关键字相等的数据项看起来是浪费时间的。while循环中对枢纽和数组数据项进行比较的<和>操作符产生了这种额外的交换。然而,假设使用<=和>=操作符,这确实防止了相等数据项的交换,但是这也使得在算法结束时leftPtr和rightPtr停在了数组的两端。
划分算法的效率:
划分算法的执行时间为O(N)。两个指针开始时分别在数组的两端,然后以或大或小的恒定速度相向移动,停止移动并且在移动的过程中交换。当两个指针相遇时,划分完成。如果要划分两倍树目的数据项,指针以同样的速率移动,但是需要比较和交换两倍数目的数据项,因此这个过程耗时也是两倍。从而,运行时间和N成正比。
更为特别的,每一次划分都有N+1或者N+2次的比较。每个数据项都由这个或那个指针参与比较,这产生了N次比较,但是在指针发现它们已经彼此“越过”之前,他们已经移动过头了,所以在划分完成之前多了一次或者两次额外的比较,比较的次数不取决于数据是如何排列的(除了在扫描结束时的一次或者两次额外比较的不确定性)。
但是,交换的次数确实是取决于数据是如何排列的。如果数据是逆序排列的,并且取得枢纽把数据项分为两半,那么每一对值都需要交换,也就是N/2次交换。
对于任意的数据,在一次划分中交换的次数将小于N/2,即使一半的数据项小于枢纽,一半的数据项大于枢纽。这是因为总会有一些数据项在正确的位置上。如果枢纽比大多数的竖条都大(或者小),会有更少的交换次数,这是因为只有很少的大于(或者小于)枢纽的数据项需要交换。对于任意的数据,在平均情况下,大约执行数据项最大数目的一般的交换操作。
尽管交换的次数少于比较的次数,但它们都是和N成正比的。
划分数据就是把数据分为两组,使所有关键字大于特定值的数据项在一组,使所有关键字小于特定值的数据项在另一组。
很容易想象划分数据的情况。比如可以将支援记录分为两组:家住办公地点15公里以内的雇员和住在15公里以外的雇员。或者学校管理者想要把学生分成年级平均成绩高于60分和低于60分的两组,一次来判定哪些学生应该在系主任掌握的名单里,等等。
在完成了划分之后,数据还不能称为有序:这只是把数据简单的分成了两组。但是,数据还是比没有划分之前更接近有序了。注意划分是不稳定的。这也就是说,每一组中的数据项 并不是按照它原来的顺序排列的。事实上,划分往往会颠倒组中一些数据的顺序。
划分的Java程序:
public int pattitionIt(int left, int right, long pivot)
{
int leftPtr=left -1; //right of first elem
int rightPtr=right+1; //left of pivot
while(true)
{
while(leftPtr<right && //find bigger item
theArray[++leftPtr]<pivot) // theArray是需要进行划分的数组
; //(nop)
while(rightPtr>left && //find smaller item
theArray[--rightPtr]>pivot)
; //(nop)
if(leftPtr>=rightPtr) //if pointers cross,
break; //partition done
else
swap(leftPtr,rightPtr); //swap elements
} //end while(true)
return ledtPtr;
} // end partitionIt()
public void swap(int dex1, int dex2) //swap two elements
{
long temp;
temp=theArray[dex1];
theArray[dex1]=theArray[dex2];
theArray[dex1]=temp;
}
划分的过程不一定要把数组分成大小小相同的两半;这取决于枢纽以及数据的关键字的值。有可能一组中的数据项个数多于另一组中的数据项个数。
划分算法:
划分算法由两个指针开始工作,两个指针分别指向数组的两头(此指针非彼指针)。在昨天的指针,leftPtr,向右推移,而在右边的指针,rightPtr,向左推移。
实际上,leftPtr初始化时是在第一个数据项的左一位,rightPtr是在最后一个数据项的右边一位,这是因为在它们工作之前,它们都要分别的加一和减一。
停止和交换
当leftPtr遇到比枢纽小的数据项时,它继续右移,因为这个数据项的位置已经处在数组的正确一边了。但是,当遇到比枢纽大的数据项时,它就停下来。类似的,当rightPtr遇到大于枢纽的数据项时,它继续左移,但是当发现比枢纽小的数据项时,它也挺下来,两个内层的while循环,第一个应用与leftPtr,第二个应用于rightPtr,控制这个扫描过程。因为指针退出了while循环,所以它停止移动。下面是一段扫描不在适当位置上的数据项的简化代码:
while(theArray[++leftPtr]<pivot) //find bigger item
;
while(theArray[--right>piovt]) //find smaller item
;
swap(leftPtr,rightPtr);
第一个while 循环在发现比枢纽大的数据项时退出;第二个循环在发现比枢纽小的数据项时退出,当这两个循环都退出之后,leftPtr和rightPtr都指着在数组错误一方位置上的数据项,所以交换这两个数据项。
交换之后,继续移动两个指针,当指向的数据项在数组的错误一方时,再次停止,然后交换数据项。所有的这些操作都包含在一个外部循环中。当两个指针最终相遇的时候,划分过程结束,并且退出这个外层while循环。
处理异常数据
如果肯定数组最右端的数据项小于枢纽,并且数组最左端的数据项大于枢纽,那么前面看到的简化的while循环会很好的执行。遗憾的是,可能用算法来划分的那些数据并没有排列得这么好。
例如,如果所有的的数据项都小于枢纽,leftPtr变量将会遍历整个数组,徒劳地寻找大于枢纽的数据项,然后跑出数组的最右端,产生数组越界的异常。当所有的数据都大于枢纽的时候,类似的糟糕结果也会发生在rightPtr上。
为了避免这些问题,必须在while循环中增加数组边界的检测:在第一个循环中增加leftPtr<right,第二个循环中增加rightPtr>left。
精巧的代码
这个while循环中的代码相当精巧。举例来说,想要从内部循环条件中出去加1操作符,并且用这个加1操作符代替空操作指令语句。例如,可以把如下的代码:
while(leftPtr<right && //find bigger item
theArray[++leftPtr]<pivot) // theArray是需要进行划分的数组
;
改为
while(leftPtr<right && //find bigger item
theArray[leftPtr]<pivot) // theArray是需要进行划分的数组
++ leftPtr ;
对于另一个内部while循环的改变是相似的。这些改变使指针的初始值分别设为left和right成为可能,这比设为left-1和right+1要更为清晰。
但是,这些改变导致只有在满足条件的情况下指针才会加1。而算法要求指针在任何情况下都必须移动,所以需要在外层的while循环中增加两个附加的语句强迫指针变换。空操作指令是最有效的解决办法。
相等的关键字
下面是要在partitionIt()方法中做的另一个细微的改变。如果想要对所有与枢纽相等的数据项运行partitionIt()方法,将发现每一次比较都会产生一次交换。交换关键字相等的数据项看起来是浪费时间的。while循环中对枢纽和数组数据项进行比较的<和>操作符产生了这种额外的交换。然而,假设使用<=和>=操作符,这确实防止了相等数据项的交换,但是这也使得在算法结束时leftPtr和rightPtr停在了数组的两端。
划分算法的效率:
划分算法的执行时间为O(N)。两个指针开始时分别在数组的两端,然后以或大或小的恒定速度相向移动,停止移动并且在移动的过程中交换。当两个指针相遇时,划分完成。如果要划分两倍树目的数据项,指针以同样的速率移动,但是需要比较和交换两倍数目的数据项,因此这个过程耗时也是两倍。从而,运行时间和N成正比。
更为特别的,每一次划分都有N+1或者N+2次的比较。每个数据项都由这个或那个指针参与比较,这产生了N次比较,但是在指针发现它们已经彼此“越过”之前,他们已经移动过头了,所以在划分完成之前多了一次或者两次额外的比较,比较的次数不取决于数据是如何排列的(除了在扫描结束时的一次或者两次额外比较的不确定性)。
但是,交换的次数确实是取决于数据是如何排列的。如果数据是逆序排列的,并且取得枢纽把数据项分为两半,那么每一对值都需要交换,也就是N/2次交换。
对于任意的数据,在一次划分中交换的次数将小于N/2,即使一半的数据项小于枢纽,一半的数据项大于枢纽。这是因为总会有一些数据项在正确的位置上。如果枢纽比大多数的竖条都大(或者小),会有更少的交换次数,这是因为只有很少的大于(或者小于)枢纽的数据项需要交换。对于任意的数据,在平均情况下,大约执行数据项最大数目的一般的交换操作。
尽管交换的次数少于比较的次数,但它们都是和N成正比的。