算法 - 快速排序 - 经典快排 | 随机快排
经典快排
经典快排的思路是选取数组的最后一个数 x,按照问题一的思路把整个数组划分成 小于等于 x | 大于 x
两个部分,将 x 和 大于 x 部分数组的第一个元素交换位置。此时整个数组划分成 小于等于 x | x | 大于 x
三个部分,也就是这一次排序将 x 值排好位置。
再分别对 小于等于 x
和 大于 x
中的数组递归划分,直到划分成一个数,此时所有元素也完成排序。
按照问题二的思路可以对经典快排做改进,使得每次划分数组成为小于 x | 等于 x | 大于 x
三个部分,通过这种排序方式可以一次划分多个 x 值的位置,排序效率得到提高。
但是,经典快排出现问题与数据状况有关。每次选择 x 值都是数组的最后一个数,如果遇到 [1,2,3,4,5]
或者 [5,4,3,2,1]
这种数组,算法时间复杂度将变成 O(n^2)。
随机快排
随机快排是经典快排的一种改进,通过生成随机下标 i,选择 a[i] 和最后一个数 x 进行交换,再使用经典快排。此时的事件就是一个概率事件,需要使用期望来估计算法的时间复杂度。
仍以 [1,2,3,4,5]
为例,经过随机快排初始变换,可以形成下列五种情况,数据状况的影响有效降低。在长期期望下,随机快排算法的时间复杂度为 O(N*logN)。由于每次划分数据都需要记录 =x 数组的下标范围,因此额外的空间复杂度为 O(logN)。
5,2,3,4,1;
1,5,3,4,2;
1,2,5,4,3;
1,2,3,5,4;
1,2,3,4,5.
思想
随机快排和经典快排的差别就在于添加了一行代码,使比较的数 x 具有随机性。
通过这种随机的方法处理特殊的数据,使得算法具有更好的鲁棒性。
单边循环法和双边循环法
上述快排的实现都是从一个方向上遍历元素,然后分成两个数组,成为单边循环法。还有另一种实现的方法是双边循环,在《大话数据结构》书中可以看到实现。
虽然实现方法多种多样,但是其核心本质仍然是选取数组中的值与数组其他元素比较大小,经过一轮循环之后将数组分成两个部分。单边循环和双边循环本质上是一样的,只是实现方式上不同。
快排代码
思路:使用 quickSort() 函数处理数组,先进行随机处理,使用核心方法 partition() 将数据分成 小于 x | 等于 x | 大于 x
三个部分,返回等于区域的左右下标值 [a, b],递归调用 小于 x
区域和 大于 x
区域。
quickSort(arr, left, right) {
if (left < right)
swap(); //随机快排的改动处
int[] p = partition(arr, left, right); //通过方法返回的是分组后确定的 = num 区域范围
quickSort(arr, left, p[0] - 1); //递归 < num 区域
quickSort(arr, p[1] + 1, right); //递归 > num 区域
}
partition(); //就是荷兰国旗问题的过程
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int left, int right) {
//多余判断
//if (left == right) {
// return;
//}
if (left < right) {
//随机快排核心
//Math.random() 取值范围 [0, 1)
//(Math.random() * (right - left + 1)) 此处数值 <= (right - left),因此可以保证参数 first 在区间内
swap(arr, left + (int) (Math.random() * (right - left + 1)), right);
int[] p = partition(arr, left, right);
quickSort(arr, left, p[0] - 1);
quickSort(arr, p[1] + 1, right);
}
}
public static int[] partition(int[] arr, int left, int right) {
int less = left - 1;
int more = right + 1;
//int more = right;
//left 成为数组遍历的 cur 指针,当触碰到 more 边界时终止循环
while (left < more) {
if (arr[left] < arr[right]) {
swap(arr, less + 1, left);
less++;
left++;
//左神代码
//swap(arr, ++less, left++);
} else if (arr[left] == arr[right]) {
left++;
} else {
swap(arr, left, more - 1);
more--;
//左神代码
//swap(arr, left, --more);
}
}
return new int[]{less + 1, more - 1};
//return new int[]{less + 1, more};
}
手写代码过程中发现有些判断多余,因此做了注释。在整体代码中,有些语句能够精简成更简洁的代码,但是为了思路的连贯性,仅将简洁的代码做注释保留。从这些精简的代码中也可以看到一些优雅的代码真令人惊叹。