【问题标题】:find cube root using limited math operators使用有限的数学运算符求立方根
【发布时间】:2019-05-21 13:33:40
【问题描述】:

我需要编写一个函数来告诉我一个数字是否是一个完美的立方体。如果是我希望立方根返回 else false 如下:

cubeRoot(1)   // 1
cubeRoot(8)   // 2
cubeRoot(9)   // false
cubeRoot(27)  // 3
cubeRoot(28)  // false

它必须适用于非常大的数字。性能是一个巨大的奖励。

但是,我使用的库意味着我只能使用以下数学函数/运算符:

abs, round, sqrt
/ * + -
===
> >= < <=
%
^

如果仅使用上述运算符在 JS 中提供答案,我可以自己将答案转换为 (big.js) 语法(我正在使用的库)。这可能吗?

PS 我需要使用big.js,因为它保证了精度。

【问题讨论】:

  • 发布的问题似乎根本没有包含any attempt 来解决问题。 StackOverflow 期待您 try to solve your own problem first,因为您的尝试有助于我们更好地了解您想要什么。请编辑问题以显示您尝试过的内容,以说明您遇到minimal reproducible example 的特定障碍。欲了解更多信息,请参阅How to Ask 并拨打tour
  • 以您的声誉,您肯定知道这是一个非常广泛的问题。
  • 您可以创建一个包含完美立方体数列表的数组并在其上循环吗?
  • Mathematics 可能是这个问题的更好地方。一旦你知道了数学公式,将它翻译成 JavaScript 应该是微不足道的。

标签: javascript math big.js


【解决方案1】:

为避免立方根头痛,您可以使用 big.js 的一个亲戚,称为 decimal.js-light(单独使用或与 big.js 一起使用)

big.js 不支持分数幂,但decimal.js-light 支持,因此您可以获得立方根,如下所示:

const Big = require('big.js')
const Decimal = require('decimal.js-light')

const nthRoot = (bigNumber, intRoot) => {
  const strBigNumber = bigNumber.toFixed()
  const decimal = Decimal(strBigNumber)
  const root = decimal.pow(1 / intRoot)
  return Big(root.toFixed())
}

module.exports = nthRoot

并按如下方式使用:

nthRoot(Big(8), 3)          // 1.9999999999999998613
nthRoot(Big(8), 3).round()  // 2

【讨论】:

    【解决方案2】:

    您可以使用 JS 内置 BigInt。我假设输入是正整数。对于while 循环,我提供时间复杂度近似值,其中 n 是输入十进制位数。这个版本的答案受到Salix alba answerwiki "cube root" 的启发:

    1. Binary search O(n log2(10)/3) O(1.11*n) (对于 n=1000 我得到 1110 次迭代 - 测试 @ 987654325@, - 对于l=0r=a O(10/3 n))

      function cubicRoot(a) 
      { 
        let d = Math.floor((a.toString(2).length-1)/3); // binary digits nuber / 3
        let r = 2n ** BigInt(d+1); // right boundary approximation
        let l = 2n ** BigInt(d);   // left boundary approximation
        let x=BigInt(l); 
        let o=BigInt(0);           // old historical value
        
        while(1) {
          o = x;
          y = x * x * x;      
          y<a ? l=x : r=x;      
          if(y==a) return x;
          x = l + (r - l)/2n;      
          if(o==x) return false;
        }
      }
      
      // TEST
      
      let t = "98765432109876543210987654321098765432109876543210987";
      let B = BigInt(t) * BigInt(t) * BigInt(t);
      
      console.log('cRoot(B):   ', cubicRoot( B )     .toString());
      console.log('cRoot(B+1): ', cubicRoot( B +1n ) .toString());
      console.log('cRoot(B-1): ', cubicRoot( B -1n ) .toString());
      console.log('B=',B.toString().split('').map((x,i)=>(i%60?'':'\n')+x).join('')); // split long number to multiline string
    2. Newton-Raphson 方案 O(log(9n))(对于 ntest)。我对“停止”条件有疑问 - 对于数字 a=b*b*b - 1 我需要检查 x 的两个历史值(如果它们至少发生一次然后停止) - 但我不知道在某些情况下我们应该需要检查树或更多历史值来停止算法。

      function cubicRoot(a) 
      { 
        let d = Math.floor((a.toString(2).length-1)/3); // binary digits nuber / 3
        let x = 2n ** BigInt(d);    
        let o=BigInt(0); // last history value
        let u=BigInt(0); // pre-last history value
        let i=0; // loop counter for worst scenario stop condition
        
        while(i<d*4) {
          i++;
          u = o;
          o = x;     
          y = x*x*x;            
          if(y===a) return x;
          x = ( a / (x*x) + 2n* x ) / 3n;
          if(o==x || u==x) return false; 
        }
        
        return false; // worst scenario - if for some case algorithm not finish after d*4 iterations
      }
      
      // TEST
      
      let t = "98765432109876543210987654321098765432109876543210987";
      let B = BigInt(t) * BigInt(t) * BigInt(t);
      
      console.log('cRoot(B):   ', cubicRoot( B )     .toString());
      console.log('cRoot(B+1): ', cubicRoot( B +1n ) .toString());
      console.log('cRoot(B-1): ', cubicRoot( B -1n ) .toString());
      console.log('B=',B.toString().split('').map((x,i)=>(i%60?'':'\n')+x).join('')); // split long number to multiline string
    3. Halley method O(log(3n))(对于经过测试的 ntest)

      function cubicRoot(a) 
      { 
        let d = Math.floor((a.toString(2).length-1)/3); // binary digits nuber / 3
        let x = 2n ** BigInt(d);    
        let o=BigInt(0); // last history value
        let i=0; // loop counter for worst scenario stop condition
        
        while(i<d) {
          i++;
          o = x;     
          y = x*x*x;            
          if(y==a) return x;
          x = 1n + x*(y + 2n*a)/(2n*y + a);
          if(o==x) return false; 
        }
        
        return false; // worst scenario (??)
      }
      
      // TEST
      
      let t = "98765432109876543210987654321098765432109876543210987";
      let B = BigInt(t) * BigInt(t) * BigInt(t);
      
      console.log('cRoot(B):   ', cubicRoot( B )     .toString());
      console.log('cRoot(B+1): ', cubicRoot( B +1n ) .toString());
      console.log('cRoot(B-1): ', cubicRoot( B -1n ) .toString());
      console.log('B=',B.toString().split('').map((x,i)=>(i%60?'':'\n')+x).join('')); // split long number to multiline string

    【讨论】:

    • 另一个非常好的答案会被接受,但其他人为我将逻辑转换为 Big。但是,这值得非常感谢,如果可以的话,我会支持更多
    【解决方案3】:

    让我们看看一些二进制的立方体

    2^3 = 8 = 100b (3 binary digits)
    4^3 = 64 = 100 000b  (6 binary digits)
    8^3 = 512 = 100 000 000b (9 binary digits)
    (2^n)^3 = 2^(3n) = (3n binary digits).
    

    因此,粗略地计算一下二进制数字的数量,比如d,然后除以三,让n = d/3 这告诉我们立方根数是否在2^n2^(n+1) 之间。计数数字可以链接到对数的原始第一近似值。

    如果您无法访问二进制数字,只需重复除以 8(或 8 的幂),直到得到零结果。

    现在我们可以使用 Newton-Raphson 来确定解决方案。 Wikipedia cube root 为我们提供了迭代公式。如果a 是我们想要找到根的数字,x_0 是我们使用上述方法的第一个猜测

    x_{n+1} = ( a / x_n^2 + 2 x_n ) / 3.
    

    这可以很快收敛。例如a=12345678901234567890,我们发现a 介于 8^21 和 8^22 之间,因此立方根必须介于 2^21 和 2^22 之间。

    运行迭代

    x_1 = 2333795, x_1^3 = 12711245751310434875 
    x_2 = 2311422, x_2^3 = 12349168818517523448
    x_3 = 2311204, x_3^3 = 12345675040784217664
    x_4 = 2311204, x_4^3 = 12345675040784217664
    

    我们看到它在 3 次迭代后已经收敛。检查显示a 介于 2311204^3 和 2311205^3 之间。

    此算法可以使用 big.js 进行计算。上面的计算是使用 Java 的 BigInt 类完成的。

    【讨论】:

    • 非常感谢您花时间整理这些内容。由于他为我转换为 Big.js,因此接受了其他人的回答,但这真的很有用,非常感谢您抽出宝贵的时间
    【解决方案4】:

    这是与 Kamil Kiełczewski 代码相同想法的另一个版本,但采用了 big.js API 并依赖于其实现细节。

    function isZero(v) {
        let digits = v.c;
        return digits.length === 1 && digits[0] === 0;
    }
    
    function isInteger(v) {
        if (isZero(v))
            return true;
        return v.c.length <= v.e + 1;
    }
    
    function neg(v) {
        return new Big(0).minus(v);
    }
    
    
    function cubeRoot(v) {
        const ZERO = Big(0);
        const TEN = new Big(10);
    
        let c0 = v.cmp(ZERO);
        if (c0 === 0)
            return ZERO;
        if (c0 < 0) {
            let abs3 = cubeRoot(v.abs());
            if (abs3 instanceof Big)
                return neg(abs3);
            else
                return abs3;
        }
    
        if (!isInteger(v))
            return false;
    
        // use 10 because it should be fast given the way the value is stored inside Big
        let left = TEN.pow(Math.floor(v.e / 3));
        if (left.pow(3).eq(v))
            return left;
    
        let right = left.times(TEN);
    
        while (true) {
            let middle = left.plus(right).div(2);
            if (!isInteger(middle)) {
                middle = middle.round(0, 0); // round down
            }
            if (middle.eq(left))
                return false;
            let m3 = middle.pow(3);
            let cmp = m3.cmp(v);
            if (cmp === 0)
                return middle;
            if (cmp < 0)
                left = middle;
            else
                right = middle;
        }
    }
    

    此代码背后的主要思想是使用二分搜索,但搜索开始时对 leftright 的估计比在 Kamil 的代码中要好一些。特别是,它依赖于 Big 以标准化指数表示法存储值的事实:作为十进制数字和指数的数组。所以我们可以很容易地找到这样的n10^n &lt;= cubeRoot(value) &lt; 10^(n+1)。这个技巧应该减少循环的一些迭代。可能使用Newton-Raphson iteration 而不是简单的二分搜索可能会更快一些,但我认为在实践中您看不出区别。

    【讨论】:

    • 很好的答案,非常感谢您将这些放在一起 - 我擅长编程,而不是数学!这有助于加载,甚至为我转换为 Big!如果可以的话,我会投票更多
    【解决方案5】:

    据我所知,Javascript 中的指数只能通过 Math.pow 的数学库访问。

    使用指数,x 的三次根可以通过cubeRoot(x) = x^(1/3) 计算。在使用 Math 的 javascript 中,这看起来像 var cubeRoot = Math.pow(x, 1/3)

    由于如果结果是小数,您的函数必须返回 false,我将使用 Math.round 来比较三次根。你的函数应该是这样的:

    function cubeRoot(x) {
        var root = Math.pow(x, 1/3);
        if (Math.round(root) !== root) {
            return false;
        }
        return root;
    }
    

    然而,由于1/3 实际上是0.33333... 具有一定的浮动精度,这不适用于大立方体。例如,Math.pow(45629414826904, 1/3) 可能会返回类似35733.99999999998 的内容。

    然后我会做的是,如果与舍入结果的差异非常小(比如小于1/1000000),请重新计算数字以查看这是否能让您恢复原来的x

    function cubeRoot(x) {
        var root = Math.pow(x, 1/3);
        var roundedRoot = Math.round(root);
        var diff = Math.abs(root - roundedRoot);
    
        if (diff <= 1/1000000) {
            var reCubed = Math.pow(roundedRoot, 3);
            if (reCubed === x) {
               return roundedRoot;
            }
            return false;
        }
        if (diff !== roundedRoot) {
            return false;
        }
        return root;
    }
    

    我在本地 Nodejs 上进行了一些测试,似乎它可以处理像 8000120000600001(或 200001^3)一样大的多维数据集,然后在某些非多维数据集上无法返回 false。尚未对其进行广泛测试,但鉴于您的问题的局限性,这是我能想到的最好的 hack。

    【讨论】:

      【解决方案6】:

      我找到了this great answer,它显示了一个我稍微修改过的算法。你可以这样做:

      function simpleCubeRoot(x) {    
          if (x === 0) {
              return 0;
          }
          if (x < 0) {
              return -simpleCubeRoot(-x);
          }
      
          var r = x;
          var ex = 0;
      
          while (r < 0.125) { 
              r *= 8; ex--; 
          }
          while (r > 1.0) { 
              r *= 0.125; ex++; 
          }
      
          r = (-0.46946116 * r + 1.072302) * r + 0.3812513;
      
          while (ex < 0) { 
              r *= 0.5; ex++; 
          }
          while (ex > 0) { 
              r *= 2; ex--; 
          }
      
          r = (2.0 / 3.0) * r + (1.0 / 3.0) * x / (r * r);
          r = (2.0 / 3.0) * r + (1.0 / 3.0) * x / (r * r);
          r = (2.0 / 3.0) * r + (1.0 / 3.0) * x / (r * r);
          r = (2.0 / 3.0) * r + (1.0 / 3.0) * x / (r * r);
      
          if (Number.isInteger(r)) {
              return r;
          }
          return false;
      }
      

      演示:

      function simpleCubeRoot(x) {
          if (x === 0) {
              return 0;
          }
          if (x < 0) {
              return -simpleCubeRoot(-x);
          }
      
          var r = x;
          var ex = 0;
      
          while (r < 0.125) {
              r *= 8;
              ex--;
          }
          while (r > 1.0) {
              r *= 0.125;
              ex++;
          }
      
          r = (-0.46946116 * r + 1.072302) * r + 0.3812513;
      
          while (ex < 0) {
              r *= 0.5;
              ex++;
          }
          while (ex > 0) {
              r *= 2;
              ex--;
          }
      
          r = (2.0 / 3.0) * r + (1.0 / 3.0) * x / (r * r);
          r = (2.0 / 3.0) * r + (1.0 / 3.0) * x / (r * r);
          r = (2.0 / 3.0) * r + (1.0 / 3.0) * x / (r * r);
          r = (2.0 / 3.0) * r + (1.0 / 3.0) * x / (r * r);
      
          if (Number.isInteger(r)) {
              return r;
          }
          return false;
      }
      
      console.log(simpleCubeRoot(27)); //Should return 3
      console.log(simpleCubeRoot(0)); //Should return 0

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-09-26
        • 1970-01-01
        • 2020-12-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多