一、前言
快排是体现了分治法的经典算法,那么我们能从中获取的绝对不止是学到了一个排序算法,更重要的是分治法的核心思想—分。
快排的核心是如何分,然后才是治之。即分而治之。
二、快排的实现
快排的核心在于对待排序数组的划分,然后把小的都放在左边,大的都放在右边,不断的缩小划分的范围,最后出来的就是一个升序的数组。
一个元素是大还是小是相对于一个比较的值而言的,那这个值就叫主元。
例如:{4,3,1}
我们定3为主元,那么1应该放到3左边,4应该放到3的右边,
最后出来的就是 {1,3,4} 这样一个有序的数组。
一个长度为20的数组,第一次划分出左10个右10个元素(划分主元理想状态下)
然后左边的10个再5、5划分,右边的元素再5、5划分,依次进行,每次划分的范围都越来越小,重复着划分排序这一步骤,最后进行合并即可(其实都不用合并,因为当划分完后就是一个有序的数组了)。
很明显这是一个递归调用。划分,每次除了规模大小不一样之外,其他都是一样的。注意:主元也是会动态改变的,并不是固定的。
重点:划分
快排的核心在于划分,划分有三种算法。
掌握划分算法与思想,才是我们学习快排的目的。
划分算法有三种:
1、单向扫描法
思路:三定一循环两交换
步骤
定义一个主元p,默认为左边界第一个元素
定义一个左指针 left(起点,p+1)
定义一个右指针 right(终点,不一定是length-1)
while循环(左指针 <= 右指针)
左指针不断的拿当前指向元素与主元进行对比,
如果:当前元素<=主元
则:左指针右移,右指针不动
否则:左指针不动, 交换左指针与右指针所指向元素,右指针往左移
循环结束后。右指针所指向的就是位置就是主元应该在的位置。
循环结束
两个位置上的元素交换
返回右指针所在位置(此时就是就是划分的左右界限)
简单来说:定义好主元后,左指针不断的往右边走,取出所指向的元素,如果比主元小,就继续走,如果比主元大,就停下来,左指针指向的元素跟右指针指向的元素交换(不管右边过来的元素是多少),右指针往左走一步,然后左指针继续比较过来的元素,小了就继续走,大了就继续和右指针交换元素,右指针往左走,如此反复,知道左指针和右指针擦肩而过了,就停止了。
代码:
// 快速排序
public class Demo26 {
public static void main(String[] args) {
int[] arr = Util.RandomIntArr(14, 1, 20); // 自定义方法,生成长度为15的1-20的随机数组
System.out.print("排序前:");
Util.printlnArr(arr); // 自定义方法,打印数组
quickSort(arr, 0, arr.length-1); // 排序
System.out.print("排序后:");
Util.printlnArr(arr);
}
public static void quickSort(int[] arr, int p, int r) {
if (p < r) {
int mid = partition(arr, p, r);
// 注意理解左右边界的变化
quickSort(arr, p, mid-1); // 划分主元左边
quickSort(arr, mid+1, r); // 划分主元右边
}
}
// 单向扫描法
public static int partition(int[] arr, int p, int r) {
// 定义主元、左指针、右指针
int prvValue = arr[p];
int left = p+1;
int right = r;
while (left <= right) {
if (arr[left] < =prvValue ) {
left ++; // 如果左指针指向的元素小于或等于主元就继续往右走
}else {
Util.swap(arr, left, right); // 否则,和右指针交换元素,右指针往左走一位
right --;
}
}
// 循环结束,将主元与右指针交换元素
Util.swap(arr, right, p);
return right;
}
运行结果:
2、双向扫描法
思路:三定三循环两交换
双向扫描法与单向扫描法的原理基本一致,
区别在于,双向扫描法是左右指针同时往中间走,当左指针遇到比主元大的值就停下来,当右指针遇到比主元小的值也停下来,
当两个指针都停下来后,就进行交换元素操作,交换后符合条件了就继续走,直至两指针交错。
步骤
三定
大循环
左指针循环 --->停下来
右指针循环 ---> 停下来
交换两元素
大循环结束
交换右指针与主元的元素
返回右指针
代码实现
// 快速排序
public class Demo26 {
public static void main(String[] args) {
int[] arr = Util.RandomIntArr(14, 1, 20); // 自定义方法,生成长度为15的1-20的随机数组
System.out.print("排序前:");
Util.printlnArr(arr); // 自定义方法,打印数组
quickSort(arr, 0, arr.length-1); // 排序
System.out.print("排序后:");
Util.printlnArr(arr);
}
public static void quickSort(int[] arr, int p, int r) {
if (p < r) {
int mid = partition2(arr, p, r); // 划分的界限
// 注意理解左右边界的变化
quickSort(arr, p, mid-1); // 划分主元左边
quickSort(arr, mid+1, r); // 划分主元右边
}
}
// 快排划分算法:双向扫描法
public static int partition2(int arr[], int p, int r) {
// 初始化主元、left、right
int prv = arr[p];
int left = p + 1;
int right = r;
// 两指针没有交错
while (left <= right) {
// 两指针没有交错,在<主元的情况下,left一直向右走
// left <= right是防止进来之后越界了
while (left <= right && arr[left] <= prv) {
left ++;
}
// 两指针没有交错,在>主元的情况下,right一直向左走
while (left <= right && arr[right] > prv) {
right --;
}
// 两指针没有交错,且两指针都已经停下了,就交换两个的元素。继续走下去。
if (left <= right) {
Util.swap(arr, left, right);
}
}
// 走完之后,right停下来的位置就是最后一个小于主元的位置,与其交换元素
// 此时,right左边的都是小于或等于主元的元素,右边都是大于主元的元素
// 中间值下标就是right
Util.swap(arr, p, right);
return right;
}
运行结果:
3、三分法
思路:四定两交换一循环
一共有三个指针
分别指向
小于主元的(left),
等于主元的(mid),
大于主元的位置(right)
初始化时:left、mid指针指向同一个位置
当小于主元时,left、mid交换元素,left、mid一起++
(交换元素的意义在于当上次循环是等于的时候,将mid指向等于的那个元素,
此时可以加一个判断,mid != left 的时候才交换)
当等于主元时,left++,mid不动(这是理解上面交换的意义的关键)
当大于主元时,交换left、right的元素,right--
循环结束
交换主元与right的元素
return right
实现代码:
// 快速排序
public class Demo26 {
public static void main(String[] args) {
int[] arr = Util.RandomIntArr(14, 1, 20); // 自定义方法,生成长度为15的1-20的随机数组
System.out.print("排序前:");
Util.printlnArr(arr); // 自定义方法,打印数组
quickSort(arr, 0, arr.length-1); // 排序
System.out.print("排序后:");
Util.printlnArr(arr);
}
public static void quickSort(int[] arr, int p, int r) {
if (p < r) {
int mid = partition3(arr, p, r);
// 注意理解左右边界的变化
quickSort(arr, p, mid-1); // 划分主元左边
quickSort(arr, mid+1, r); // 划分主元右边
}
}
// 快排划分的算法:三分法
public static int partition3(int[] arr, int p, int r) {
// 定义主元、左指针,中指针,右指针
int prv = arr[p]; // 主元
int left = p+1; // 指向小于主元的元素
int mid = p+1; // 指向等于主元的元素
int right = r; // 指向大于主元的元素
while (left <= right) {
if(arr[left] < prv) {
/**
*交换一次的意义是:
*当上一次是相等的时候,mid不动,left ++
*然后遇到小于的时候就交换位置,mid与left交换元素,
*因为mid肯定是指向的元素等于主元,肯定大于当前left指向的元素
*/
if (mid != left) {
Util.swap(arr, mid, left); // 自定义方法,用于数组元素的交换
}
mid ++;
left ++ ;
}
// 当遇到等于的主元的元素时,mid指针就不动了,left继续往右走
else if (arr[left] == prv) {
left ++;
}
// left 遇到大于主元的元素时
else{
Util.swap(arr, left, right);
right -- ;
}
}
Util.swap(arr, p, right);
return right;
}
运行结果:
三、优化
以上代码还有可优化的空间,例如使用三点中值法、绝对中值法来确定主元的值,使的分区更加的合理。
可以自己去研究研究。
最总要的还是掌握分区的思想,即分治的思想。
四、总结
文章写给自己,也写给有需要的人,水平不高,希望大佬们不要吐槽,有什么不对的地方,请指正,谢谢。