分治法应用场景之一:大学选毕业生代表
- 【分】
提出【大范围问题 】 校长告诉大四级长:从所有大四学生中选出毕业生代表
处理为【小范围问题】大四级长告诉每个系主任:从各自的系里面选一个系学生代表
再处理为【最小范围问题】系主任告诉每个班主任:从各自的班里选一个班级学生代表
- 【治】
最先反馈【最小范围问题】每个班主任分别告诉系主任:这是我们班级学生代表。每个系主任从班级代表里面选出最好的成为系学生代表。
再反馈【小范围问题】每个系主任分别告诉大四级长:这是我们的系学生代表。大四级长从系学生代表里面选出最好的成为毕业生代表
解决【大范围问题】大四级长告诉校长:我已经筛选出最好学生成为毕业生代表
- 分治法
强调治理分离。宏观来讲,分治法一个大范围的问题可以切割成若干个小范围的问题。若干个小范围问题处理后一定要反馈给大范围的问题。这种从大范围分割问题,再通过小范围问题返回结果的模式,很适合使用递归的方法。
算法题
- 思路
提出【大范围问题】在数组中找到出现过最多的数
处理为【小范围问题】分别从数组的左半部分,右半部分找一个众数。
再处理为【最小范围问题】数组已不可再分,当前的数就是众数。左右半边都选了个众数,等待上头来决定吧。
以上可以写出代码:
/**
* 分治法 -- 使用系统栈
* @param nums 数组 a[6, 2, 3, 6, 6]
* @param lo 数组低位边界 初始lo = 0
* @param hi 数组高位边界 初始hi = a.length - 1
* @return
*/
private int majorityElement(int[] nums, int lo, int hi) {
// 数组已不可再分
if (lo == hi) {
// 让上头决定的逻辑
}
// 每次都找中间边界
int mid = (hi-lo)/2 + lo;
// 左边的区间找众数
int left = majorityElemeec(nums, lo, mid);
// 右边的区间找众数
int right = majorityElemeec(nums, mid+1, hi);
}
最先反馈【最小范围问题】
private int majorityElement(int[] nums, int lo, int hi) {
// 数组已不可再分
if (lo == hi) {
// 单个数,不需要比较,直接返回
return nums[lo];
}
// 每次都找中间边界
int mid = (hi-lo)/2 + lo;
// 左边的区间找众数
int left = majorityElemeec(nums, lo, mid);
// 右边的区间找众数
int right = majorityElemeec(nums, mid+1, hi);
}
再反馈【小范围问题】当前左右半边的众数已经找到,告诉范围大的数组:需要选举确定那个最合适的代表。
private int majorityElement(int[] nums, int lo, int hi) {
// 数组已不可再分
if (lo == hi) {
// 单个数,不需要比较,直接返回
return nums[lo];
}
// 每次都找中间边界
int mid = (hi-lo)/2 + lo;
// 左边的区间找众数
int left = majorityElemeec(nums, lo, mid);
// 右边的区间找众数
int right = majorityElemeec(nums, mid+1, hi);
// ------------以下处理小范围问题------------------------------------------
// 左右区间返回的众数都一样了,这里不用再选举了,直接返回左边的给上头。
if (left == right) {
return left;
}
// 左右区间拿到的众数不一样,需要选举。这次选举一定要公平,所以合并左右区间进行选举
int leftCount = countInRange(nums, left, lo, hi);
int rightCount = countInRange(nums, right, lo, hi);
// 返回选票多的区间的众数,如果选举票数一样,返回右边区间众数
return leftCount > rightCount ? left : right;
}
选举函数:
private int countInRange(int[] nums, int num, int lo, int hi) {
int count = 0;
for (int i = lo; i <= hi; i++) {
if (nums[i] == num) {
// 区间内出现过一次选票增加1
count++;
}
}
return count;
}
解决【大范围问题】本数组的左右半边都以选出众数,我认为x是最好的代表。
// 上面代码已经蕴含了这个逻辑
int leftCount = countInRange(nums, left, lo, hi);
int rightCount = countInRange(nums, right, lo, hi);
// 由于题目:给定的数组总是存在众数,那么在最后一次大范围问题是遍历整个数组进行二选一,必定有结果。
return leftCount > rightCount ? left : right;
最终代码
public class Solution2 {
/**
* 分治法 -- 使用系统栈
* @param nums 数组
* @param lo 数组低位边界
* @param hi 数组高位边界
* @return
*/
private int majorityElemeec(int[] nums, int lo, int hi) {
// 左右值重合,则说明该数组大小只有 1, 直接返回元素,属于递归到底的情况
if (lo == hi) {
return nums[lo];
}
int mid = (hi-lo)/2 + lo;
int left = majorityElemeec(nums, lo, mid);
int right = majorityElemeec(nums, mid+1, hi);
// 左右区间拿到的众数一样直接返回
if (left == right) {
return left;
}
// 左右区间拿到的众数不一样,需要选举。这次选举一定要公平,所以合并左右区间进行选举
int leftCount = countInRange(nums, left, lo, hi);
int rightCount = countInRange(nums, right, lo, hi);
// 如果选举票数一样,返回右边区间众数
return leftCount > rightCount ? left : right;
}
/**
* 选举方法
* @param nums 数组
* @param num 待选举的数
* @param lo 数组低位边界
* @param hi 数组高位边界
* @return
*/
private int countInRange(int[] nums, int num, int lo, int hi) {
int count = 0;
for (int i = lo; i <= hi; i++) {
if (nums[i] == num) {
count++;
}
}
return count;
}
// 给最上层调用
public int majorityElement(int[] nums) {
return majorityElemeec(nums, 0, nums.length-1);
}
}
-
优化的点
分治法的时间复杂度:O(nlogn); 空间复杂度:O(logn);
哈希算法的时间复杂度:O(n);空间复杂度O(n)此题使用分治法是使用时间换空间的解决方案。
-
不足
Boyer-Moore 投票算法,可以不牺牲时间复杂度就降低空间复杂度,是这道题的最佳解法。