leetcode 分治算法
分治算法入门
以下节选自 leetcode上的入门题
分治算法
所谓的分治算法通俗来讲,就是将大的问题拆解成许多单一的子问题,通过解决子问题,并合并子问题结果反推原问题。也就是递归的思想。
采用暴力算法,依次遍历数组中每个元素出现的次数,时间复杂度为O(n*n),会超时。肯定不是改题目的本意。
方法一:分治思想,递归思路
采用分治思想,递归思路。
* 1、确定切分的终止条件,直到所有的子问题都是长度为 1 的数组,停止切分。
* 2、拆分数组,递归地将原数组二分为左区间与右区间,直到最终的数组只剩下一个元素,将其返回
* 3、处理子问题得到子结果,并合并
* 3.1 长度为 1 的子数组中唯一的数显然是众数,直接返回即可。
* 3.2 如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。
* 3.3 如果他们的众数不同,比较两个众数在整个区间内出现的次数来决定该区间的众数
代码实现:
public class Main {
/**
* 采用分治思想,递归思路。
* 1、确定切分的终止条件,直到所有的子问题都是长度为 1 的数组,停止切分。
* 2、拆分数组,递归地将原数组二分为左区间与右区间,直到最终的数组只剩下一个元素,将其返回
* 3、处理子问题得到子结果,并合并
* 3.1 长度为 1 的子数组中唯一的数显然是众数,直接返回即可。
* 3.2 如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。
* 3.3 如果他们的众数不同,比较两个众数在整个区间内出现的次数来决定该区间的众数
* @param nums
* @return
*/
public int majorityElement(int[] nums) {
if (nums.length < 1) return 0;
return help(nums, 0, nums.length - 1);
}
private int help(int[] nums, int start, int end) {
// 1、拆分数组,直到剩下最后一个 一定为众数
if (start == end) return nums[start];
// 2、处理子问题
int mid = start + (end - start) / 2;
int left = help(nums,start,mid);
int right = help(nums, mid+1,end);
// 如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。
if (left == right)
return left;
// 统计左右区间的众数
int leftCount = countElement(nums, left, start, end);
int rightCount = countElement(nums, right, start, end);
return leftCount > rightCount ? left : right;
}
private int countElement(int[] nums, int num, int start, int end) {
int count = 0;
for (int i = start; i <= end; i++) {
if (num == nums[i])
count++;
}
return count;
}
public static void main(String[] args) {
Main test = new Main();
int[] nums = {3,3,3,2,1};
System.out.println(test.majorityElement(nums));
}
}
复杂度分析
- 时间复杂度:O($nlogn$)
- 空间复杂度:O($nlogn$)。用到了递归,需要调用栈,所以复杂度为O($nlong$)
递归讲起来比较抽象,需要debug看看调用栈的信息,下面介绍个好理解的方法
方法二:HashMap
HashMap采用key-value的形式存储数据,刚好可以统计数组中各元素出现的次数。
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Main {
public static int majorityElement(int[] nums) {
HashMap<Integer, Integer> map = new HashMap<>();
// 统计各元素出现的次数
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i],0) + 1);
}
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
for (Map.Entry<Integer,Integer> entry : entries) {
// 找出满足条件的众数
if (entry.getValue() > (nums.length / 2)) {
return entry.getKey();
}
}
return 0; // 题目描述一定存在,所以这块不会被执行到,返回不报错的整数即可
}
public static void main(String[] args) {
int[] nums = {3,3,3,2,1};
System.out.println(Main.majorityElement(nums));;
}
}
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(n). 哈希表中最多包含 n - n/2个键值对,所以占用空间为O(n). 题目保证一定有一个众数,而一个长度为n的数组最多只包含n个不同的值,会占用n/2 + 1个数字,所以最多有n-(n/2+1)个不同的其他数字,所以最多有n-n/2(取整)个不同的元素。
更多方法参考:leetcode讨论区
方法一:暴力算法
public int maxSubArray(int[] nums) {
int res = Integer.MIN_VALUE; // 每次遍历寻找最大子序和
for (int i = 0; i < nums.length; i++) {
int sum = 0; // 用来保存子序列的和
for (int j = i; j < nums.length; j++) {
sum += nums[j];
res = Math.max(res, sum);
}
}
return res;
}
复杂度分析:
- 时间复杂度:O(N*N)
- 空间复杂度:O(1)
方法二:动态规划
第 i 个子组合的最大值可以通过第i-1个子组合的最大值和第 i 个数字获得,如果第 i-1 个子组合的最大值没法给第 i 个数字带来正增益,我们就抛弃掉前面的子组合,自己就是最大的了。
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0]; // 初始化
for (int i = 1; i < nums.length; i++) {
// 状态转移方程
if (dp[i-1] >= 0) {
dp[i] = dp[i-1] + nums[i];
} else {
dp[i] = nums[i];
}
}
// dp数组中记录了所有的子序列的和,找出最大的即可
int res = Integer.MIN_VALUE;
for (int i = 0; i < dp.length; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
结合本题,可以进一步优化,具体如下:
public int maxSubArray(int[] nums) {
int res = nums[0];
int sum = nums[0];
for (int i = 1; i < nums.length; i++) {
if (sum >= 0) {
sum += nums[i];
} else {
sum = nums[i];
}
res = Math.max(res, sum);
}
return res;
}
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
方法三:分治法
类似于归并排序,先切分后合并
public int maxSubArray(int[] nums) {
return maxSubArrayDivideWithBorder(nums, 0, nums.length-1);
}
private int maxSubArrayDivideWithBorder(int[] nums, int start, int end) {
// 递归终止条件,只有一个元素的时候
if (start == end) return nums[start];
int mid = start + (end - start) / 2;
int leftMax = maxSubArrayDivideWithBorder(nums, start, mid);
int rightMax = maxSubArrayDivideWithBorder(nums, mid + 1, end);
// 下面计算横跨两个子序列的最大值
// 计算包含左侧子序列最后一个元素的子序列最大值
int leftCrossMax = Integer.MIN_VALUE;
int leftCrossSum = 0;
for (int i = mid; i >= start; --i) {
leftCrossSum += nums[i];
leftCrossMax = Math.max(leftCrossMax, leftCrossSum);
}
// 计算包含右侧子序列最后一个元素的子序列最大值
int rightCrossMax = nums[mid + 1];
int rightCrossSum = 0;
for (int i = mid + 1; i <= end; i++) {
rightCrossSum += nums[i];
rightCrossMax = Math.max(rightCrossMax, rightCrossSum);
}
// 计算跨中心的子序列的最大值
int crossMax = leftCrossMax + rightCrossMax;
return Math.max(crossMax, Math.max(leftMax, rightMax));
}
复杂度分析:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(logn)
参考:leetcode讨论区
实现 [pow(x, n)],即计算 x 的 n 次幂函数。
思路:
public double myPow(double x, int n) {
if (x == 0.0) return 0.0;
double res = 1.0;
boolean isP = true; // 判断是否为正数
if (n < 0) {
isP = false;
n = -n;
}
while (n != 0) {
if ((n&1) == 1) { // 等价于 n % 2 == 1 奇数
res *= x;
}
x *= x; // 等价于 x = x^2;
n /= 2; // 减半
}
return isP?res:1/res;
}
复杂度分析:
- 时间复杂度:O($logn$)
- 空间复杂度:O(1)
套路:
1) 拆分
对于数组元素,一般分解到不能拆分的 单个元素位置 直接return
2)解决子问题
int mid = start + (end-start)/2
int left = Recursion(start,mid)
int right = Recursion(mid+1,end)
3)合并
根据题意要求编写逻辑
(前两步都是固定的,最后一步需要根据题意灵活处理)
拓展
给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。
示例 1:
输入: "2-1-1"
输出: [0, 2]
解释:
((2-1)-1) = 0
(2-(1-1)) = 2
示例 2:
输入: "2*3-4*5"
输出: [-34, -14, -10, -10, 10]
解释:
(2*(3-(4*5))) = -34
((2*3)-(4*5)) = -14
((2*(3-4))*5) = -10
(2*((3-4)*5)) = -10
(((2*3)-4)*5) = 10
采用分治算法,套模板
比如:2*3-4*5
经过递归拆分成 2 3 4 5
之后再从4 * 5 依次往上推
1)拆分 去除运算符,拆解到只剩下单个元素为止
具体步骤:将输入的字符串中的运算符去除,转化为整数形式
2)解决子问题 // 通过运算符将字符串分为左右两部分 递归运算得到两个集合结果,接着运算子序列
3)合并 将得到的子序列运算结果存在result中,并在map中存一份,目的是为了避免重复运算子序列提高效率
具体实现:
HashMap<String,List<Integer>> map = new HashMap<>();
public List<Integer> diffWaysToCompute(String input) {
// 边界处理
if (input == null || input.length() < 1) return null;
// 1)拆分 除去运算符,拆解到只剩下一个单个元素为止
//如果已经有当前解了,直接返回
if(map.containsKey(input)){
return map.get(input);
}
// 将单个元素转换为整数形式
List<Integer> result = new ArrayList<>();
int num = 0;
int i;
for (i = 0; i < input.length() && !isOperation(input.charAt(i)); i++) {
num = num * 10 + input.charAt(i) - '0';
}
//将全数字的情况直接返回
if (i == input.length()){
result.add(num);
// 存到map
map.put(input, result);
return result;
}
// 2)解决子问题
for (int j = 0; j < input.length(); j++) {
// 通过运算符将字符串分为两部分
if (isOperation(input.charAt(j))) {
List<Integer> result1 = diffWaysToCompute(input.substring(0,j)); // 左闭右开区间
List<Integer> result2 = diffWaysToCompute(input.substring(j+1)); // 省略最右边界
// 将两个结果依次运算
for (int k = 0; k < result1.size(); k++) {
for (int l = 0; l < result2.size(); l++) {
char op = input.charAt(j);
result.add(calculate(result1.get(k), op, result2.get(l)));
}
}
}
}
//存到 map
map.put(input, result);
Collections.reverse(result); // 因为是递归,不加这句得到的答案逆序的,提交过不了
return result;
}
private int calculate(int num1, char c, int num2) {
switch (c) {
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
}
return -1;
}
private boolean isOperation(char c) {
return c == '+' || c == '-' || c == '*';
}
参考连接:讨论版