尝试优化代码以删除嵌套循环或使其更高效

时间:2019-05-04 22:01:31

标签: javascript algorithm optimization nested-loops

  • 我的一个朋友带一个从1到n(其中n> 0)的数字序列

  • 在此序列中,他选择了两个数字a和b

  • 他说a和b的乘积应等于序列中所有数字的和,不包括a和b

  • 给出一个数字n,你能告诉我他从序列中排除的数字吗?

已经从Code Wars找到了该Kata的解决方案,但是当我运行它时,它在编辑器中超时(12秒后);还有什么想法我应该如何进一步优化嵌套的for循环或删除它?

function removeNb(n) {
  var nArray = [];
  var sum = 0;
  var answersArray = [];
  for (let i = 1; i <= n; i++) {
    nArray.push(n - (n - i));
    sum += i;
  }
  var length = nArray.length;
  for (let i = Math.round(n / 2); i < length; i++) {
    for (let y = Math.round(n / 2); y < length; y++) {
      if (i != y) {
        if (i * y === sum - i - y) {
          answersArray.push([i, y]);
          break;
        }
      }
    }
  }
  return answersArray;
}

console.log(removeNb(102));
.as-console-wrapper { max-height: 100% !important; top: 0; }

5 个答案:

答案 0 :(得分:8)

我认为填充数组后没有必要计算总和,可以在填充数组时进行计算。

function removeNb(n) {
    let nArray = [];
    let sum = 0;
    for(let i = 1; i <= n; i++) {
        nArray.push(i);
        sum += i;
    }
}

并且由于公式a * b = sum - a - b的输入只能有两个数字a和b,因此每个数字只能有一个可能的值。因此,找到它们后无需继续循环。

if(i*y === sum - i - y) {
    answersArray.push([i,y]);
    break;
}

我建议以另一种方式看问题。

您正尝试使用此公式a * b = sum - a - b查找两个数字a和b。

为什么不这样简化公式:

a * b + a = sum - b

a ( b + 1 ) = sum - b

a = (sum - b) / ( b + 1 )

然后,您只需要一个for循环即可产生b的值,请检查(sum-b)是否可被(b + 1)整除,并且除法是否会产生小于n的数字。

for(let i = 1; i <= n; i++) {
    let eq1 = sum - i;
    let eq2 = i + 1;
    if (eq1 % eq2 === 0) {
        let a = eq1 / eq2;
        if (a < n && a != i) {
            return [[a, b], [b, a]];
        }
    }
}

答案 1 :(得分:6)

您可以使用two pointers method(本书第77页)在线性时间内解决此问题。

为了获得对解决方案的直觉,让我们开始考虑代码的这一部分:

for(let i = Math.round(n/2); i < length; i++) {
    for(let y = Math.round(n/2); y < length; y++) {
        ...

您已经发现这是您的代码中缓慢的部分。您正在尝试iy的每种组合,但是如果不必尝试每种组合怎么办?

让我们举一个小例子来说明为什么不必尝试每种组合。

假设n == 10,所以我们有1 2 3 4 5 6 7 8 9 10,其中sum = 55

假设我们尝试的第一个组合是1*10

接下来尝试1*9有意义吗?当然不是,因为我们知道1*10 < 55-10-1,所以我们知道我们必须增加产品,而不是减少产品。

因此,让我们尝试2*10。好吧,20 < 55-10-2,所以我们仍然需要增加。

3*10==30 < 55-3-10==42

4*10==40 < 55-4-10==41

然后5*10==50 > 55-5-10==40。现在我们知道我们必须减少产品。我们可以减少5或减少10,但是我们已经知道,减少5是没有解决方案的(因为在上一步中已经尝试过了)。因此,唯一的选择是减少10

5*9==45 > 55-5-9==41。同样,我们必须减少9

5*8==40 < 55-5-8==42。现在我们必须再次增加...


您可以将上面的示例视为具有2个指针,这些指针被初始化为序列的开头和结尾。在每一步我们都

  • 将左指针移向右
  • 或将右指针移向左

在开头,指针之间的区别是n-1。在每一步中,指针之间的差异减少一。我们可以在指针相互交叉时停止(并说,如果到目前为止还没有找到解决方案,那么将无法获得解决方案)。因此很明显,在得出解决方案之前,我们只能进行n个计算。这就是说关于n的解是 linear 的意思。无论n有多大,我们所做的计算都不会超过n。将此与您的原始解决方案进行对比,实际上,随着n^2的增长,我们最终进行了n计算。

答案 2 :(得分:0)

哈桑是正确的,这是一个完整的解决方案:

function removeNb (n) {
  var a = 1;
  var d = 1;

  // Calculate the sum of the numbers 1-n without anything removed
  var S = 0.5 * n * (2*a + (d *(n-1)));

  // For each possible value of b, calculate a if it exists.
  var results =  [];
  for (let numB = a; numB <= n; numB++) {
      let eq1 = S - numB;
      let eq2 = numB + 1;
      if (eq1 % eq2 === 0) {
          let numA = eq1 / eq2;
          if (numA < n && numA != numB) {
              results.push([numA, numB]);
              results.push([numB, numA]);
          }
      }
  }

  return results;
}

答案 3 :(得分:0)

这部分是评论,部分是答案。

用工程术语来说,最初发布的功能是使用“蛮力”解决问题,迭代每个(或超过所需的)可能的组合。迭代次数很大-如果您尽一切可能

n * (n-1) = bazillio n

少即是

因此,让我们看一下可以优化的事情,首先是一些小事情,我对第一个for循环和nArray感到有些困惑:

// OP's code
 for(let i = 1; i <= n; i++) {
    nArray.push(n - (n - i));
    sum += i;
  }

???您真的不使用nArray做任何事情吗? Length只是n ..我是否因此失去睡眠,所以想念某些东西?虽然可以使用1-n循环来求和连续的整数for序列,但是有一种避免循环的直接简便的方法:

sum = ( n + 1 ) * n * 0.5 ;

   // OP's loops, not optimized
for(let i = Math.round(n/2); i < length; i++) {
    for(let y = Math.round(n/2); y < length; y++) {
      if(i != y) {
        if(i*y === sum - i - y) {

优化注意事项:

我看到您在某种程度上处于正确的轨道上 ,将自i, y的起始值减少了一半。但是,您正在朝着相同的方向迭代它们:UP。而且,较低的数字看起来可以将其降低到n的一半以下(也许不是因为序列从1开始,我没有证实这一点,但确实如此)。

此外,我们希望在每次启动循环实例化时避免除法(即,将变量设置一次,并且还要对其进行更改)。最后,使用IF语句,i和y永远不会像我们将要创建循环的方式那样彼此相等,因此这是一个可能消失的条件。

但是更重要的是 遍历循环的方向 。较小的因子low可能接近最低的循环值(约为n的一半),较大的因子hi 大概将接近于。如果我们有一些扎实的数学理论说“ hi永远不会小于0.75n”,那么我们可以制作几个mod来利用这些知识。

下面显示循环的方式,它们在高和低循环相遇之前中断并迭代。

此外,哪个循环选择较低或较高的数字无关紧要,因此我们可以使用它来缩短测试数字对时的内部循环,从而使每次循环变小。我们不想浪费时间多次检查同一对数字!较低因子的循环将从n的一半以下开始并上升,而较高因子的循环将从n开始并下降。

// Code Fragment, more optimized:

let nHi = n;
let low = Math.trunc( n * 0.49 );
let sum = ( n + 1 ) * n * 0.5 ;

                    // While Loop for the outside (incrementing) loop
while( low < nHi ) {
                                       // FOR loop for the inside decrementing loop
    for(let hi = nHi; hi > low; hi--) {
                                        // If we're higher than the sum, we exit, decrement.
        if( hi * low + hi + low > sum ) { 
            continue; 
        }
                                               // If we're equal, then we're DONE and we write to array.
        else if( hi * low + hi + low === sum) {
            answersArray.push([hi, low]);
            low = nHi; // Note this is if we want to end once finding one pair
            break; // If you want to find ALL pairs for large numbers then replace these low = nHi; with low++;
        } 
               // And if not, we increment the low counter and restart the hi loop from the top.
        else {
            low++;
            break;
        }
    } // close for
} // close while

教程:

因此,我们设置了一些变量。请注意,low设置为略小于n的一半,因为更大的数字看起来可能会少几个点。此外,我们不舍入,而是截断,本质上是“总是舍入”,并且在性能上稍好一些(尽管在这种情况下仅分配一次就很重要)。

while循环从最低值开始并递增,可能一直到n-1。 hi FOR循环从n开始(复制到nHi),然后递减直到找到因子,或者它在低+ 1处截取。

条件: 第一个IF::如果我们高于总和,则退出,递减并继续以较低的hi因子值继续。 ELSE IF::如果我们相等,那就完成了,休息一下吃午饭。我们将low设置为nHi,这样当我们退出FOR循环时,我们还将退出WHILE循环。 ELSE:如果到达此处是因为我们小于总和,因此我们需要增加while循环并重置hi FOR循环以从n(nHi)重新开始。

答案 4 :(得分:0)

如果有兴趣,CY白羊座pointed this out

ab + a + b = n(n + 1)/2

在两侧加1

ab + a + b + 1 = (n^2 + n + 2) / 2
(a + 1)(b + 1) = (n^2 + n + 2) / 2

因此,我们正在寻找(n^2 + n + 2) / 2的因数,并有关于最小因数的迹象。这不一定意味着实际搜索的复杂性会大大提高,但这仍然很酷。