172. 阶乘后的零


ID: 172
TITLE: 阶乘后的零
TAG: Java,Python

方法一:计算阶乘

这种方法速度太慢了,但却是一个好的起点。虽然不会在面试中实现它,但是你可以简单的描述它是个解决问题的办法之一。

解决这个问题的最简单的办法就是计算 n ! n! n!,然后计算它的末尾数 0 个数。阶乘是通过将所有在 1 1 1 n n n 之间的数字相乘计算的。例如, 10 ! = 10 ⋅ 9 ⋅ 8 ⋅ 7 ⋅ 6 ⋅ 5 ⋅ 4 ⋅ 3 ⋅ 2 ⋅ 1 = 3 , 628 , 800 10! = 10 \cdot 9 \cdot 8 \cdot 7 \cdot 6 \cdot 5 \cdot 4 \cdot 3 \cdot 2 \cdot 1 = 3,628,800 10!=10987654321=3,628,800。因此,可以使用以下算法迭代计算阶乘。

define function factorial(n):
    n_factorial = 1
    for i from 1 to n (inclusive):
        n_factorial = n_factorial * i
    return n_factorial

如果一个数字末尾有零,那么它可以被 10 10 10整除。除以 10 10 10 将删除该零,并将所有其他数字右移一位。因此,我们可以通过反复检查数字是否可以被 10 10 10 整除来计算末尾 0 的个数。

define function zero_count(x):
    zero_count = 0
    while x is divisible by 10: 
        zero_count += 1
        x = x / 10
    return zero_count

通过将这两个函数放到一起,我们可以计算阶乘后的零个数。

算法:

在 Java 中,我们需要使用 BigInteger,防止在计算阶乘的过程中溢出。

def trailingZeroes(self, n: int) -> int:
        
    # Calculate n!
    n_factorial = 1
    for i in range(2, n + 1):
        n_factorial *= i
    
    # Count how many 0's are on the end.
    zero_count = 0
    while n_factorial % 10 == 0:
        zero_count += 1
        n_factorial //= 10
        
    return zero_count
import java.math.BigInteger;

public int trailingZeroes(int n) {
    
    // Calculate n!
    BigInteger nFactorial = BigInteger.ONE;
    for (int i = 2; i <= n; i++) {
        nFactorial = nFactorial.multiply(BigInteger.valueOf(i));
    }
                    
    // Count how many 0's are on the end.
    int zeroCount = 0;
    
    while (nFactorial.mod(BigInteger.TEN).equals(BigInteger.ZERO)) {
        nFactorial = nFactorial.divide(BigInteger.TEN);
        zeroCount++;
    }
    
    return zeroCount;
}

复杂度分析

  • 时间复杂度:低于 O ( n 2 ) O(n ^ 2) O(n2)

计算阶乘是重复的乘法。通常,当我们知道乘法是固定大小的数字上(例如 32 位或 64 位整数)时,我们可以视为 O ( 1 ) O(1) O(1) 运算。但是,这里要乘以的数字会随着 n n n 大小而增长,所以这里不能这么做。

因此,这里的第一步是考虑乘法的成本,因为我们不能假设它是 O ( 1 ) O(1) O(1)。把两个大数字相乘的流行方法它的成本是 O ( ( log ⁡ x ) ⋅ ( log ⁡ y ) ) O((\log x) \cdot (\log y)) O((logx)(logy))。我们将在近似值中使用它。

接下来,我们考虑以下在计算 n ! n! n! 时,我们做了什么乘法运算。前几个乘法如下:

1 ⋅ 2 = 2 1 \cdot 2 = 2 12=2
2 ⋅ 3 = 6 2 \cdot 3 = 6 23=6
6 ⋅ 4 = 24 6 \cdot 4 = 24 64=24
24 ⋅ 5 = 120 24 \cdot 5 = 120 245=120
120 ⋅ 6 = 720 120 \cdot 6 = 720 1206=720
. . . ... ...

这些乘法的成本:

log ⁡ 1 ⋅ log ⁡ 2 \log 1 \cdot \log 2 log1log2
log ⁡ 2 ⋅ log ⁡ 3 \log 2 \cdot \log 3 log2log3
log ⁡ 6 ⋅ log ⁡ 4 \log 6 \cdot \log 4 log6log4
log ⁡ 24 ⋅ log ⁡ 5 \log 24 \cdot \log 5 log24log5
log ⁡ 120 ⋅ log ⁡ 6 \log 120 \cdot \log 6 log120log6
. . . ... ...

我们可以改写为:

log ⁡ , 1 ! ⋅ log ⁡ , 2 \log , 1! \cdot \log , 2 log,1!log,2
log ⁡ , 2 ! ⋅ log ⁡ , 3 \log , 2! \cdot \log , 3 log,2!log,3
log ⁡ , 3 ! ⋅ log ⁡ , 4 \log , 3! \cdot \log , 4 log,3!log,4
log ⁡ , 4 ! ⋅ log ⁡ , 5 \log , 4! \cdot \log , 5 log,4!log,5
log ⁡ , 5 ! ⋅ log ⁡ , 6 \log , 5! \cdot \log , 6 log,5!log,6
. . . ... ...

发现了吗?每行的格式为 ( log ⁡ k ! ) ⋅ ( log ⁡ k + 1 ) (\log k!) \cdot (\log k + 1) (logk!)(logk+1),最后一行是什么?计算阶乘的最后一步是乘以 n n n。因此,最后一行必须是:

看到图案了吗?每行的格式为 ( log ⁡ , k ! ) ⋅ ( log ⁡ , k + 1 ) (\log,k!)\cdot(\log,k+1) logklogk+1。最后一行是什么?计算阶乘的最后一步是乘以n$。因此,最后一行必须是:

log ⁡ ( ( n − 1 ) ! ) ⋅ log ⁡ ( n ) \log ((n - 1)!) \cdot \log (n) log((n1)!)log(n)

因为我们一个接一个地做这些乘法运算,所以我们应该把它们相加,得到总的时间复杂度。得到:

log ⁡ 1 ! ⋅ log ⁡ 2 + log ⁡ 2 ! ⋅ log ⁡ 3 + log ⁡ 3 ! ⋅ log ⁡ 4 + ⋯ + log ⁡ ( ( n − 2 ) ! ) ⋅ log ⁡ ( n − 1 ) + log ⁡ ( ( n − 1 ) ! ) ⋅ log ⁡ n \log 1! \cdot \log 2 + \log 2! \cdot \log 3 + \log 3! \cdot \log 4 + \cdots + \log ((n - 2)!) \cdot \log (n - 1) + \log ((n - 1)!) \cdot \log n log1!log2+log2!log3+log3!log4++log((n2)!)log(n1)+log((n1)!)logn

这个序列相加起来相当复杂,我们不是要找到一个确切的答案,而是通过扔掉不太重要的项,找到粗略的下界近似。

在这一点上,你会发现算法比 O ( n ) O(n) O(n) 差,因为我们添加了 n n n 项。考虑到这个问题要求我们提出一个不低于 O ( log ⁡ n ) O(\log n) O(logn) 的算法。我们将进一步探讨,但是如果你已经理解到这一点,已经十分棒了。

注意 log ⁡ ( ( n − 1 ) ! ) \log ((n - 1)!) log((n1)!) log ⁡ n \log n logn 大的多。因此,我们将删除这部分,留下 log ⁡ ( ( n − 1 ) ! ) \log ((n - 1)!) log((n1)!)。得到:

log ⁡ 1 ! + log ⁡ 2 ! + log ⁡ 3 ! + ⋯ + log ⁡ ( ( n − 2 ) ! ) + log ⁡ ( ( n − 1 ) ! ) \log 1! + \log 2! + \log 3! + \cdots + \log ((n - 2)!) + \log ((n - 1)!) log1!+log2!+log3!++log((n2)!)+log((n1)!)

下一部分涉及到一个 log 原则,你可能听说过,也可能没有。如果你还没听说过,那么绝对指的记住,因为它非常有用。

O ( log ⁡ n ! ) = O ( n log ⁡ n ) O(\log n!) = O(n \log n) O(logn!)=O(nlogn)

我们根据这个原则重写序列:

1 ⋅ log ⁡ 1 + 2 ⋅ log ⁡ 2 + 3 ⋅ log ⁡ 3 + ⋯ + ( n − 2 ) ⋅ log ⁡ ( n − 2 ) + ( n − 1 ) ⋅ log ⁡ ( n − 1 ) 1 \cdot \log 1 + 2 \cdot \log 2 + 3 \cdot \log 3 + \cdots + (n - 2) \cdot \log (n - 2) + (n - 1) \cdot \log (n - 1) 1log1+2log2+3log3++(n2)log(n2)+(n1)log(n1)

像以前一样,我们把较小的项去掉,看看剩下什么:

1 + 2 + 3 + . . . + ( n − 2 ) + ( n − 1 ) 1 + 2 + 3 + ... + (n - 2) + (n - 1) 1+2+3+...+(n2)+(n1)

这是个非常常见的序列,它的成本是 O ( n 2 ) O(n^2) O(n2)

那么,我们能得出什么结论呢?丢弃了项以后会使我们的时间复杂度低于真实的时间复杂度。换句话说,这个阶乘算法复杂度小于 O ( n 2 ) O(n^2) O(n2)

但是 O ( n 2 ) O(n^2) O(n2) 绝对不够好!。

尽管这种丢弃项的方法看起来有点奇怪,但快速做出早期决策非常有用,而不必费心与高等数学。只有当我们决定进一步研究改算法时,才会尝试得出更加精确的时间复杂度。在这种情况下,我们的下限足够让我们相信它绝对不值得一看!

第二部分,在结尾数零,与第一部分相比微不足道。

  • 空间复杂度: O ( log ⁡ n ! ) = O ( n log ⁡ n ) O(\log n!) = O(n \log n) O(logn!)=O(nlogn),为了存储 n ! n! n!,我们需要 O ( log ⁡ n ! ) O(\log n!) O(logn!) 位,而它等于 O ( n log ⁡ n ) O(n \log n) O(nlogn)

方法二:计算因子 5

这种方法也太慢了,但是在解决问题的过程中,它很可能是提出对数方法的一个步骤。

与方法 1 中那样计算阶乘不同,我们可以认识到阶乘末尾的每个 0 0 0 表示乘以 10 10 10

那么,我们在计算 n ! n! n! 时乘以 10 10 10 的次数是多少?两个数字 a a a b b b 相乘。例如,要执行 42 ⋅ 75 = 3150 42 \cdot 75 = 3150 4275=3150,可以重写如下:

42 = 2 ⋅ 3 ⋅ 7 42 = 2 \cdot 3 \cdot 7 42=237
75 = 3 ⋅ 5 ⋅ 5 75 = 3 \cdot 5 \cdot 5 75=355
42 ⋅ 75 = 2 ⋅ 3 ⋅ 7 ⋅ 3 ⋅ 5 ⋅ 5 42 \cdot 75 = 2 \cdot 3 \cdot 7 \cdot 3 \cdot 5 \cdot 5 4275=237355

现在,为了确定最后有多少个零,我们应该看有多少对 2 2 2 5 5 5 的因子。在上面的例子中,我们有一对 2 2 2 5 5 5 的因子。

那么,这和阶乘有什么关系呢?在一个阶乘中,我们把所有 1 1 1 n n n 之间的数相乘,这和把所有 1 1 1 n n n 之间所有数字的因子相乘是一样的。

例如,如果 n = 16 n=16 n=16,我们需要查看 1 1 1 16 16 16 之间所有数字的因子。我们只对 2 2 2 5 5 5 有兴趣。包含 5 5 5 因子的数字是 5 , 10 , 15 {5,10,15} 51015,包含因子 2 2 2 的数字是 2 、 4 、 6 、 8 、 10 、 12 、 14 、 16 {2、4、6、8、10、12、14、16} 246810121416。因为只三个完整的对,因此 16 ! 16! 16! 后有三个零。

把这个放到一个算法中,我们得到:

twos = 0
for i from 1 to n inclusive:
    if i is divisible by 2:
        twos += 1

fives = 0
for i from 1 to n inclusive:
    if i is divisible by 5:
        fives += 1

tens = min(fives, twos)

这可以解决大部分情况,但是有的数字存在一个以上的因子。例如,若 i = 25,那么我们只做了 fives += 1。但是我们应该 fives += 2,因为 25 25 25 有两个因子 5。

因此,我们需要计算每个数字中的因子 5 5 5。我们可以使用一个循环而不是 if 语句,我们若有因子 5 5 5 将数字除以 5 5 5。如果还有剩余的因子 5 5 5,则将重复步骤。

我们可以这样做:

twos = 0
for i from 1 to n inclusive:
    remaining_i = i
    while remaining_i is divisible by 2:
        twos += 1
        remaining_i = remaining_i / 2

fives = 0
for i from 1 to n inclusive:
    remaining_i = i
    while remaining_i is divisible by 5:
        fives += 1
        remaining_i = remaining_i / 5

tens = min(twos, fives)

这样我们就得到了正确答案,但是我们仍然可以做一些改进。

首先,我们可以注意到因子 2 2 2 数总是比因子 5 5 5 大。为什么?因为每四个数字算作额外的因子 2 2 2,但是只有每 25 25 25 个数字算作额外的因子 5 5 5。下图我们可以清晰的看见:

172. 阶乘后的零_第1张图片
因此我们可以删除计算因子 2 2 2 的过程,留下:

fives = 0
for i from 1 to n inclusive:
    remaining_i = i
    while remaining_i is divisible by 5:
        fives += 1
        remaining_i = remaining_i / 5

tens = fives

我们可以做最后一个优化。在上面的算法中,我们分析了从 1 1 1 n n n 的每个数字。但是只有 5 , 10 , 15 , 20 , 25 , 30 , . . . 等 等 5, 10, 15, 20, 25, 30, ... 等等 5,10,15,20,25,30,... 至少有一个因子 5 5 5。所以,ww偶们不必一步一步的往上迭代,可以五步的往上迭代:因此可以修改为:

fives = 0
for i from 5 to n inclusive in steps of 5:
    remaining_i = i
    while remaining_i is divisible by 5:
        fives += 1
        remaining_i = remaining_i / 5

tens = fives

算法:

这是我们设计的算法

def trailingZeroes(self, n: int) -> int:
        
    zero_count = 0
    for i in range(5, n + 1, 5):
        current = i
        while current % 5 == 0:
            zero_count += 1
            current //= 5

    return zero_count
public int trailingZeroes(int n) {
        
    int zeroCount = 0;
    for (int i = 5; i <= n; i += 5) {
        int currentFactor = i;
        while (currentFactor % 5 == 0) {
            zeroCount++;
            currentFactor /= 5;
        }
    }
    return zeroCount;
}

或者,我们可以检查 5 5 5 的幂,而不是每次除以 5 5 5 来计算因子数。这是通过检查 i 是否可以被 5 5 5 25 25 25 125 125 125 等整除来实现的。

def trailingZeroes(self, n: int) -> int:
        
    zero_count = 0
    for i in range(5, n + 1, 5):
        power_of_5 = 5
        while i % power_of_5 == 0:
            zero_count += 1
            power_of_5 *= 5

    return zero_count
public int trailingZeroes(int n) {
    
    int zeroCount = 0;
    for (int i = 5; i <= n; i += 5) {
        int powerOf5 = 5;
        while (i % powerOf5 == 0) {
            zeroCount += 1;
            powerOf5 *= 5;
        }
    }
    return zeroCount;
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n)

在方法一中,我们不能将出发看作 O ( 1 ) O(1) O(1),不过在此方法中,我们保持在它的范围内,因此可以将除法和乘法看作 O ( 1 ) O(1) O(1)

为了计算零计数,我们循环从 5 5 5 n n n 的每五个数字处理一次,这里是 O ( n ) O(n) O(n) 步(将 1 5 \frac{1}{5} 51 作为常量处理)。

在每个步骤中,虽然看起来像是执行 O ( log ⁡ n ) O(\log n) O(logn) 操作来计算 5 的因子数,但实际上它只消耗 O ( 1 ) O(1) O(1),因为绝大部分的数字只包含一个因子 5 5 5。可以证明,因子 5 5 5 的总数小于 2 ⋅ n 5 \frac{2 \cdot n}{5} 52n

所以我们得到 O ( n ) ⋅ O ( 1 ) = O ( n ) O(n) \cdot O(1) = O(n) O(n)O(1)=O(n)

  • 空间复杂度: O ( 1 ) O(1) O(1),只是用了一个整数变量。

方法三:高效的计算因子 5

在方法 2 中,我们找到了一种计算阶乘后的零的个数的方法,而不需要实际计算阶乘。这是通过在 5 5 5 的每个倍数上循环,从 5 5 5 n n n,并计算 5 5 5 的每个倍数中有多少个因子。我们把所有这些数字加起来得到最终结果。

然而,无论是实际上还是对问题的要求来说,方法 2 仍然太慢。为了得出一个足够快的算法,我们需要做进一步改进,这个改进能使我们在对数时间内计算出我们的答案。

思考我们之前简化的算法(但不正确),它不正确是因为对于有多个因子 5 5 5 时会计算出错,例如 25 25 25

fives = 0
for i from 1 to n inclusive:
    if i is divisible by 5:
        fives += 1

你会发现这是执行 n 5 \frac{n}{5} 5n 的低效方法。我们只对 5 5 5 的倍数感兴趣,不是 5 5 5 的倍数可以忽略,因此可以简化成:

fives = n / 5
tens = fives

那么,如何解决有多重因子的数字呢?所有包含两个及以上的因子 5 5 5 的数字都是 25 25 25 的倍数。所以我们可以简单的除以 25 25 25 来计算 25 25 25 的倍数是多少。另外,我们在 n 5 \frac{n}{5} 5n 已经计算了 25 25 25 一次,所以我们只需要再计算额外因子一次 n 25 \frac{n}{25} 25n (而不是 2 ⋅ n 25 2\cdot\frac{n}{25} 225n

所以结合起来我们得到:

fives = n / 5 + n / 25
tens = fives

但是如果有三个因子 5 5 5 呢!为了得到最终的结果,我们需要将所有的 n 5 \dfrac{n}{5} 5n n 25 \dfrac{n}{25} 25n n 125 \dfrac{n}{125} 125n n 625 \dfrac{n}{625} 625n 等相加。得到:

f i v e s = n 5 + n 25 + n 125 + n 625 + n 3125 + ⋯ fives=\dfrac{n}{5}+\dfrac{n}{25}+\dfrac{n}{125}+\dfrac{n}{625}+\dfrac{n}{3125}+\cdots fives=5n+25n+125n+625n+3125n+

这样看起来会一直计算下去,但是并非如此!我们使用整数除法,最终,分母将大于 n n n,因此当项等于 0 时,就可以停止我们的计算。

例如,如果 n = 12345 n=12345 n=12345,我们得到:

f i v e s = 12345 5 + 12345 25 + 12345 125 + 12345 625 + 12345 3125 + 12345 16075 + 12345 80375 + ⋯ fives=\dfrac{12345}{5}+\dfrac{12345}{25}+\dfrac{12345}{125}+\dfrac{12345}{625}+\dfrac{12345}{3125}+\dfrac{12345}{16075}+\dfrac{12345}{80375}+\cdots fives=512345+2512345+12512345+62512345+312512345+1607512345+8037512345+

等于:

f i v e s = 2469 + 493 + 98 + 19 + 3 + 0 + 0 + ⋯ = 3082 fives = 2469 + 493 + 98 + 19 + 3 + 0 + 0 + \cdots = 3082 fives=2469+493+98+19+3+0+0+=3082

在代码中,我们可以通过循环 5 5 5 的幂来计算。

fives = 0
power_of_5 = 5
while n >= power_of_5:
    fives += n / power_of_5
    power_of_5 *= 5

tens = fives

算法:

def trailingZeroes(self, n: int) -> int:
    zero_count = 0
    current_multiple = 5
    while n >= current_multiple:
        zero_count += n // current_multiple
        current_multiple *= 5
    return zero_count
public int trailingZeroes(int n) {
    int zeroCount = 0;
    // We need to use long because currentMultiple can potentially become
    // larger than an int.
    long currentMultiple = 5;
    while (n >= currentMultiple) {
        zeroCount += (n / currentMultiple);
        currentMultiple *= 5;
    }
    return zeroCount;
}

编写此算法的另一种方法是,我们不必每次尝试 5 5 5 的幂,而是每次将 n n n 本身除以 5 5 5。这也是一样的,因为我们最终得到的序列是:

f i v e s = n 5 + ( n 5 ) 5 + ( ( n 5 ) 5 ) 5 + ⋯ fives=\dfrac{n}{5}+\dfrac{(\dfrac{n}{5})}{5}+\dfrac{(\dfrac{(\frac{n}{5})}{5})}{5}+\cdots fives=5n+55n+555n+

注意,在第二步中,我们有 ( n 5 ) 5 \dfrac{(\frac{n}{5})}{5} 55n。这是因为前一步将 n n n 本身除以 5 5 5。等等。

如果你熟悉分数规则,你会发现 ( n 5 ) 5 \dfrac{(\frac{n}{5})}{5} 55n n 5 ⋅ 5 = n 25 \dfrac{n}{5\cdot 5}=\frac{n}{25} 55n=25n 是一样的。这意味着序列与:

n 5 + n 25 + n 125 + ⋯ \dfrac{n}{5}+\dfrac{n}{25}+\dfrac{n}{125}+\cdots 5n+25n+125n+

因此,这种编写算法的替代方法是等价的。

def trailingZeroes(self, n: int) -> int:
    zero_count = 0
    while n > 0:
        n //= 5
        zero_count += n
    return zero_count
public int trailingZeroes(int n) {
    int zeroCount = 0;
    long currentMultiple = 5;
    while (n > 0) {
        n /= 5;
        zeroCount += n;
    }
    return zeroCount;
}

复杂度分析

  • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)。在这种方法中,我们将 n n n 除以 5 5 5 的每个幂。根据定义, 5 5 5 log ⁡ 5 n \log_5n log5n 幂小于或等于 n n n。由于乘法和除法在 32 位整数范围内,我们将这些计算视为 O ( 1 ) O(1) O(1)。因此,我们正在执行 log ⁡ 5 n ⋅ O ( 1 ) = log ⁡ n \log_5 n\cdot O(1)=\log n log5nO1=logn 操作
  • 空间复杂度: O ( 1 ) O(1) O(1),只是用了常数空间。

你可能感兴趣的:(leetcode题解翻译)