【算法】快速排序算法的编码和优化
参考资料
快速排序算法的编码描述
快排的基本思路
- 先通过第一趟排序,将数组原地划分为两部分,其中一部分的所有数据都小于另一部分的所有数据。原数组被划分为2份
- 通过递归的处理, 再对原数组分割的两部分分别划分为两部分,同样是使得其中一部分的所有数据都小于另一部分的所有数据。 这个时候原数组被划分为了4份
- 就1,2被划分后的最小单元子数组来看,它们仍然是无序的,但是! 它们所组成的原数组却逐渐向有序的方向前进。
- 到最后, 数组被划分为多个由一个元素或多个相同元素组成的单元, 这时候整个数组就有序了
3 1 4 1 5 9 2 6 5 3
1 1 2 3 3 4 5 5 6
快排的实现步骤
下面我就只讲解1和2步骤, 而在1,2中,关键在于如何实现“划分”
切分的关键点: 基准元素, 左游标和右游标
划分的过程有三个关键点:“基准元素”, “左游标” 和“右游标”。
- 基准元素:它是将数组划分为两个子数组的过程中, 用于界定大小的值, 以它为判断标准, 将小于它的数组元素“划分”到一个“小数值数组”里, 而将大于它的数组元素“划分”到一个“大数值数组”里面。这样,我们就将数组分割为两个子数组, 而其中一个子数组里的元素恒小于另一个子数组里的元素
- 左游标: 它一开始指向待分割数组最左侧的数组元素。在排序过程中,它将向右移动
- 右游标: 它一开始指向待分割数组最右侧的数组元素。在排序过程中,它将向左移动
一趟切分的具体过程
- “基准元素v是怎么选的?”
- 游标i,j的移动的过程中发生了什么事情(比如元素交换)?,
- 为什么左右游标相遇时一趟切分就完成了?
- 左游标向右扫描, 跨过所有小于基准元素的数组元素, 直到遇到一个大于或等于基准元素的数组元素, 在那个位置停下。
- 右游标向左扫描, 跨过所有大于基准元素的数组元素, 直到遇到一个大于或等于基准元素的数组元素,在那个位置停下
- 左右游标没有相遇
- 左右游标相遇了
【注意】这里你可能会问: 在我们制定的规则里, 左游标先扫描和右游标先扫描有区别吗? (如果你这样想的话就和我想到一块去了...嘿嘿),因为就上图而言,两种情况下一趟排序中两个游标相遇的位置是不同的(一般而言,除非相遇位置的下方的元素刚好和基准元素相同):
- 如果右游标先扫描,左右游标相遇的位置应该是3上方(图示)
- 但如果左游标先扫描, 左右游标相遇的位置却是9上方
6 1 2 5 4 3 9 7 10 8
1 2 5 4 3 6 9 7 10 8
总结一趟排序的过程
快速排序代码展示
具体的代码
// 交换两个数组元素 private static void exchange(int [] a , int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; }
private static int partition (int[] a, int low, int high) { int i = low, j = high+1; // i, j为左右扫描指针 PS: 思考下为什么j比i 多加一个1呢? int pivotkey = a[low]; // pivotkey 为选取的基准元素(头元素) while(true) { while (a[--j]>pivotkey) { if(j == low) break; } // 右游标左移 while(a[++i]<pivotkey) { if(i == high) break; } // 左游标右移 if(i>=j) break; // 左右游标相遇时候停止, 所以跳出外部while循环 else exchange(a,i, j) ; // 左右游标未相遇时停止, 交换各自所指元素,循环继续 } exchange(a, low, j); // 基准元素和游标相遇时所指元素交换,为最后一次交换 return j; // 一趟排序完成, 返回基准元素位置 }
private static void sort (int [] a, int low, int high) { if(high<= low) { return; } // 终止递归 int j = partition(a, low, high); // 调用partition进行切分 sort(a, low, j-1); // 对上一轮排序(切分)时,基准元素左边的子数组进行递归 sort(a, j+1, high); // 对上一轮排序(切分)时,基准元素右边的子数组进行递归 }
对切分函数partition的解读
while (a[--j]>pivotkey) { ... }
先将右游标左移一位,然后判断指向的数组元素和基准元素pivotkey比较大小, 如果该元素大于基准元素, 那么循环继续,j再次减1,右游标再次左移一位...... (循环体可以看作是空的)
if(i>=j) break;
从i < j到 i == j 代表了“游标未相遇”到“游标相遇”的过度过程,此时跳出外部循环, 切分已接近完成,紧接着通过 exchange(a, low, j) 交换基准元素和相遇游标所指元素的位置, low是基准元素的位置(头部元素), j是当前两个游标相遇的位置
while (a[--j]>pivotkey) { if(j == low) break; } // 右游标左移
中,当随着右游标左移,到j = low + 1的时候,有 a[--j] == pivotkey为true(两者都是基准元素),自动跳出了while循环,所以就不需要在循环体里再判断 j == low 了
int i = low, j = high+1
结合下面两个While循环中的判断条件:
while (a[--j]>pivotkey) { ... } while (a[++i]<pivotkey) { ... }
可知道, 左游标 i 第一次自增的时候, 跳过了对基准元素 a[low] 所执行的 a[low] < pivotkey判断, 这是因为在我们当前的算法方案里,基准元素和左游标初始所指的元素是同一个,所以没有执行a[low]>pivotke这个判断的必要。所以跳过( 一开始a[low] == pivotkey,如果执行判断那么一开始就会跳出内While循环,这显然不是我们希望看到的)
对主体函数sort的解读
sort(a, low, j-1); sort(a, j+1, high);
进行下一轮递归,设置j -1 和j + 1 是因为上一轮基准元素的位置已经是有序的了,不要再纳入下一轮递归里
public class QuickSort { // 交换两个数组元素 private static void exchange(int [] a , int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } private static int partition (int[] a, int low, int high) { int i = low, j = high+1; // i, j为左右扫描指针 PS: 思考下为什么j比i 多加一个1呢? int pivotkey = a[low]; // pivotkey 为选取的基准元素(头元素) while(true) { while (a[--j]>pivotkey) { if(j == low) break; } // 右游标左移 while(a[++i]<pivotkey) { if(i == high) break; } // 左游标右移 if(i>=j) break; // 左右游标相遇时候停止, 所以跳出外部while循环 else exchange(a,i, j) ; // 左右游标未相遇时停止, 交换各自所指元素,循环继续 } exchange(a, low, j); // 基准元素和游标相遇时所指元素交换,为最后一次交换 return j; // 一趟排序完成, 返回基准元素位置 } private static void sort (int [] a, int low, int high) { if(high<= low) { return; } // 当high == low, 此时已是单元素子数组,自然有序, 故终止递归 int j = partition(a, low, high); // 调用partition进行切分 sort(a, low, j-1); // 对上一轮排序(切分)时,基准元素左边的子数组进行递归 sort(a, j+1, high); // 对上一轮排序(切分)时,基准元素右边的子数组进行递归 } public static void sort (int [] a){ //sort函数重载, 只向外暴露一个数组参数 sort(a, 0, a.length - 1); } }
public class Test { public static void main (String [] args) { int [] array = {4,1,5,9,2,6,5,6,1,8,0,7 }; QuickSort.sort(array); for (int i = 0; i < array.length; i++) { System.out.print(array[i]); } } }
01124556789
优化点一 —— 切换到插入排序
if(high<= low) { return; }
if(high<= low + M) { Insertion.sort(a,low, high) return; } // Insertion表示一个插入排序类
private static void sort (int [] a, int low, int high) { if(high<= low + 10) { Insertion.sort(a,low, high) return; } // Insertion表示一个插入排序类 int j = partition(a, low, high); // 调用partition进行切分 sort(a, low, j-1); // 对上一轮排序(切分)时,基准元素左边的子数组进行递归 sort(a, j+1, high); // 对上一轮排序(切分)时,基准元素右边的子数组进行递归 }
优化点二 —— 基准元素选取的随机化
- 排序前打乱数组的顺序
- 通过随机数保证取得的基准元素的随机性
- 三数取中法取得基准元素(推荐)
public static void sort (int [] a){ StdRandom.shuffle(a) // 外部导入的乱序算法,打乱数组的分布 sort(a, 0, a.length - 1); }
private static int getRandom (int []a, int low, int high) { int RdIndex = (int) (low + Math.random()* (high - low)); // 随机取出其中一个数组元素的下标 exchange(a, RdIndex, low); // 将其和最左边的元素互换 return a[low]; } private static int partition (int[] a, int low, int high) { int i = low, j = high+1; // i, j为左右扫描指针 PS: 思考下为什么j比i 多加一个1呢? int pivotkey = getRandom (a, low, high); // 基准元素随机化 while(true) { while (a[--j]>pivotkey) { if(j == low) break; } // 右游标左移 while(a[++i]<pivotkey) { if(i == high) break; } // 左游标右移 if(i>=j) break; // 左右游标相遇时候停止, 所以跳出外部while循环 else exchange(a,i, j) ; // 左右游标未相遇时停止, 交换各自所指元素,循环继续 } exchange(a, low, j); // 基准元素和游标相遇时所指元素交换,为最后一次交换 return j; // 一趟排序完成, 返回基准元素位置 }
package mypackage; public class QuickSort { // 交换两个数组元素 private static void exchange(int [] a , int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } // 选取左中右三个元素,求出中位数, 放入数组最左边的a[low]中 private static int selectMiddleOfThree(int[] a, int low, int high) { int middle = low + (high - low)/2; // 取得位于数组中间的元素middle
if(a[low]>a[high]) { exchange(a, low, high); //此时有 a[low] < a[high] } if(a[middle]>a[high]){ exchange(a, middle, high); //此时有 a[low], a[middle] < a[high] } if(a[middle]>a[low]) { exchange(a, middle, low); //此时有a[middle]< a[low] < a[high] } return a[low]; // a[low]的值已经被换成三数中的中位数, 将其返回 } private static int partition (int[] a, int low, int high) { int i = low, j = high+1; // i, j为左右扫描指针 PS: 思考下为什么j比i 多加一个1呢? int pivotkey = selectMiddleOfThree( a, low, high); while(true) { while (a[--j]>pivotkey) { if(j == low) break; } // 右游标左移 while(a[++i]<pivotkey) { if(i == high) break; } // 左游标右移 if(i>=j) break; // 左右游标相遇时候停止, 所以跳出外部while循环 else exchange(a,i, j) ; // 左右游标未相遇时停止, 交换各自所指元素,循环继续 } exchange(a, low, j); // 基准元素和游标相遇时所指元素交换,为最后一次交换 return j; // 一趟排序完成, 返回基准元素位置 } private static void sort (int [] a, int low, int high) { if(high<= low) { return; } // 当high == low, 此时已是单元素子数组,自然有序, 故终止递归 int j = partition(a, low, high); // 调用partition进行切分 sort(a, low, j-1); // 对上一轮排序(切分)时,基准元素左边的子数组进行递归 sort(a, j+1, high); // 对上一轮排序(切分)时,基准元素右边的子数组进行递归 } public static void sort (int [] a){ //sort函数重载, 只向外暴露一个数组参数 sort(a, 0, a.length - 1); } }
优化点三 —— 去除不必要的边界检查
while(a[++i]<pivotkey) { if(i == high) break; }
我们只要尝试把这一作用交给a[++i]<pivotkey去完成, 不就可以把 if(i == high) break; 给去掉了吗?
public class QuickSort { // 交换两个数组元素 private static void exchange(int [] a , int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } //将原数组里最大的元素移到最右边, 构造“哨兵” private static void Max(int [] a) { int max = 0; for(int i = 1; i<a.length;i++) { if(a[i]>a[max]) { max = i; } } exchange(a, max, a.length -1); } private static int partition (int[] a, int low, int high) { int i = low, j = high+1; // i, j为左右扫描指针 PS: 思考下为什么j比i 多加一个1呢? int pivotkey = a[low]; // pivotkey 为选取的基准元素(头元素) while(true) { while (a[--j]>pivotkey) { } // 空的循环体 while(a[++i]<pivotkey) { } // 空的循环体 if(i>=j) break; // 左右游标相遇时候停止, 所以跳出外部while循环 else exchange(a,i, j) ; // 左右游标未相遇时停止, 交换各自所指元素,循环继续 } exchange(a, low, j); // 基准元素和游标相遇时所指元素交换,为最后一次交换 return j; // 一趟排序完成, 返回基准元素位置 } private static void sort (int [] a, int low, int high) { if(high<= low) { return; } // 当high == low, 此时已是单元素子数组,自然有序, 故终止递归 int j = partition(a, low, high); // 调用partition进行切分 sort(a, low, j-1); // 对上一轮排序(切分)时,基准元素左边的子数组进行递归 sort(a, j+1, high); // 对上一轮排序(切分)时,基准元素右边的子数组进行递归 } public static void sort (int [] a){ //sort函数重载, 只向外暴露一个数组参数 Max(a); // 将原数组里最大元素移到最右边, 构造“哨兵” sort(a, 0, a.length - 1); } }
优化点四 —— 三切分快排(针对大量重复元素)
- 左游标向右扫描, 跨过所有小于基准元素的数组元素, 直到遇到一个大于或等于基准元素的数组元素, 在那个位置停下。
- 右游标向左扫描, 跨过所有大于基准元素的数组元素,直到遇到一个大于或等于基准元素的数组元素,在那个位置挺停下
package mypackage; public class Quick3way { public static void exchange(int [] a , int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } public static void sort (int [] a, int low, int high) { if(low>=high) { return; } int lt = low, gt = high, i =low+1; int v = a[low]; while(i<=gt) { int aValue = a[i]; if(aValue>v) { exchange(a, i, gt--); } else if(aValue<v) { exchange(a, i++, lt++); } else{ i++; } } sort(a, low, lt-1); sort(a, gt+1, high); } public static void sort (int [] a) { sort(a, 0, a.length - 1); } }