算法:具有奇数和偶数的数组

时间:2016-12-21 18:22:07

标签: arrays algorithm time-complexity sub-array

给定一个带有 n 元素的数组,我想计算数组的最大范围,其中奇数和偶数一样多。

实施例

输入:

2 4 1 6 3 0 8 10 4 1 1 4 5 3 6

预期产出:

12

我尝试了什么

我尝试使用以下步骤:

  • 将所有奇数更改为1,将所有偶数更改为-1
  • 检查所有可能的子阵列,并为每个子阵列计算1和-1值的总和。
  • 取最大的这些子数组,总和为0

但是它的时间复杂度为 O(n ^ 2)

问题

如何在 O(n)

中执行此操作

3 个答案:

答案 0 :(得分:3)

给定:数组a

任务:找到具有偶数奇数和偶数量的最大子阵列

解决方案 O(n)

  1. 在java中用-1替换奇数和偶数1时,为每个累积和创建一个哈希映射m:

    Map<Integer, Integer> m = new HashMap<>();
    int sum = 0;
    for (int i = 0; i < a.length; i++) {
      sum += a[i] % 2 == 0 ? 1 : -1;
      m.put(sum, i);
    }
    
  2. 通过在java中查找最大距离来找到最大为0的最大子数组:

    int bestStart = -1, bestEnd = -1; // indexes, so end inclusive
    sum = 0;
    for (int i = 0; i < a.length; i++) {
      Integer end = m.get(sum);
      sum += a[i] % 2 == 0 ? 1 : -1;
      if (end != null && end - i > bestEnd - bestStart) {
        bestStart = i;
        bestEnd = end;
      }
    }
    
  3. 这是基于观察,你可以得到总和(在将元素转换为1和-1之后)从x到y与cumSum [y] - cumSum [x - 1]。因此,如果我们希望它为0,那么它们必须是相同的(如果x = 0则小心,然后cumSum = 0,未定义cumSum [-1])。

答案 1 :(得分:1)

这是一个常见的动态编程问题。我们可以通过遍历列表并同时更新最佳解决方案来维护次优解决方案。它类似于查找数组的最大元素。将第一个元素设置为最大值,并在需要时在每次迭代中更新它。这个问题需要更多。

我们需要5个指针(整数,实际上是5个)。启动指针,结束指针和当前指针,maxend,maxstart。将startcurrent指针设置为数组的开头。当接下来的元素遵守规则(交替奇数和偶数)时,增加current指针。一旦他们不遵守规则,将结束指针设置为当前指针。比较end-start指针的差异,如果它大于maxend-maxstart,则更改maxend和maxstart并继续此操作。 最后,您可以在maxstart和maxend之间打印数组部分。

答案 2 :(得分:1)

使用1表示奇数,-1表示偶数的方法确实是正确的。让我们将这些值称为增量(包括减量)。

通过示例输入,您可以按如下方式显示这些增量:

  2   5   6   0   8   3   4   5   2   7   4   8   2   6   6   5   7 
 -1   1  -1  -1  -1   1  -1   1  -1   1  -1  -1  -1  -1  -1   1   1   

然后,您可以看到它下面的增量的累积总和:

  2   5   6   0   8   3   4   5   2   7   4   8   2   6   6   5   7 
 -1   1  -1  -1  -1   1  -1   1  -1   1  -1  -1  -1  -1  -1   1   1   
0  -1   0  -1  -2  -3  -2  -3  -2  -3  -2  -3  -4  -5  -6  -7  -6  -5

 \     / \
   \ /     \
             \
               \ 
                 \     / \     / \     / \   
                   \ /     \ /     \ /     \ 
                                             \
                                               \
                                                 \
                                                   \
                                                     \             /
                                                       \         /
                                                         \     /
                                                           \ /

请注意,具有相同数量的偶数和奇数的范围对应于增量总和为0的范围。您可以通过选择开始/结束点来识别此类范围,以便在开始之前的累积总和是等于该范围结束后的累计和。这就像在上面的“图形”中绘制一条水平线并采用最远的交叉点。

因此,例如,总和中第一次出现的两个-1值表示具有[5, 6]的子数组是有效范围(其中存在相等数量的奇数和偶数)。寻找其他这样的范围,我们可以发现取左和右-3会产生更大的结果:[3 4 5 2 7 4]。我们也可以将-2作为边界值:[8 3 4 5 2 7]

我们还可以看到,最长范围必须与0结束末尾和(在示例中为-5)之间的总和相对应。例如,不在此范围内的一个:示例中的-6。由于0和-5都在-6的同一侧,我们确信我们可以用-5获得更好的结果(在图中移动水平线)。对于范围为0和最终总和的所有中间和值都是如此。

可以得出的另一个结论是,总是有可能找到一个最佳解决方案,其中左端点与方向变化协调。在这个例子中,这是第一个点为-3的情况。

算法

你可以制作一个递归算法,只要找到符合上述规则的点就会递归:

  • 添加起点值之前的总和在0和最终总和
  • 之内
  • 起始位置的增量与之前的增量不同(或之前没有),并且从开始到结束都与整个方向相反。这对应于上图中的一个山谷,但当最终总和为正时,它将是一个山顶。

当到达总和时,递归停止,等于最终总和。这会立即产生尺寸。此大小返回给调用者(在递归树中向上一级),并且数组的末尾被缩短,直到最终总和等于在递归的该级别处查看的总和。这再次导致尺寸。这两种尺寸中最好的一种返回给调用者,等等。

算法中没有创建数组,但递归生成的callstack除外。但是如果数组完全是随机的,那么递归调用的数量平均应该很小,因为总和在统计上的预期值为0。

时间复杂度是 O(n),因为总和的计算显然是 O(n),而其他两个循环要么移动开始,要么数组在一个方向上的结束索引,再也不会访问同一个元素。

JavaScript实现

此代码使用最简单的JavaScript语法,因此算法很明确:

function value(x) {
    // Return 1 when the given value is odd, else -1
    return (x % 2) || -1;
}

function largestRange(a) {
    var sign, sumEnd, end;

    // Calculate final sum
    sumEnd = 0;
    for (end = 0; end < a.length; end++) {
        sumEnd = sumEnd + value(a[end]);
    }
    // ... and its sign (1 or -1 or 0)
    sign = Math.sign(sumEnd);

    function recurse(start, sumStart) {
        var sum, i, size, val;
        // End of recursion:
        if (sumStart === sumEnd) return end - start;
        sum = sumStart
        for (i = start; sum !== sumEnd; i++) {
            val = value(a[i]);
            // Got closer to sumEnd, and now moving away from it
            if (val !== sign && Math.sign(sum - sumStart) == sign) break;
            sum = sum + val;
        }
        // Get longest range size for this particular sum
        size = recurse(i, sum);
        // Get range size for sumStart
        while (sumEnd !== sumStart) {
            end--;
            sumEnd = sumEnd - value(a[end]);
        }
        // Retain the best of both:
        if (end - start > size) size = end - start;
        return size;
    }
    // Initiate the recursion and return result
    return recurse(0, 0);
}

// Sample input
var a = [2, 5, 6, 0, 8, 3, 4, 5, 2, 7, 4, 8, 6, 6, 5, 7];
// Calculate
var size = largestRange(a); 
// Output size of longest range
console.log(size);

性能

由于随机数组的预期总和预计接近0,因此大多数中间和值可能超出0和最终总和的范围,这意味着它们不会占用太多时间和空间。

我做了一些性能测试,将它与为每个遇到的总和创建带有密钥的哈希的解决方案进行比较。对于较短的输入数组,这些算法更快,但对于较大的数组(如1000个条目),上述表现更好。当然,可以调整基于散列的解决方案以考虑我上面确定的规则,然后它对于更大的阵列也会表现得更好。递归带来了哈希映射没有的一点开销。

但是当您对评论感兴趣以查看没有哈希映射的解决方案时,我选择了此解决方案。