数据结构与算法实战(六)快速排序法
一、原理
QuickStart(arr, l, r){
if(l >= r) return;
int p = partition(arr, l, r);
//对arr[l, p - 1]进行排序
QuickStart(arr, l, p-1);
//对arr[p + 1, r]进行排序
QuickStart(arr, p + 1, r);
}
二、基础的Partition实现
private static <E extends Comparable<E>> int partition(E[] arr, int l, int r){
// 循环不变量:arr[l+1 ... j] < v ; arr[j+1 ... i] >= v
int j = l;
for (int i = l + 1; i <= r; i++) {
if(arr[i].compareTo(arr[l]) < 0){
j ++;
swap(arr,i,j);
}
}
swap(arr,l,j);
return j;
}
private static <E> void swap(E[] arr, int i, int j){
E t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
三、快速排序1.0
1、实现
public class QuickSort {
private QuickSort(){}
public static <E extends Comparable<E>> void sort(E[] arr){
sort(arr, 0, arr.length - 1);
}
private static <E extends Comparable<E>> void sort(E[] arr,int l, int r){
if(l >= r) return;
int p = partition(arr, l, r);
sort(arr, l, p - 1);
sort(arr, p + 1, r);
}
private static <E extends Comparable<E>> int partition(E[] arr, int l, int r){
// 循环不变量:arr[l+1 ... j] < v ; arr[j+1 ... i] >= v
int j = l;
for (int i = l + 1; i <= r; i++) {
if(arr[i].compareTo(arr[l]) < 0){
j ++;
swap(arr,i,j);
}
}
swap(arr,l,j);
return j;
}
private static <E> void swap(E[] arr, int i, int j){
E t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
2、问题
public static void main(String[] args) {
int n = 100000;
Integer[] arr = ArrayGenerator.generateOrderedArray(n);//生成1~n的有序数组
SortingHelper.sortTest("QuickSort",arr);
}
发现运行结果栈溢出了
对于完全有序的数组,会发生这种情况,时间复杂度为O(n²),递归深度为O(n)
3、解决
目标:生成一个[l , r]区间的随机值
生成一个[0 , r-l]区间的随机值 再加上l ,就生成了一个[l , r]区间的随机值
在partition方法中加入这样的代码
//生成 [l , r]之间的随机索引
int p = l + new Random().nextInt(r - l + 1);
swap(arr, l , p);
就可以解决此问题
4、另一问题
使用快速排序1.0处理只包含同一元素的数组
public static void main(String[] args) {
int n = 100000;
Integer[] arr2 = ArrayGenerator.generateRandomArray(n,1);//生成都是0的数组
SortingHelper.sortTest("QuickSort",arr2);
}
发现又报出了栈溢出的错位
问题分析
将除了标定点的元素全都划分给arr[j + 1 … i -1]这个区间
四、双路快速排序法
如果i 指向的e大于v 或者 j指向的e小于v,将划分给右区间 or 左区间
而双路排序的思想:i从前向后,j从后向前,对于等于标定点的元素有可能分布在两侧区间中
实现
主要是修改partition方法
private static <E extends Comparable<E>> int partition(E[] arr, int l, int r){
//生成 [l , r]之间的随机索引
int p = l + new Random().nextInt(r - l + 1);
swap(arr, l , p);
//arr[l + 1...i - 1] <= v ; arr[j + 1...r] >= v
int i = l + 1, j = r;
while (true){
while (i <= j && arr[i].compareTo(arr[l])<0)
i ++;
while ((j >= i && arr[j].compareTo(arr[l])>0))
j --;
if(i >= j) break;
swap(arr,i,j);
i ++;
j --;
}
swap(arr,l,j);
return j;
}
使用它测试一下
public static void main(String[] args) {
int n = 500000;
Integer[] arr2 = ArrayGenerator.generateRandomArray(n,1);//生成都是0的数组
SortingHelper.sortTest("QuickSortTwoWays",arr2);
}
发现之前对于完全一样元素的数组栈溢出问题解决。
五、快速排序复杂度分析
最坏复杂度为O(n²) 不过概率非常低
快速排序算法是一个随机算法,使用期望来分析其复杂度
复杂度的期望值是O(n logn)
对于普通算法:
- 看最差:能找到一组数据100%恶化
对于随机算法:
- 看期望:没有一组数据能100%恶化
对于多次调用:
- 尝试使用均摊复杂度分析
六、三路排序算法
对于双路排序算法中一个数组中全是相同元素的情况,在将其划分为arr[l + 1…i - 1] <= v ; arr[j + 1…r] >= v 时,对于左半部分和右半部分还会继续进行快速排序,这是一个重复且没必要的递归过程
而三路的的排序算法是将数组划分为三部分,< v; == v; > v
三路快速排序的思想,引入了lt和gt两个索引
当i 与 gt重合时,循环结束,最后将l和lt指向的元素交换,而l和lt分别向右移了一位。
这样就无需对等于v的元素进行排序
实现
public class QuickSortThreeWays {
private QuickSortThreeWays(){}
public static <E extends Comparable<E>> void sort(E[] arr){
Random random = new Random();
sort(arr, 0, arr.length - 1,random);
}
private static <E extends Comparable<E>> void sort(E[] arr,int l, int r,Random random){
if(l >= r) return;
//生成 [l , r]之间的随机索引
int p = l + random.nextInt(r - l + 1);
swap(arr, l , p);
//arr[l + 1 , lt] < v; arr[lt + 1, i - 1] == v; arr[gt , r] > v
//i 指向当前正在处理的那个元素
int lt = l, i = l + 1, gt = r + 1;
while (i < gt){
if(arr[i].compareTo(arr[l]) < 0){
lt ++;
swap(arr,i,lt);
i ++;
}
else if(arr[i].compareTo(arr[l]) > 0){
gt --;
swap(arr,i,gt);
}else {
//arr[i] == arr[l]
i ++;
}
}
swap(arr,l,lt);
//arr[l , lt - 1] < v; arr[lt, gt - 1] == v; arr[gt , r] > v
sort(arr, l, lt - 1, random);
sort(arr,gt,r,random);
}
private static <E> void swap(E[] arr, int i, int j){
E t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
七、Leetcode75题
75. 颜色分类
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
注意:
不能使用代码库中的排序函数来解决这道题。
示例:
输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
//使用三路快速排序
class Solution {
public void sortColors(int[] nums) {
/**
* 首先定义循环不变量
* nums[0 ... zero] = 0
* nums[zero + 1 ... i - 1] = 1
* nums[two , n - 1] = 2
*/
int zero = -1, i = 0, two = nums.length ;
while (i < two){
if(nums[i] == 0){
zero ++;
swap(nums,zero,i);
i ++;
}
else if(nums[i] == 2){
two --;
swap(nums,i,two);
}
else {
i ++;
}
}
swap(nums,0,zero);
}
private static void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
八、Select K
给出一个无序数组,找出数组的第K小的元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kkvQGwWA-1604994042163)(img/image-20201110103442089.png)]
215. 数组中的第K个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
//使用双路快速排序
class Solution {
public int findKthLargest(int[] nums, int k) {
Random random = new Random();
int len = nums.length;
int targetIndex = len - k;
int l =0, r = len - 1;
while (true){
int p = partition(nums,l,r,random);
if(p == targetIndex)
return nums[p];
else if(p > targetIndex)
r = p - 1;
else
l = p + 1;
}
}
private int partition(int[] arr, int l, int r, Random random){
//生成 [l , r]之间的随机索引
int p = l + random.nextInt(r - l + 1);
swap(arr, l , p);
//arr[l + 1...i - 1] <= v ; arr[j + 1...r] >= v
int i = l + 1, j = r;
while (true){
while (i <= j && arr[i] < arr[l])
i ++;
while (j >= i && arr[j] > arr[l])
j --;
if(i >= j) break;
swap(arr,i,j);
i ++;
j --;
}
swap(arr,l,j);
return j;
}
private void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
剑指 Offer 40. 最小的k个数
难度简单159
输入整数数组 arr
,找出其中最小的 k
个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
Random random = new Random();
int len = arr.length;
int l = 0, r = len - 1;
if (k == 0 || arr.length == 0) {
return new int[0];
}
return quickSort(arr,l,r,k - 1,random);
}
private int[] quickSort(int[] arr, int l,int r,int k,Random random){
int p = partition(arr,l,r,random);
if(p == k)
return Arrays.copyOf(arr,p + 1);
return p > k ? quickSort(arr,l,p - 1, k,random): quickSort(arr,p+1,r, k,random);
}
private int partition(int[] arr, int l, int r, Random random){
int randomIndex = l + random.nextInt(r - l + 1);
swap(arr,l,randomIndex);
//arr[l + 1...i - 1] <= v ; arr[j + 1...r] >= v
int i = l + 1, j = r;
while (true){
while (i <= j && arr[i] < arr[l])
i ++;
while (j >= i && arr[j] > arr[l])
j --;
if(i >= j) break;
swap(arr,i,j);
i ++;
j --;
}
swap(arr,l,j);
return j;
}
private void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}