一,思想描述:
给定原始数列如下,要求从小到大排序:
一开始和挖坑法相似,我们首先选定基准元素Pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素:
接下来是第一次循环,从right指针开始,把指针所指向的元素和基准元素做比较。如果大于等于pivot,则指针向左移动;
如果小于pivot,则right指针停止移动,切换到left指针。
轮到left指针行动,把指针所指向的元素和基准元素做比较。如果小于等于pivot,则指针向右移动;
如果大于pivot,则left指针停止移动。
这时候,我们让left和right指向的元素进行交换.
接下来,我们进入第二次循环、第三次循环…,
进入最后一次循环,right移动到元素x停止,这时候请注意,left和right指针已经重合在了一起。
当left和right指针重合之时,我们让pivot基准值元素和left指针与right指针重合点的元素进行交换。
此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。
二,代码演示—以首位元素为基准值:
public class KuaiPai14 {
public static void main(String[] args) {
// int[] arr = new int[]{4, 7, 6, 5, 3, 2, 8, 1};
int arr[] = new int[]{7, 2, -6, -3, 4, 5, 31, 8, 9, 10};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
/**
* 快速排序--指针交换法
* @param arr 数组
* @param startIndex 左边界索引
* @param endIndex 右边界索引
*/
public static void quickSort(int[] arr, int startIndex, int endIndex) {
// 递归结束条件:startIndex大等于endIndex的时候
if (startIndex >= endIndex) {
return;
}
// 得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 根据基准元素,分成两部分递归排序
// 用分治法递归数列的两部分
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
/**
* 具体每一轮的快速排序:
* @param arr 数组
* @param startIndex 左边界索引
* @param endIndex 右边界索引
* @return 返回基准值的位置,此时基准值左边的元素都小于基准值,基准值右边的元素都大于基准值
*/
private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
//初始化左右游标/指针
int leftYb = startIndex;
int rightYb = endIndex;
//大循环在左右指针重合时结束
while (leftYb < rightYb) {
// 先遍历右边, 控制right指针比较并左移
// leftYb<rightYb ,左游标永远小于右游标,是遍历元素并发生元素变动的前提:
while (leftYb < rightYb && arr[rightYb] >= pivot) {
// 如果元素大于基准值--右游标左移:
rightYb--;
}
//跳出了上边while循环,说明此时的右游标所在元素小于基准值
//再遍历左边, 控制right指针比较并右移
// leftYb<rightYb ,左游标永远小于右游标,是遍历元素并发生元素变动的前提:
while (leftYb < rightYb && arr[leftYb] <= pivot) {
//如果元素小于基准值--左游标右移,
leftYb++;
}
//跳出了上边while循环,说明此时的左游标所在元素大于基准值,
//交换左游标和右游标所在位置的元素
if (leftYb < rightYb) {
int temp = arr[leftYb];
arr[leftYb] = arr[rightYb];
arr[rightYb] = temp;
}
}
//跳出了大循环,说明此时此刻左右游标是重合的,
//将pivot基准值和左右游标重合位置的元素进行交换
//arr[startIndex]-->pivot基准值
//这里也可以是arr[rightYb]
int temp = arr[leftYb];
arr[leftYb] = arr[startIndex];
arr[startIndex] = temp;
//返回当前基准值所在位置,
//此时,基准值左边的元素都小于基准值,基准值右边的元素都大于基准值,这一轮快排宣告结束;
return leftYb;
}
}
三,随机选取基准值—代码演示:
对于快速排序来说,最简单的方式是选择数列的第一个元素,这种选择在绝大多数情况是没有问题的。
但是,假如有一个原本逆序的数列,期望排序成顺序数列,那么会出现什么情况呢?
当数据有序逆序时,以第一个关键字为基准分为两个子序列,前一个子序列为空,此时执行效率最差,如果每一轮快排都是如此,那么时间复杂度为n^2。
随机选择基准 可以降低逆序带来的O(n^2)复杂度的可能性。
基准选择的三种方式:
- 固定基准 (比如第一个),当原始序列或子序列逆序时,复杂度接近O(n^2),所以应该避免使用这种方式。
- 随机选择 ,降低了逆序复杂度变高的可能性,但仍然不容乐观。
- 三数取中,随机选择三个数,取中间大小的作为基准,当然,本身三个数都是不确定的,随机选择无意义,直接取原始数列的头、中、尾三个元素,得出中间大小作为基准即可。进一步降低了数列逆序的可能性。
可以看到,即使是性能最差的 固定基准 ,也不一定是 选择第一个元素作为基准 ,所以,我们应该掌握的快排,是不以第一个元素为基准的快速排序。
既然,以 第一个元素为基准的快速排序 ,理解和代码实现都是最简单的,那么我们为什么不这样写呢?
无论你使用了哪种 基准选择 的方式,在 元素移动 之前,你都可以让你的 基准 先与 第一个元素 进行 交换,这样是不是就变成了你手到擒来的快速排序?
所以你需要记住快速排序的总过程是:
- 选择基准
- 与第一元素交换
- 元素移动
通过代码分析,我们得出结论,当 随机选择基准 时,无论是挖坑法还是指针交换法,都会增加代码复杂度,甚至,循环判断降低了代码性能。
- 挖坑法,增加判断条件
- 指针交换法,影响指针优先移动顺序
不妨,将复杂问题简单化,统一快速排序的过程
- 选择基准
- 与第一元素交换
- 元素移动
/**
* 快速排序 -- 指针交换法
* @param arr 数组
* @param leftIndex 数组的左边下标索引
* @param rightIndex 数组的右边下标索引
*/
public static void quicksort(int[] arr, int leftIndex, int rightIndex) {
// 初始化左右游标:
int leftYouBiao = leftIndex;
int rightYouBiao = rightIndex;
//随机找一个基准值:
int randomIndex = RandomUtils.nextInt(leftIndex, rightIndex + 1);
//简单起见,不徒增复杂度,将当前基准值和数组首位元素互换位置:
int temp = arr[randomIndex];
arr[randomIndex] = arr[leftIndex];
arr[leftIndex] = temp;
//初始化交换位置之后的基准值:
int pivoltVal = arr[leftIndex];
int pivoltIndex = leftIndex;
//开始循环遍历数组,左边比中轴值大的和右边比中轴值小的对调:
while (leftYouBiao < rightYouBiao) {
//先遍历右边:
//右边比中轴值大的,略过:
while (leftYouBiao < rightYouBiao && arr[rightYouBiao] >= pivoltVal) {
rightYouBiao--;
}
//左边比中轴值小的,略过:
while (leftYouBiao<rightYouBiao && arr[leftYouBiao] <= pivoltVal) {
leftYouBiao++;
}
//上边两个循环已经过去,
// 1,此时说明左游标所在位置元素大于中轴值、右游标所在位置元素小于中轴值:
if (leftYouBiao < rightYouBiao) {
// 此时,对调元素:
int temp2 = arr[leftYouBiao];
arr[leftYouBiao] = arr[rightYouBiao];
arr[rightYouBiao] = temp2;
}
}
//此时,跳出大循环,说明此时左右游标重合,将基准值与重合位置元素对调:
int temp3 = arr[pivoltIndex];
arr[pivoltIndex] = arr[leftYouBiao];
arr[leftYouBiao] = temp3;
//递归调用:
if (leftYouBiao - 1 > leftIndex) {
quicksort(arr, leftIndex, leftYouBiao-1);
}
if (rightIndex > rightYouBiao+1) {
quicksort(arr, rightYouBiao+1, rightIndex );
}
}
牛客网测试:
四,为什么最后要将基准值和重合位置元素进行交换呢?
指针交换法,为什么最后将基准值和重合位置元素进行交换呢?
疑问:基准值在首位,如果重合位置元素大于基准值,和处在首位的基准值交换之后,那么该大于基准值的重合位置元素岂不是在了基准值的左边?这就不符合快速排序了?
答:先说结论,重合位置的元素一定是小于基准值的。
因为代码中,每一次交换完元素之后,都是右指针先开始左移遍历,只有右指针遇到小于基准值元素停下之后,才开始左指针右移遍历。
所以,左右指针重合有两种情况:
1,右指针不动、左指针追上了右指针;
这种情况是:上一次交换完元素之后,右指针率先进行了左移遍历,然后遇到了小于基准值元素之后停下,然后左指针开始右移遍历,
左指针右移遍历,一路上都是小于基准值的元素,所以左指针一路无阻,一直追上了右指针,两指针重合;
2,左指针不动,右指针追上了左指针;
这种情况是:上一次交换完元素之后,右指针率先再次进行了左移遍历,一路上都是大于基准值的元素,所以右指针一路无阻,一直追上了上一次交换完元素之后压根还没动的左指针。
3,总结:
对于第一种情况,右指针不动、左指针追上了右指针,此时,重合的位置一定是右指针遇到了小于基准值的元素了的位置,所以此时的重合位置的元素一定是小于基准值的,
此时基准值在首位,所以重合位置元素和基准值交换完之后,该重合位置元素一定在基准值左边。
对于第二种情况,左指针不动,右指针追上了左指针,此时,左指针是上一次刚刚交换过位置的、原本是处在右边的小于基准值的那个元素,
所以此时重合位置的元素,即左指针所在位置的元素,也一定是小于基准值的,所以和基准值交换完之后,该处在重合位置且小于基准值的元素也一定在基准值的左边。
·
·