快速排序
就像归并之于归并排序,划分是快速排序的核心
划分数据即将数据项分为两组,使全部值小于特定值的数据项在一组,使全部值大于特定值的数据项在另一组
划分算法由两个指针开始,两个指针分别指向数组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次的比较对效率是没什么影响的,反而会增加编码的复杂度,就像递归,递归的效率肯定没有循环的效率高,但是递归可以降低问题的复杂性