分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。
前言
算法活动已经到了第十六期,大家都非常给力。这是一个培养习惯的过程,同样也是我们提高算法的过程。希望在未来的日子里,大家不要只是为了做题而学习,我们的目的是了解算法思想,培养潜意识。
上周回顾:
公众号同步「每天一道算法题(第六期)」

移除元素
给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}题解:
思路1:减针法
匹配到,通过splice来修改原数组,最后再让循环倒退一步。
执行用时:80ms;内存消耗:33.6MB;
var removeElement = function(nums, val) {
let len=nums.length
for(let i=0;i<len;i++){
if(nums[i]==val){
nums.splice(i,1);
i--
}
}
return nums.length
}
思路2:拷贝覆盖法
循环匹配值,记录指针,如果不同,则覆盖原值,指针加加。通过修改索引来覆盖原值。
执行用时:80ms;内存消耗:33.6MB;
var removeElement = function(nums, val) {
let ans = 0;
for(const num of nums) {
if(num != val) {
nums[ans] = num;
ans++;
}
}
return ans;
}
搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2示例 2:
输入: [1,3,5,6], 2
输出: 1示例 3:
输入: [1,3,5,6], 7
输出: 4示例 4:
输入: [1,3,5,6], 0
输出: 0题解:
思路1:条件索引法
根据题意,先判断数组中是否含有target,如果有返回索引,如果没有则寻找数组最大值,如果大于最大值,则索引为数组的长度;如果小于最大值再通过数组过滤出比target大的数组,返回首位的索引。
执行用时:80ms;内存消耗:33.9MB;
var searchInsert = function(nums, target) {
if(nums.includes(target)){
return nums.indexOf(target)
}else{
let maxVal=Math.max(...nums);
if(maxVal<target){
return nums.length
}
if(maxVal>target){
let list=nums.filter(r=>r>target);
return nums.indexOf(list[0])
}
}
}
思路2:循环判断法
相比上一个解法,少了很多复杂的逻辑,在循环中判断。
执行用时:80ms;内存消耗:34.3MB
var searchInsert = function(nums, target) {
for(let i=0;i<nums.length;i++){
if(nums[i]>=target){
return i
}
}
return nums.length
}
最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
题解:
思路1:动态规划
这道题用动态规划比较容易解决,设置一个基准值ans,再用一个值sum来记录当前值之前的最大子序列和,然后与基准值对比获取最大值。
每一次循环中都处理以上逻辑,最后ans为所求值。
执行用时:108ms;内存消耗:35.9MB
var maxSubArray = function(nums) {
let ans = nums[0];
let sum = 0;
for(const num of nums) {
if(sum > 0) {
sum += num;
} else {
sum = num;
}
ans = Math.max(ans, sum);
}
return ans;
}
思路2:分治算法
这道题是典型的分治算法;把一个大的问题分解成小的问题,这些问题有两个特点:
相互独立;
性质相同;
首先我们排除空数组的特例;
然后获取到一个最小的值,这是目前数组已知最小的子序列和;
把整个数组分成左边和右边两个小问题来处理,但是这不能完全满足我们的需求,我们还需要一个中间值(中间向两边延伸)。
相当于我们把数组分为3块,左边,中间、右边;最后获取三者的最优解。(与快排的区别是快排是典型的二分法,只需合并数组即可)。
我们通过索引来分治,每次递归都返回最优解;
maxSubArraySum为主函数来实现分治;
在每次递归中获取中间索引;
需要注意的是中间值的获取,中间向两边延伸,需要取到左边和右边的最优解,因为数组中有负数的存在,所以这个左边的解和右边的解需要限制条件,如果有一边的子序列和小于最小值,就取最小值,同时也意味着此中间值maxCrossingSum相比两边要小,不是最终结果。
执行用时:84ms;内存消耗:34.9MB
var maxSubArray = function(nums) {
if(nums.length==0)return 0;
let minNum=Math.min(...nums);
let maxCrossingSum=(l,m,r)=>{
let sum=0;
let leftSum=minNum;
for(let i=m;i>=l;i--){
sum+=nums[i];
if(sum>leftSum){
leftSum=sum
}
}
sum=0;
let rightSum=minNum;
for(let i=m+1;i<=r;i++){
sum+=nums[i];
if(sum>rightSum){
rightSum=sum
}
}
return leftSum + rightSum;
}
let maxSubArraySum=(l,r)=>{
if (l == r) {
return nums[l];
}
let m=(l + r) >>> 1;
let leftNum=maxSubArraySum(l, m);
let rightNum=maxSubArraySum(m+1, r);
let midNum=maxCrossingSum(l,m,r);
return Math.max(leftNum,rightNum,midNum)
}
let res=maxSubArraySum(0, nums.length - 1);
return res
}
求众数
给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在众数。
示例 1:
输入: [3,2,3]
输出: 3示例 2:
输入: [2,2,1,1,1,2,2]
输出: 2题解:
思路1:计数法
先理解题意,着重点是众数一定是大于n/2的元素,至于出现的次数,最小是1,所有我们只要判断两个条件同时满足就能找到众数;通过对象的方式,保存数组中每个元素出现的次数,然后同时满足条件的一定是所求值。
执行用时:80ms;内存消耗:37.2MB
var majorityElement = function(nums) {
let count=1;
let len=nums.length/2;
let obj={};
let res=1;
nums.forEach(item=>{
if(obj[item]){
count=++obj[item]
}else{
obj[item]=1
}
if(count>len&&obj[item]==count)res=item;
})
return res
}
思路2:排序法
众数是指在数组中出现次数大于n/2的元素,所以只要有众数,它的索引一定处于数组中心Math.floor。
执行用时:80ms;内存消耗:37.2MB
var majorityElement = function(nums) {
nums.sort()
return nums[Math.floor(nums.length/2)];
}
思路3:投票法
投票是比较经典的算法,如果我们把众数记为 +1 ,把其他数记为 -1,将它们全部加起来,显然和大于 0 ,从结果本身我们可以看出众数比其他数多
执行用时:88ms;内存消耗:36.8MB
var majorityElement = function(nums) {
let count=0;
let candidate=null;
for (let num of nums) {
if (count == 0) {
candidate = num;
}
count += (num == candidate) ? 1 : -1;
}
return candidate
}
Nim 游戏
你和你的朋友,两个人一起玩 Nim 游戏:桌子上有一堆石头,每次你们轮流拿掉 1 - 3 块石头。拿掉最后一块石头的人就是获胜者。你作为先手。
你们是聪明人,每一步都是最优解。编写一个函数,来判断你是否可以在给定石头数量的情况下赢得游戏。
示例:
输入: 4
输出: false
解释: 如果堆中有 4 块石头,那么你永远不会赢得比赛;
因为无论你拿走 1 块、2 块 还是 3 块石头,最后一块石头总是会被你的朋友拿走。
题解:
思路1:极小化极大
我们小时候也常玩的游戏,每次最大能拿掉3块石头,先手是我;如果堆中石头的数量 nn 不能被 44 整除,那么你总是可以赢得 Nim 游戏的胜利。
执行用时:76ms;内存消耗:33.7MB
var canWinNim = function(n) {
return !!(n % 4);
}
六期结束,希望有更多的小伙伴加入。

关注公众号回复「算法」。拉你进群。
前期回顾: