[英]Algorithm: array with odd and even numbers
给定一个具有n个元素的数组,我想计算数组的最大范围,其中奇数和偶数一样多。
输入:
2 4 1 6 3 0 8 10 4 1 1 4 5 3 6
预期产量:
12
我尝试使用以下步骤:
但这具有O(n ^ 2)的时间复杂度。
如何在O(n)中执行此操作?
给定 :数组a
任务 :找到具有偶数个奇偶数的最大子阵列
解 O(n)
在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); }
在Java中使用m查找最大距离,从而找到最大为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; } }
基于观察,您可以使用cumSum [y]-cumSum [x-1]获得x和y的和(在将元素转换为1和-1之后)。 因此,如果我们希望它为0,则它们必须相同(请注意,如果x = 0,则cumSum = 0,未定义cumSum [-1])。
这是一个常见的动态编程问题。 我们可以通过遍历列表并同时更新最佳解决方案来维护次优解决方案。 这类似于查找数组的最大元素。 将第一个元素设置为最大值,并在每次迭代中对其进行更新(如果需要)。 这个问题只需要一点。
我们需要5个指针(整数,实际上是5个)。 开始指针,结束指针和当前指针maxend,maxstart。 将start
和current
指针设置为数组的开始。 当下一个元素遵循规则时(交替使用奇数和偶数),请增加current
指针。 一旦他们不遵守规则,请将结束指针设置为当前指针。 比较end-start
指针的差,如果它大于maxend-maxstart,则更改maxend和maxstart并继续此操作。 最后,您可以打印maxstart和maxend之间的数组部分。
使用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。
时间复杂度为O(n),因为总和的计算显然是O(n) ,而其他两个循环沿一个方向移动数组的开始索引或结束索引,从此不再访问同一元素。
此代码使用最简单的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个条目),以上算法表现更好。 当然,可以修改基于散列的解决方案,以考虑到我上面确定的规则,然后对于较大的数组也将表现更好。 递归带来了哈希映射所没有的一些开销。
但是,由于您对注释感兴趣,希望看到没有哈希图的解决方案,因此我选择了该解决方案。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.