透过算法了解编程之1与-1

在开始之前,朋友们可以去力扣的今日每日一题挑战一下。

挑战完,别忘了回来继续支持我

接下来,本文将从 1 和 -1两个数字开始。

如果1代表某一个状态,那么-1则可以代表它的反向状态。
两者相加结果为 0,说明两者的价值抵消了。同时说明两者的价值是相等的。

假设,我们拥有一个长度为 n,且 n 为偶数的数组,数组中只有 1 和 -1 两种元素,且 1 与 -1 的数量相等,请问,此时的数组的所有元素之和是否等于 0 ?

例子1:[1,1,-1,-1,1,-1,-1,1]
例子2:[1,1,1,1,-1,-1,-1,-1]
例子3:[1,-1,-1,-1,-1,1,1,1]

以上3个例子的结果都为 0,说明当 1 和 -1 的数量相等时,无论总数量是多少都不会影响总和为 0 的定律,也可以得出结论:当数组中只有 1 和 -1两个状态,且两个状态的数量相等时,它们的状态之和必为 0; 或当两个状态的数量不再相等,若找到其中两个状态数量相等的子数组,则必须满足前面的那个条件(这个实际中怎么计算,后面会细讲)。

到这里,我们可以似乎初见端倪,那开端的每日一题,其实就是 1 和 -1 两种状态组成的数组的一个变体,只是变形后数组的元素不再是 1 和 -1 了,但通过抽象其实还是 1 和 -1 而已。

说到这里,我们讨论一下力扣的出题原则。

力扣不仅要为用户设计出有趣的每日一题,尽管有时候每日一题并不是很有趣,还要考虑到用户的刷题感受,以及是否对新老用户都那么友好。看似简单的要求,却很难做到极致,这有点像是销售,不同的客户,你要抛出不同的卖点,来让客户买单。我们每个人都喜欢新鲜的东西,也为了创造出更多的新鲜事物而煞费苦心,乃至生命。为了不让消费者觉察到这一点,生产者不得不与时间妥协,来制造一些表面的假象,就好比现在各大汽车品牌,几十年的前发动机披了一层符合现代人审美的外衣,算法题也是如此。这就是为什么说,孙悟空眼里妖怪都是一个模样!要想成为孙悟空,你需要火眼金睛和九九八十一难。

我们在回到刚才那个问题:当这种两种状态不规律的出现在一个数组当中,也就是 count(1) != count(-1) 时,我们应该怎么找出其中这两种状态相等的子数组呢?进一步地如何找到数量最大的满足要求的子数组呢?

我们同样举例子来求解

例子1: [1, -1, -1, 1, -1, 1, -1, -1, -1, 1]
例子2: [1, 1, -1, 1, 1, 1, -1, -1, 1, -1, -1]

如果简单看过去,似乎很难直接找出来。

例子1的长度为 10,分别有下标(index)[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],每个下标对应一个元素。

1 的数量为 4,那么 -1 的数量不用数就知道是 10 - 4 = 6。因此最大的满足条件的子数组数量不会超过 4 * 2 = 8。

那应该怎么计算呢?

回顾刚才的求和,如果该数组中必然存在满足要求的子数组,那么该数组中必然存在和为 0 的子数组。

由于,我们不知道满足要求的 i j,因此我们可以先暴力求解。暴力之前,我们需要维护一个 maxLen 变量。

对于数组中的每个下标,我们都将其看作为满足要求的 i,从 i 开始计算接下来的元素和,当元素和为 0 时,说明当前的下标 ji所组成的子数组必然是满足要求(1和-1的数量相等)的,此时我们更新 maxLen

代码如下:

    public int longestSubarray(int[] arr) {
        int n = arr.length, maxLen = 0;
        for (int i = 0; i < n; ++i) {
            int sum = 0;
            // 对于接下来的遍历长度小于当前答案时,已经没有必要
            if (maxLen >= n - i) break;
            for (int j = i; j < n; ++j) {
                sum += arr[j];
                if (sum == 0) maxLen = Math.max(maxLen, j - i + 1);
            }
        }
        return maxLen;
    }

暴力法,符合常人的思考方式,也易于理解。但对于长度很大的数组,就勉为其难,且必然会超时。

这时候我们就需要发挥过去的学习经验了。

暴力法的时间复杂度一般都是:O(N^2) 的,这里我们要寻找一个 O(N) 的解法。

可是怎么找?

既然求和可以找出答案,那么还有哪种求和可以帮助我们快速找到答案呢?

暴力法中,我们知道,无论是以哪个下标作为起点,都可能会将多余的元素计算在内,比如:

透过算法了解编程之1与-1_第1张图片

图片中的,红色交叉线部分就是多余计算的部分。对于,其它类型满足要求的子数组不是从下标 0 开始的,则红色交叉线部分也会出现在前半部分。

暴力法中,由于我们是假设从每个 i开始都存在满足要求的答案,挑出距离最长的即可。因此,这里存在两个问题:①多余计算;②重复计算。所谓重复计算就是每个下标或子数组可能要被计算 1 次或 1次以上。而子数组是数组中连续的子序列,对于子数组的和,它又是整个数组之和中一部分,说明这一部分可以通过子数组前后相邻的前缀和(不好意思,这里说漏了嘴,这是我们接下来要讨论的话题)值相差求出。这样一来似乎距离 O(N) 求出答案越来越近了。

前缀和:

这是这道题的另外一个知识点,也是需要解开此题的条件之一。前缀和很简单,就是新建一个与原数组大小相同的空数组,累加求和从原数组下标 0 开始到下标 (n - 1),空数组的每个下标是依次记录从 0 开始到 n - 1 的每个阶段的值。

示例1的前缀和如下:

[1, 0, -1, 0, -1, 0, -1, -2, -3, -2]

我们看到从下标 0 开始,存在 3 个为 0 的前缀和下标,意味着以 0 为起点,有 3 个子数组满足条件,那么最长的就是 0 ~ 5 了。非起点为 0 的答案还有:3 ~ 6。

现在我们来印证这些满足条件的子数组是否能通过前缀和计算出,以 3 ~ 6 为例。

下标 3 的当前前缀和为 0,下标为 6 的前缀和为 -1,相差为 -1。为何是 -1 呢?刚接触前缀和的同学可以思考一下。

之所以为 -1 是因为我们少计算了一个元素,用下标为 6 的前缀和减去下标为 3 的前缀和只能计算出 4 ~ 6 的子数组的和,而不是 3 ~ 6(这里的为什么,大家也可以思考一下,这里就不描述了)。所以我们要用下标 6 的前缀和减去下标 2 的前缀和 -1 - (-1) = 0。

那么是不是前缀和数组当中 i 和 j 相等,就满足条件呢?

此处列出满足 preSumArr[j] - preSumArr[i] 为 0 的下标对:

[1, 3], [3, 5], [2, 4], [4, 6], [7, 9], [1, 5], [2, 6] 等

经计算,这些下标对构成的子数组之和都为 0,在验证时,需要注意计算的是 [i + 1, j] 的子数组和。上面的下标对,是对前缀和数组的相差列举的。

好了,有了这些技巧,我们是不是可以大刀阔斧的写代码了!?

怎么写?

大致可以围绕一下几点:

  1. hash表记录前缀和每个值第一次出现的下标

  1. 当某个值第二次出现时,意味着与第一次出现的下标 +1 处所构成的子数组满足条件,此时更新最大距离和下标

代码如下:

    public String[] findLongestSubarray(String[] array) {
        Map counter = new HashMap<>();
        // 这里的前缀和我没有使用“数组”,而是一个 preSum 局部变量代替
        // 或者说这个数组就是 counter 哈希表,可以更快速的访问到我们需要的某个值
        int preSum = 0, x = 0, max = 0, n = array.length;
        counter.put(0, -1);
        for (int i = 0; i < n; ++i) {
            preSum += array[i].charAt(0) > 57 ? 1 : -1;
            if (counter.containsKey(preSum)) {
                int pre = counter.get(preSum);
                if (i - pre > max) {
                    max = i - pre;
                    x = pre + 1;
                }
            } else {
                counter.put(preSum, i);
            }
        }
        return Arrays.copyOfRange(array, x, x + max);
    }

最后在唠叨几句刷算法的好处吧。毕竟不经常写博客。

我在刷力扣之前,写个 API 可能都要借助 IDEA 开发工具提示。对于某些特殊的数据结构比如 PriorityQueue, Deque 等闻所未闻,甚至怎么用都不知道。现在如果题目需要,基本信手沾来。

而且 95% 的题目都是直接在力扣开发窗口写代码,不借用开发工具,也就是盲写代码。

可能有人会反驳说,算法在实际开发当中用不到。我想说,你刷算法使用的数据结构大部分都会在你的开发工作当中用到,刷算法是让你更熟悉它们和合理的利用他们,也可以说是提高你的开发效率。就好比你读书,并不是为了某个片段会在某一天你跟你女友聊天提到,而是你懂得储备,等着那一刻的到来。

好了,今天就说到这里吧。希望在接下来的道路上你可以改变技术,而不是技术改变你!

你可能感兴趣的:(算法,Java,算法,java,数据结构,开发语言)