每天一道算法题(第六期)

349 阅读8分钟
分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。

前言

算法活动已经到了第十六期,大家都非常给力。这是一个培养习惯的过程,同样也是我们提高算法的过程。希望在未来的日子里,大家不要只是为了做题而学习,我们的目的是了解算法思想,培养潜意识。

上周回顾:

1、罗马数字转整数

2、最长公共前缀

3、有效的括号

4、合并两个有序链表

5、删除排序数组中的重复项

公众号同步「每天一道算法题(第六期)


移除元素

给定一个数组 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);
}

六期结束,希望有更多的小伙伴加入。

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

每天一道算法题(第四期)

每天一道算法题(第三期)

每天一道算法题(第二期)

每天一道算法题(第一期)