快速排序
就像归并之于归并排序,划分是快速排序的核心
划分数据即将数据项分为两组,使全部值小于特定值的数据项在一组,使全部值大于特定值的数据项在另一组
划分算法由两个指针开始,两个指针分别指向数组2端,相向移动,当左侧指针遇到大于特定值的数据项时,停止移动,当右侧指针遇到小于特定值的数据项时,停止移动,此时交换两个指针所指数据项的值,交换完成后,两个指针再次相向移动,直到两个指针之间没有其它数据项或两个指针之间只有一个数据项
public class Partition {
private int[] data;
public Partition(int[] data){
this.data = data;
}
//返回右侧数组中第一个数据项在整个数组中的位置
public int part(int left, int right, int keyNum){
while(true){
while(left < right &&
data[left] < keyNum)
left++;
while(right > left &&
data[right] >= keyNum)
right--;
if(right <= left)
break;
swap(left, right);
}
return right;
}
private void swap(int left, int right){
int temp = data[right];
data[right] = data[left];
data[left] = temp;
}
public void display(){
for(int i = 0 ; i < data.length ; i++){
System.out.print(data[i] + " ");
}
System.out.print("\n");
}
public static void main(String[] args){
int[] ints = {17, 53, 44, 9, 12, 86, 144, 72, 22, 30};
//int[] ints = {1,2,3};
int keyNum = ints[ints.length-1];
Partition p = new Partition(ints);
System.out.println(p.part(0, ints.length-1, keyNum));
p.display();
}
}
4 17 22 12 9 44 86 144 72 53 30
划分算法在数组长度很短(比如数组长度为1、2或3)的时候也可以正常工作
划分算法的效率:
数据比较次数:左右指针相遇时比较结束,故需比较N次
数据交换次数:交换次数小于比较次数,但确切的交换次数取决于数组的排列及keyNum的大小
故总的时间复杂度为O(N)
快速排序:把一个数组划分为两个子数组,然后递归地调用自身为每一个子数组进行快速排序
取每个待划分数组右端数据项的值作为本次划分的keyNum
public class QuickSort {
private int[] data;
public QuickSort(int[] data){
this.data = data;
}
public void sort(){
this.reSort(0, data.length - 1);
}
private void reSort(int left, int right){
if(right <= left)
return;
int n = this.part(left, right);
//显示局部排序结果
this.display();
//递归调用当前方法为划分出来的子数组进行排序
//排序过程最后会将keyNum移动至右端数组的最左端
//故在当前排序结束后keyNum是有序的
//不需要再对keyNum进行排序
reSort(left, n - 1);
reSort(n + 1, right);
}
private int part(int left, int right){
int tail = right;
int keyNum = data[right];
//当前数组最右端的数据已被选为keyNum
//不需要参与比较过程
right--;
while(true){
while(left < right &&
data[left] < keyNum)
left++;
while(right > left &&
data[right] > keyNum)
right--;
if(right == left)
break;
swap(left,right);
}
//整个当前数组都小于等于keyNum
//这时候不需要移动keyNum的位置
//因为整个右端数组只包含keyNum这一个数据项
if(data[right] > keyNum)
swap(right,tail);
return right;
}
private void swap(int left, int right){
int temp = data[right];
data[right] = data[left];
data[left] = temp;
}
public void display(){
for(int i = 0 ; i < data.length ; i++){
System.out.print(data[i] + " ");
}
System.out.print("\n");
}
public static void main(String[] args) {
int[] data = {42, 89, 63, 12, 94, 27, 78, 3, 50, 36};
//int[] data = {1};
QuickSort qs = new QuickSort(data);
qs.sort();
System.out.println("result:");
qs.display();
}
}
排序过程如下图所示:
3 27 12 36 94 89 78 42 50 63 3 12 27 36 94 89 78 42 50 63 3 12 27 36 50 42 63 89 94 78 3 12 27 36 42 50 63 89 94 78 3 12 27 36 42 50 63 78 94 89 3 12 27 36 42 50 63 78 89 94 result: 3 12 27 36 42 50 63 78 89 94
当每次划分都能将数组分为两个大小相等的子数组时,快速排序的效率是最高的,为O(N*logN),因为此时完成排序所需的总划分次数是最少的
考虑如下情况:如果数组为逆序,则选择待划分数组右端数据项作为keyNum将导致快速排序退化为冒泡排序,此时快速排序的效率将变为O(N*N)
所以keyNum的选择对于快速排序来说是非常重要的,keyNum应尽量接近待划分数组数据项的平均值,避免出现keyNum是待划分数组最大数据项或最小数据项的情况
三数据项取中:
public class QuickSort {
private int[] data;
public QuickSort(int[] data){
this.data = data;
}
public void sort(){
this.reSort(0, data.length - 1);
}
private void reSort(int left, int right){
//对于长度少于3的待划分数组,使用冒牌排序
if(right - left + 1 <= 3){
shortPart(left, right);
// System.out.print("normal:");
// display();
}else{
//通过三数据项取中的方式得到keyNum
int p = getMiddleNum(left, right);
int keyNum = data[p];
//普通划分过程
int n = part(left, right, keyNum);
display();
reSort(left, n - 1);
reSort(n + 1, right);
}
}
private void shortPart(int left, int right){
//待划分数组长度为1
if(right - left == 0)
return;
//待划分数组长度为2
else if(right - left == 1){
if(data[right] < data[left])
swap(right, left);
}else{
int middle = left + 1;
if(data[left] > data[middle])
swap(left, middle);
if(data[middle] > data[right])
swap(middle, right);
if(data[left] > data[middle])
swap(left, middle);
}
}
//将3个数据项中的中间值作为keyNum
//并将keyNum移动至3个数据项的中间位置
//避免划分出的2个子数组一个过大一个过小
//如果不将keyNum移动至中间位置,在有些情况下
//会导致划分次数变多,排序效率降低
private int getMiddleNum(int left, int right){
int middle = (left + right)/2;
if(data[left] > data[middle])
swap(left, middle);
if(data[middle] > data[right])
swap(middle, right);
if(data[left] > data[middle])
swap(left, middle);
return middle;
}
private int part(int left, int right, int keyNum){
while(true){
while(left < right &&
data[left] < keyNum)
left++;
while(right > left &&
data[right] >= keyNum)
right--;
if(right == left)
break;
swap(left,right);
}
return right;
}
private void swap(int left, int right){
int temp = data[right];
data[right] = data[left];
data[left] = temp;
}
public void display(){
for(int i = 0 ; i < data.length ; i++){
System.out.print(data[i] + " ");
}
System.out.print("\n");
}
public static void main(String[] args) {
int[] data = {42, 89, 63, 12, 94, 27, 78, 3, 50, 36};
QuickSort qs = new QuickSort(data);
qs.sort();
System.out.println("result:");
qs.display();
}
}
排序过程如下图所示:
36 3 27 12 42 63 78 89 50 94 3 12 27 36 42 63 78 89 50 94 3 12 27 36 42 63 78 50 89 94 result: 3 12 27 36 42 50 63 78 89 94
可以看到确实比使用待划分数组右端数据项的值作为keyNum所需的划分次数要少
PS:
写这个算法费了不少力气,主要是考虑的太过复杂,对于一个长度很长的数据,多1-2次的比较对效率是没什么影响的,反而会增加编码的复杂度,就像递归,递归的效率肯定没有循环的效率高,但是递归可以降低问题的复杂性
浙公网安备 33010602011771号