前端学点基础数学知识--快速幂(Fast Power)

1,262 阅读5分钟

快速幂(fast power)

定义

快速幂也称为平方求幂(exponentiating squaring)就是快速算底数的正整数n次幂的方法。其时间复杂度为O(logn)O(logn)

幂的计算

在开始讲快速幂之前我们先来想一下,如果要你计算aba^b,你要如何计算呢?

我们一般的做法是,把a乘自身b次。

代码实现为

/**
 * 
 * @param {integer}} a 底数
 * @param {integer} b 指数
 */
function iterativePower(a, b) {
	let result = 1;
	while (b>0) {
		result *=a;
		b--;
	}
	return result;
}

这个方法的时间复杂度为O(N),N 为指数,也就是上面例子中的b

那么我们有没有更高效的方法来解决这个问题呢?

答案是有的也就是我们今天学习的方法,快速幂。这在指数比较大的时候非常有用。例如在矩阵幂,密码学中都有很好的运用。

快速幂基本方法

改方法是基于观察到,对于正整数n,我们有

xn={x(x2)n12,如果n是奇数(x2)n2,如果n是偶数x^n = \begin{cases} x(x^2)^{\frac{n-1}{2}}, 如果n是奇数\\ (x^2)^{\frac{n}{2}}, 如果n是偶数\\ \end{cases}

该方法使用指数的位(二进制的位,即bit,下文称为”位”)来确定计算哪些幂。

举个例子,如果我们计算x13x^{13}。幂指数13的二进制为1101。这些位按照从左到右的顺序使用。指数有4位,所以我们进行4次迭代。

首先,我们将结果result初始化为1.

  1. rr2(=x0)r\leftarrow r^2(=x^0), 第一位 = 1,所以计算 rrx(=x1)r\leftarrow r*x(= x^1)
  2. rr2(=x2)r\leftarrow r^2(=x^2), 第二位 = 1,所以计算 rrx(=x3)r\leftarrow r*x(= x^3)
  3. rr2(=x6)r\leftarrow r^2(=x^6), 第三位 = 0,所以这一步不用计算。
  4. rr2(=x12)r\leftarrow r^2(=x^{12}), 第四位 = 1,所以计算 rrx(=x13)r\leftarrow r*x(= x^{13})

在数学中\leftarrow相当于左边可以根据右边推导出来的意思。

下面给出快速幂的代码

递归版

/**
 *
 * @param {integer}} a 底数
 * @param {integer} b 指数
 */
function fastPower1(a, b) {
  if ((b === 0)) return 1
  if ((b === 1)) return a;
  if (b % 2 === 0) return fastPower1(a * a, b / 2)
  if (b % 2 !== 0) return a * fastPower1(a * a, (b - 1) / 2)
}


尾递归版

递归的增强版,不用再调用栈中保存计算过程中的变量,视为递归的增强版。

function exp_by_squaring(x, n) {
  return exp_by_squaring2(1, x, n);
}
function exp_by_squaring2(y, x, n) {
  if (n === 0) return y;
  if (n === 1) return x * y;
  if (n % 2 === 0) return exp_by_squaring2(y, x * x, n / 2);
  if (n % 2 === 1) return exp_by_squaring2(x * y, x * x, (n - 1) >> 1);
}

迭代版

可以用递归实现的我们都可以用迭代来做,性能上是好的,就是没递归好理解

function exp_by_squaring_iterative(x, n) {
  let y = 1;
  if (n === 0) return 1;
  while (n > 1) {
    if (n % 2 === 0) {
      x *= x;
      n /= 2;
    } else {
      y *= x;
      x *= x;
      n = (n - 1) >> 1;
    }
  }
  return x * y;
}

位运算优化版

这个版本是根据 n&1能获取到n的末尾是否为1的性质来进行奇偶性的判断。

function quickPow(a, n) {
  let base = a,
    res = 1;

  while (n) {
    if (n & 1) {
      res *= base;
    }
    base *= base;
    n >>= 1;
  }
  return res;
}

相关内容的基础知识

关于比较大的数如何展示的问题

如果结果比较大,比如2(1000)2^(1000),我们为了防止数据越界,在计算过程中对于每一步的结果会模(modulo)上1e9 + 7,至于原因嘛,是个TLDR的故事

十进制如何转换为二进制?

十进制整数转换为二进制整数十进制整数转换为二进制整数采用"除2取余,逆序排列"法。具体做法是:用2整除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为小于1时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。

789=1100010101(B)

789/2=394 余1 第10位

394/2=197 余0 第9位

197/2=98 余1 第8位

98/2=49 余0 第7位

49/2=24 余1 第6位

24/2=12 余0 第5位

12/2=6 余0 第4位

6/2=3 余0 第3位

3/2=1 余1 第2位

1/2=0 余1 第1位

原理:

众所周知,二进制的基数为2,我们十进制化二进制时所除的2就是它的基数。谈到它的原理,就不得不说说关于位权的概念。某进制计数制中各位数字符号所表示的数值表示该数字符号值乘以一个与数字符号有关的常数,该常数称为 “位权 ” 。位权的大小是以基数为底,数字符号所处的位置的序号为指数的整数次幂。十进制数的百位、十位、个位、十分位的权分别是10的2次方、10的1次方、10的0次方,10的-1次方。二进制数就是2的n次幂。

按权展开求和正是非十进制化十进制的方法。

下面我们开讲原理,举个十进制整数转换为二进制整数的例子,假设十进制整数A化得的二进制数为edcba 的形式,那么用上面的方法按权展开, 得

A=a(20)+b(21)+c(22)+d(23)+e(24)A=a(2^0)+b(2^1)+c(2^2)+d(2^3)+e(2^4) (后面的和不正是化十进制的过程吗)

假设该数未转化为二进制,除以基数2得

A/2=a(20)/2+b(21)/2+c(22)/2+d(23)/2+e(24)/2A/2=a(2^0)/2+b(2^1)/2+c(2^2)/2+d(2^3)/2+e(2^4)/2

注意:a不能整除2,但其他的能整除,因为他们都包含2,而a乘的是1,他本身绝对不包含因数2,只能余下。

商得:

b(20)+c(21)+d(22)+e(23)b(2^0)+c(2^1)+d(2^2)+e(2^3),再除以基数2余下了b,以此类推。

当这个数不能再被2除时,先余掉的a位数在原数低,而后来的余数数位高,所以要把所有的余数反过来写。正好是edcba.

索引

Exponentiation by squaring

快速幂