分治法简单理解就是分而治之,将一个复杂的问题通过一定的方式分解成若干个类似的小问题。其实,从字里行间便能体会到递归的含义。没错,本质上来说,我们还是通过分治法求解去体会递归的魅力。至少接下来的三道题,我是这样做的~~
前排提醒,一开始遇到递归的问题,私以为不要过于追求细节,这样很容易迷失在递归过程中,造成自我怀疑。有一定基础的可以自己画棵树体会过程,或者直接翻题解找到类似的图也可。重要的是体会思想,剩下的就是重复练习。
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
暴力法比较esay,不再赘述,双循环即可。
题目要求我们求连续数组的最大和,并尝试采用分治法。为了便于分解,我们可以递归地从数组中间将其分成左右两子区间。我们现在就将目光集中到如何求当前区间的最大和?三种情况奉上~
- 左区间最大和
- 右区间最大和
- 跨越中点的区间最大和
注意:这里第三种情况是容易被忽略的,当然也是比较难实现的一点,不妨用二叉树的递归模型来理解这一过程。
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
这是后序遍历的递归套路,简单理解就是左右根,我们可以将重点放在对根节点的操作上,而左右交给递归过程。我们将其类比成本题的分治法:左右递归子树就是求左右区间的最大连续和,而根节点操作就是求中间衔接段的最大和并比较三者最大和。
还有一点,当我们发现题目所给的函数的参数似乎不够用时,我们可以添加辅助函数helper
。比如本题,显然我们在递归过程需要区间的左右端点,而maxSubArray
的参数只有一个容器nums
,这时候我们可以添加辅助函数helper(vector
即可。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if(nums.size() == 0) //判空
return 0;
return helper(nums, 0, nums.size() - 1);
}
private:
int helper(vector<int>& nums, int l, int r){
if(l > r) //递归出口 -> 左端点大于右端点
return INT_MIN;
int mid = (l + r) >> 1; //位运算 -> 求数组中点
int left = helper(nums, l, mid - 1); //左区间最大和
int right = helper(nums, mid + 1, r); //右区间最大和
//中间衔接段最大和(越过中点)
int leftMax = 0, rightMax = 0, sum = 0; //由中点向左发散最大值,由中点向右发散最大值,当前区间和
for(int i = mid - 1; i >= l; i--){ //求由中点向左发散最大值
sum += nums[i];
leftMax = max(leftMax, sum);
}
sum = 0; //重新置0
for(int i = mid + 1; i <= r; i++){ //求由中点向右发散最大值
sum += nums[i];
rightMax = max(rightMax, sum);
}
//左区间最大和,右区间最大和,中间衔接段最大和,三者取最大值
return max(max(left, right), leftMax + rightMax + nums[mid]);
}
};
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入: [3,2,3]
输出: 3
示例 2:
输入: [2,2,1,1,1,2,2]
输出: 2
暴力解千愁,看见题目要求收集数组元素出现次数就应当想到空间换时间,即采用哈希表unordered_map
。
class Solution {
public:
int majorityElement(vector<int>& nums) {
if(nums.size() == 0)
return 0;
unordered_map<int,int> record;
for(auto num : nums)
record[num]++;
int len = nums.size() / 2;
int result = 0;
for(auto rec : record){
if(rec.second > len){
result = rec.first;
break;
}
}
return result;
}
};
这次用分治法的思想非常巧妙,只机械地记忆模版(取数组中间位置)可能很难想到这样做的理由。那么为什么还是要去中间呢?本题要求我们求多数元素——出现次数大于⌊ n/2 ⌋
的数。这个前提条件很重要,没有了它,后面的分治法将无从下手。
好了,我们是否可以这样想,如果我们将数组一分为二,那么这个多数元素
至少是一个部分的多数元素
。具体的反证法见leetcode官方题解,私以为这更想是一个脑筋急转弯~所以现在知道为什么⌊ n/2 ⌋
是如此的重要吧!因为没有这个“超过半数”,我们就无法反证刚才的猜想的正确性,没有这个猜想,我们就无法“堂而皇之”的取中间位置一分为二。
剩下的工作就又回到了类似于二叉树后序递归模型中来,当然这次我们需要添加一个“工具人”函数count
,让它帮我们计算每一区间内的多数元素
。这样我们就可以将目光集中到对“根节点”的操作,也就是比较两个区间的多数元素
。如果两区间多数元素
相同,那么return
这个多数元素;如果两区间多数元素
不相同,那么需要比较这两个多数元素
在合并的整个区间里出现的次数来决定return
哪个值。
class Solution {
public:
int majorityElement(vector<int>& nums) {
if(nums.size() == 0)
return 0;
return helper(nums, 0, nums.size() - 1);
}
private:
int helper(vector<int>& nums, int l, int r){
if(l == r) //递归出口,区间内仅有一个元素
return nums[l];
int mid = (r - l) / 2 + l;
int leftMax = helper(nums, l, mid); //左区间多数元素
int rightMax = helper(nums, mid + 1, r); //右区间多数元素
if(leftMax == rightMax) //左右区间多数元素相同
return leftMax;
//左右区间多数元素不同,比较在合并区间的次数
int leftCount = count(nums, leftMax);
int rightCount = count(nums, rightMax);
return leftCount > rightCount ? leftMax : rightMax;
}
int count(vector<int>& nums, int target){
int c = 0;
for(auto num : nums)
if(num == target)
c++;
return c;
}
};
注:这个c++版本会在倒数第二个测试用例栽跟头,不过思路是正确的。
经过排查发现,count
函数还是需要加上左右区间边界的,显然想利用c++ STL
的偷懒想法破灭啦~如果没有左右边界,每次执行count
函数都要对整个数组进行循环,这个时间复杂度肯定是无法接受的~
class Solution {
public:
int majorityElement(vector<int>& nums) {
if(nums.size() == 0)
return 0;
return helper(nums, 0, nums.size() - 1);
}
private:
int helper(vector<int>& nums, int l, int r){
if(l == r) //递归出口,区间内仅有一个元素
return nums[l];
int mid = (r - l) / 2 + l;
int leftMax = helper(nums, l, mid); //左区间多数元素
int rightMax = helper(nums, mid + 1, r); //右区间多数元素
if(leftMax == rightMax) //左右区间多数元素相同
return leftMax;
//左右区间多数元素不同,比较在合并区间的次数
int leftCount = count(nums, leftMax, l, r);
int rightCount = count(nums, rightMax, l, r);
return leftCount > rightCount ? leftMax : rightMax;
}
int count(vector<int>& nums, int target, int l, int r){
int c = 0;
for(int i = l; i <= r; i++)
if(nums[i] == target)
c++;
return c;
}
};
实现 pow(x, n) ,即计算 x 的 n 次幂函数。
示例 1:
输入: 2.00000, 10
输出: 1024.00000
示例 2:
输入: 2.10000, 3
输出: 9.26100
示例 3:
输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25
说明:
依旧是分治,依旧是类似于二叉树的递归模型。不过,这次加上了实际应用背景(数学相关),所以要求我们将问题用分治思想抽象成递归模型。有了小数点,一切看起来都是这么凌乱~这里有三点细节需要注意:
n
的奇偶性x
的正负性return
的类型 先给出两个计算过程:
x 64 − > x 32 − > x 16 − > x 8 − > x 4 − > x 2 − > x 1 x^{64}->x^{32}->x^{16}->x^{8}->x^{4}->x^{2}->x^{1} x64−>x32−>x16−>x8−>x4−>x2−>x1
x 77 − > x 38 − > x 19 − > x 9 − > x 4 − > x 2 − > x 1 x^{77}->x^{38}->x^{19}->x^{9}->x^{4}->x^{2}->x^{1} x77−>x38−>x19−>x9−>x4−>x2−>x1
我们发现,这其实和之前两道题的分治法并无太大区别。依旧是⌊n/2⌋
,依旧是将计算交给递归(其实更想树的递归)。我们要将目光集中在当前节点也就是计算这一轮结果的操作上。
以 x 64 < − x 32 x^{64} <- x^{32} x64<−x32为例,此时N == 64
,如何从 x 32 x^{32} x32到 x 64 x^{64} x64呢?不妨假设我们已经递归计算出上一轮结果 x 32 x^{32} x32,也就是double y = helper(x, N / 2);
的意思。这时候,我们只需要判断N
的奇偶性即可。由于是偶数,所以我们只需要y*y
即可得出这一轮结果。
再以 x 77 < − x 38 x^{77}<-x^{38} x77<−x38为例,此时N == 77
,如何从 x 77 x^{77} x77到 x 38 x^{38} x38呢?照猫画虎,已经递归计算出上一轮结果 x 38 x^{38} x38。显然N
为奇数,如果y*y
,相当于只计算出 x 76 x^{76} x76的结果,这是不够的。此时,我们只需要再乘一个x
即可,即y*y*x
。注意:x
这个参数始终不变。
class Solution {
public:
double myPow(double x, int n) {
long long N = n; //防止n越界
return N >= 0 ? helper(x, N) : 1.0 / helper(x, -N); //幂次为负
}
private:
double helper(double x, long long N){
if(N == 0)
return 1.0; //递归出口,x^0 = 1
double y = helper(x, N / 2); //上一轮结果
return N % 2 == 0 ? y * y : y * y * x; //奇数,多乘一个x
}
};
其实,理解分治算法容易,灵活运用分治算法并不容易。通过这三道题,我发现本质上都是在围绕“分隔区间再合并区间”做文章,当然,目前都是以中点为分隔点。在代码构成上,类似于二叉树的遍历递归模型。实际上,我认为可以先从二叉树相关题目入手,理解递归。弄懂了递归的奥妙,再取碰这些分治法会更轻松一些。
另外,最基础的二分查找也是分治法的一种。从它入手,也可以更好地理解分治思想。以下是我写的一个简单的二分查找方法~
int binarySearch(T arr[], int n, T target){
int l = 0, r = n; //左右边界,在[l...r)的范围里寻找target
while(l < r){ //当l==r时,区间[l...r)无效
//int mid = (l+r) / 2;
int mid = l + (r - l)/2; //l,r均为int型,当l,r均很大时,l+r可能会产生整型溢出问题。c++不报错
if(arr[mid] == target)
return target;
if(target > arr[mid])
l = mid + 1; //target在[mid+1...r)中
else //target
r = mid; //target在[l...mid)中
}
return -1;
}
以上是我对于分治算法的初步认识~
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。
本人blog:http://breadhunter.gitee.io