分治介绍:
分治思想:
①将大问题划分为两个到多个子问题。
②子问题可以继续拆分成更小的问题,直到能够简单求解。
③如果有必要,将子问题的解进行合并,得到原始问题的解。
其实我们之前有学过带有分治思想的一些算法,比如二分查找,二分查找就是将一个大问题拆分为
两个子问题,在子问题中求解。
还有快速排序算法,选定基准点后分区。还有归并排序等等。。。
分治和动态规划有点相似,他们求解时都需要拆分子问题,但是动态规划的子问题有重叠,因此需
要记录之前子问题的解,避免重复运算。然而分治的问题没有重叠。
接下来介绍一种新的算法:
快速选择算法:
它运用了分治的思想
题目:求排在第 i 名的元素, i 从 0 开始,由小到大排
6 5 1 2 4
分析:
求解这道题我们可以借鉴快速排序的思想,先随机选取一个基准点,把小于基准点的元素放在基准点左边,大于基准点的元素放在基准点右边。
如果基准点所在的位置刚好是我们需要求的位置,那直接返回即可,效率得到提升。
如果不是我们需要的位置,基准点的索引值大于我们需要的名次,那么基准点右边的元素的顺序就不用管了,我们只需关心基准点左边的元素就行,接着重复以上的过程即可。
假设我们选了4为基准点,我们需要把小于4的元素放在4左边,大于4的放在4右边。这时我们是知道基准点的索引值的,如果正好我们需要求的是第2名(从第0名开始)的元素,索引值为2恰好等于 i ,我们可以直接返回。
代码实现:
import java.util.concurrent.ThreadLocalRandom;
/**
* 快速选择算法 - 分而治之
*/
public class QuickSelect {
public static int quick(int[] a, int left, int right, int i){
int p = partition(a, left, right); //基准点索引
if(i == p){
return a[p];
}
if(p < i){
return quick(a, p + 1, right, i);
}else{
return quick(a, left, p - 1, i);
}
}
public static void main(String[] args) {
int[] array = {6, 5, 1, 2, 4};
System.out.println(quick(array, 0, array.length - 1, 0));
System.out.println(quick(array, 0, array.length - 1, 1));
System.out.println(quick(array, 0, array.length - 1, 2));
System.out.println(quick(array, 0, array.length - 1, 3));
System.out.println(quick(array, 0, array.length - 1, 4));
}
private static int partition(int[] a, int left, int right) {
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(a, idx, left);
int pv = a[left]; //创建基准点
int i = left + 1;
int j = right;
while (i <= j) {
//1.i 从左向右找到比基准点大的或者相等的
while (i <= j && a[i] < pv) {
i++;
}
//2.j 从右向左找到比基准点小的或者相等的
while (i <= j && pv < a[j]) {
j--;
}
if (i <= j) {
swap(a, i, j);
i++;
j--;
}
}
//基准数归位
swap(a, j, left);
return j;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
快速选择算法的应用:
我们先来看一道例题:
求数组中第 k 大的元素:
注: k 从 1 开始
例题分析:
之前这个题目是用小顶堆做的,但是时间复杂度是O(nlogn),不是很好,我们希望实现O(n)的时间复杂度。
这道题和前面的求排名第几的元素有点像,只不过前面那道题是从小到大来排的,我们只需找到本题和前面那道题的对应关系即可。
由题知,我们要求解数组中第 1 大的元素 ,只需求原数组(从小到大排列)中排名第 4 (第 4 小) 的元素,要求数组中第 2 大的元素,只需求原数组中排名第 3 的元素,以此类推。
因此,我们要求解数组中第 k 大的元素,只需求原数组中排名第 (数组长度 - 1) 的元素。
代码使用之前的就行了。
代码实现:
/**
* 215. 数组中的第K个最大元素
*/
public class FindKthLargestLeetcode215 {
/*
* 由大到小
* 5 4 3 2 1
* 由小到大
* 0 1 2 3 4
* 1 2 4 5 6
*
* */
public int findKthLargest(int[] nums, int k) {
return QuickSelect.quick(nums, 0, nums.length - 1, nums.length - k);
}
public static void main(String[] args) {
// 应为5
FindKthLargestLeetcode215 code = new FindKthLargestLeetcode215();
System.out.println(code.findKthLargest(new int[]{3, 2, 1, 5, 6, 4}, 2));
// 应为4
System.out.println(code.findKthLargest(new int[]{3, 2, 3, 1, 2, 4, 5, 5, 6}, 4));
}
}
注意:这并不代表小顶堆这个算法不好,小顶堆在数据流的场景下会用到,而快速选择算法只用于数组。
我们再来看一道题目:
数组中位数:
现在任意给出一组数组,比如说 1, 2, 4, 5, 6, 求数组中的中位数。
分析:
这道题就是前面求数组中第 i 小的元素的一个变式, 如果数组内的元素个数为奇数时, 那就是求 第 (数组长度) / 2 个元素。
如果数组内的元素个数是偶数,需要求两个元素, 一个是 arr.length / 2, 另一个是 arr.length / 2 - 1(这里指的都是索引)
代码实现:
/**
* 数组中的中位数 - 快速选择
*/
public class FindMedian {
/*
* 中位数 (索引)
* 0 1 2 3 4 ==> arr.length / 2
* 1 2 4 5 6
*
* 0 1 2 3 ==>
* 1 2 4 5 arr.length / 2, arr.length / 2 - 1
*
* */
public static double findMedian(int[] nums){
if(nums.length % 2 == 1){ //奇数
return QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2);
}else{ //偶数
int p = QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2);
int q = QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2 - 1);
return (p + q) / 2.0;
}
}
public static void main(String[] args) {
System.out.println("偶数");
System.out.println(findMedian(new int[]{3, 1, 5, 4}));
System.out.println(findMedian(new int[]{3, 1, 5, 4, 7, 8}));
System.out.println("奇数");
System.out.println(findMedian(new int[]{4, 5, 1}));
System.out.println(findMedian(new int[]{4, 5, 1, 6, 3}));
}
}