zoukankan      html  css  js  c++  java
  • JS Leetcode 525. 连续数组 前缀和加哈希表,小白式讲解让你彻底明白此题

    壹 ❀ 引

    题目来自LeetCode的525. 连续数组,难度中等,题目描述如下:

    给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。

    示例 1:

    输入: nums = [0,1]
    输出: 2
    说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
    

    示例 2:

    输入: nums = [0,1,0]
    输出: 2
    说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。
    

    提示:

    1 <= nums.length <= 105
    nums[i] 不是 0 就是 1

    贰 ❀ 题解分析

    根据题意,要求其实是给定我们一个数组,数组元素只包含0或者1,而我们需要找到0和1数量相等的最长子数组。比如例子1中,0和1各有1个,因此子数组长度就是2。而在例子二中,不管是[0,1]还是[1,0]都满足条件,但不管选谁,长度还是2,所以最终得到的结果还是2。

    有次我们可以推测,数组中可能会存在多段满足条件的子数组,因此求解时一定会用到Math.max用于找出满足条件的最大子数组长度。

    让我们将问题抽象化,题目要求是找到拥有包含相同数量1与0的子数组,如下数组起始都满足条件,且长度为4。

    [1,1,0,0]
    [1,0,1,0]
    

    我们在现实生活中,应该都遇到过这样类似的操作,我们在一个空的输入框打了2个数字,ctrl+z撤销2次,于是内容又回到了最初的样子。我们将一个魔方扭转了2次后,又回退刚才操作2次,于是魔方又变成了最初的样子,操作前与操作结束后,事物的状态相同

    对应到上述数组中,[1,1,0,0]可以理解为连续输入两次数字,紧接着又撤销了2次。数组[1,0,1,0]可以理解为先输入一个数字,紧接着撤销,又输入一个数字后又撤销,不管我们输入顺序如何,事物又回归到了原有的样子。

    那我们是不是可以这样理解,一开始有个数字0,遇到1我们就加个1,遇到0我们就减去1,像上述两种数组,你会发现最终结果都是0。我们可以将数组中的0都转为-1,将问题演变为元素和为0的最长子数组问题。

    说到求子数组元数和,不得不提前缀和的概念,还记得我在JS LeetCode 303. 区域和检索 - 数组不可变,一维数组的前缀和一文中,提到了前缀和概念,我们简单复习下。

    假设给定数组[1,1,1,1]以及两个下标j k j<k,现在要你求jk的所有数字的和,怎么做呢?我们当然可以根据这两个下标把对应的数字依次累加从而得到结果,这没问题,但如果我们要求很多个范围的和,这个数组也很大,每次都得从头开始累加就特别耗时了,有没有什么办法能从O(1)的时间复杂度直接得到结果,这就需要利用到前缀和。

    假设我们有一个前缀和数组preSum,它和原数组的对应关系如下,也就是说我们通过一次遍历,已经知道了从下标0到每个下标 i 的元素和

    那么现在,我们要知道下标1到下标3的元素和,扳指头都知道结果是3,但是站在presum的角度,其实我们可以知道是presum[3]-presum[0],从而我们可以得到一个结论,要求任意j到k的元素和,其结果为presum[k]-presum[j-1]

    还记得我们前面对于题目的转换吗?我们现在就是要找到一个子数组其元素和为0,那么由此可以推导为presum[k]-presum[j-1]=0,再次转换也就是presum[k] = presum[j-1]。意识到了什么没?当我们遍历一次数组,其实可以得到各种前缀和的情况,而现在我们就是要找到一个前缀和的数字出现2次的情况,只要一个和能出现两次,那么它们之间的区间的数字和一定等于0,也就是1和0出现的次数频率相等。为了方便理解,我们来看个最简单的例子:

    [1,0]
    //转变为
    [1,-1]
    

    我们假设前缀和初始值是0,且它的下标为-1,上面的数组对应关系是:

    如上图,这个数组的前缀和演变中,0出现了2次,那么符合条件的子数组其实就是下标-1到1之间的元素,长度是多少呢?其实就是第二个0的小标1减去上一个0的下标-1,也就是2。

    我们再来看个例子,比如数组[1,1,1,0],转为-1后其实就是[1,1,1,-1]。让我们看下计算过程:

    你会发现这个例子满足条件的数组长度是3-1=2,其实说到底,还记得文章开头我们所说的事物的状态吗?一个魔方一开始被打断,然后又通过回退还原成最初的样子,两个状态之间必然包含了相同的打乱次数和回退次数,那么我们只要找到相同的两个状态,自然就知道期间有多少满足条件的数字了。

    当然,可能会存在多对相同的状态,因此我们前面也说了,需要找出长度最长的满足条件的子数组。

    我们可以借用map,定义一个k为0,val为-1作为初始值,这里的k就是子数组的前缀和,而val是出现这个前缀和时当前所在的索引,让我们实现这段代码:

    /**
     * @param {number[]} nums
     * @return {number}
     */
    var findMaxLength = function (nums) {
        // 我们将数组中的0转为-1
        for (let i = 0; i < nums.length; i++) {
            if (nums[i] === 0) {
                nums[i] = -1;
            };
        };
        // 初始我们能拿到符合条件子数组的长度
        let maxLength = 0;
        // 用于求前缀和的初始值
        let sum = 0;
        let map = new Map();
        map.set(0, -1);
        for (let i = 0; i < nums.length; i++) {
            sum += nums[i];
            // 当前有这个前缀和的值了吗?
            if (map.has(sum)) {
                // 有了,那就计算他们之前的索引差,就是符合条件的子数组长度
                maxLength = Math.max(maxLength, i - map.get(sum));
            } else {
                // 没有,那就记录这个前缀和出现的索引
                map.set(sum, i);
            };
        };
        return maxLength;
    };
    

    当我们理解了状态的变化,其实我们完全没必要做把0变成-1的操作,遇到1自增,遇到0自减一个1其实也能达到同样的效果:

    /**
     * @param {number[]} nums
     * @return {number}
     */
    var findMaxLength = function (nums) {
        // 初始我们能拿到符合条件子数组的长度
        let maxLength = 0;
        // 用于求前缀和的初始值
        let sum = 0;
        let map = new Map();
        map.set(0, -1);
        for (let i = 0; i < nums.length; i++) {
            if (nums[i]) {
                sum += 1;
            } else {
                sum -= 1;
            };
            // 当前有这个前缀和的值了吗?
            if (map.has(sum)) {
                // 有了,那就计算他们之前的索引差,就是符合条件的子数组长度
                maxLength = Math.max(maxLength, i - map.get(sum));
            } else {
                // 没有,那就记录这个前缀和出现的索引
                map.set(sum, i);
            };
        };
        return maxLength;
    };
    

    OK,那么本题的分析就到这里了。

  • 相关阅读:
    Golang---反射(reflect)
    golang--交替打印一个数组中的元素
    Golang---基本类型(interface)
    利用random5 生成 random7
    Golang---基本类型(map)
    Golang---基本类型(slice)
    Golang---基本类型(string)
    二维码扫码登录原理
    Golang---内存逃逸
    关于我
  • 原文地址:https://www.cnblogs.com/echolun/p/14856819.html
Copyright © 2011-2022 走看看