第二章 算法分析

第二章 算法分析

🟦数学基础

在这个部分需要理解的概念只有一个,就是相对增长率(relative rates of growth)。要理解概念以及对应的数学符号。

1. 大 O O O 记法(算法上界)

大 O 记法是一种用于表示算法时间复杂度的数学符号表示法。它描述了在最坏情况下,算法的运行时间与输入规模的增长关系。

大 O 记法的定义如下:

假设 f ( n ) f(n) f(n) g ( n ) g(n) g(n)是定义在非负整数集合上的函数。如果存在正常数 c c c n 0 n_0 n0,使得对于所有的 n > n 0 n > n_0 n>n0,都有 f ( n ) ≤ c ⋅ g ( n ) f(n) \leq c \cdot g(n) f(n)cg(n),则称 f ( n ) f(n) f(n) O ( g ( n ) ) O(g(n)) O(g(n))

换句话说, f ( n ) f(n) f(n) O ( g ( n ) ) O(g(n)) O(g(n))表示 f ( n ) f(n) f(n) 在增长速度上不超过 g ( n ) g(n) g(n) ,或者说 g ( n ) g(n) g(n) f ( n ) f(n) f(n) 的一个渐进上界。

例如,如果一个算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),则表示算法的运行时间与输入规模的平方成正比,即它的时间复杂度为二次方。

T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n))

其中, T ( n ) T(n) T(n)表示算法的执行时间, f ( n ) f(n) f(n)表示问题规模 n n n 的函数, O ( f ( n ) ) O(f(n)) O(f(n)) 表示算法的时间复杂度。

2. Ω Ω Ω 记法(算法下界)

Ω Ω Ω 记法是用于表示算法时间复杂度的另一种渐进符号表示法。它描述了在最好情况下,算法的运行时间与输入规模的增长关系。

Ω Ω Ω 记法的定义如下:

假设 f ( n ) f(n) f(n) g ( n ) g(n) g(n) 是定义在非负整数集合上的函数。如果存在正常数 c c c n 0 n_0 n0 ,使得对于所有的 n > n 0 n > n_0 n>n0 ,都有 f ( n ) ≥ c ⋅ g ( n ) f(n) \geq c \cdot g(n) f(n)cg(n) ,则称 f ( n ) f(n) f(n) Ω ( g ( n ) ) Ω(g(n)) Ω(g(n))

换句话说, f ( n ) f(n) f(n) Ω ( g ( n ) ) Ω(g(n)) Ω(g(n)) 表示 f ( n ) f(n) f(n) 在增长速度上不低于 g ( n ) g(n) g(n) ,或者说 g ( n ) g(n) g(n) f ( n ) f(n) f(n) 的一个渐进下界。

例如,如果一个算法的时间复杂度为 Ω ( n ) Ω(n) Ω(n) ,则表示算法的运行时间与输入规模呈线性关系,即它的时间复杂度为线性。
T ( n ) = Ω ( g ( n ) ) T(n) = Ω(g(n)) T(n)=Ω(g(n))

3. Θ Θ Θ 记法(增长率相等)

Θ Θ Θ 记法是用于表示算法时间复杂度的一种渐进符号表示法,它描述了在最好情况和最坏情况下,算法的运行时间与输入规模的增长关系。

Θ Θ Θ 记法的定义如下:

假设 f ( n ) f(n) f(n) g ( n ) g(n) g(n) 是定义在非负整数集合上的函数。如果存在正常数 c 1 c_1 c1 c 2 c_2 c2 n 0 n_0 n0,使得对于所有的 n > n 0 n > n_0 n>n0,都有 c 1 ⋅ g ( n ) ≤ f ( n ) ≤ c 2 ⋅ g ( n ) c_1 \cdot g(n) \leq f(n) \leq c_2 \cdot g(n) c1g(n)f(n)c2g(n),则称 f ( n ) f(n) f(n) Θ ( g ( n ) ) Θ(g(n)) Θ(g(n))

换句话说, f ( n ) f(n) f(n) Θ ( g ( n ) ) Θ(g(n)) Θ(g(n)) 表示 f ( n ) f(n) f(n) 在增长速度上与 g ( n ) g(n) g(n) 相匹配,或者说 g ( n ) g(n) g(n) f ( n ) f(n) f(n) 的一个渐进紧确界。

例如,如果一个算法的时间复杂度为 Θ ( n ) Θ(n) Θ(n),则表示算法的运行时间与输入规模呈线性关系,且算法的最好情况和最坏情况下的运行时间相同。

4. 小 o o o 记法(上界不含相等情况)

o o o 记法是用于表示算法时间复杂度的一种渐进符号表示法,它描述了在最坏情况下,算法的运行时间与输入规模的增长关系。

o o o 记法的定义如下:

假设 f ( n ) f(n) f(n) g ( n ) g(n) g(n) 是定义在非负整数集合上的函数。如果对于任意正常数 c > 0 c > 0 c>0,存在一个正常数 n 0 n_0 n0,使得对于所有的 n > n 0 n > n_0 n>n0,都有 f ( n ) < c ⋅ g ( n ) f(n) < c \cdot g(n) f(n)<cg(n),则称 f ( n ) f(n) f(n) o ( g ( n ) ) o(g(n)) o(g(n))

换句话说, f ( n ) f(n) f(n) o ( g ( n ) ) o(g(n)) o(g(n)) 表示 f ( n ) f(n) f(n) 在增长速度上低于 g ( n ) g(n) g(n),或者说 g ( n ) g(n) g(n) f ( n ) f(n) f(n) 的一个渐进上界。

例如,如果一个算法的时间复杂度为 o ( n 2 ) o(n^2) o(n2),则表示算法的运行时间与输入规模的平方的增长关系相比,其增长速度更慢。

PS:各位看官不要害怕,这是在算法分析这个情境下的比大小,简单理解就好。(~ ̄▽ ̄)~😎

5. 三个重要结论

  • RULE 1 :
    If T 1 ( n ) = O ( f ( n ) ) T1(n) = O(f(n)) T1(n)=O(f(n)) and T 2 ( n ) = O ( g ( n ) ) T2(n) = O(g(n)) T2(n)=O(g(n)), then

    • (a) T 1 ( n ) + T 2 ( n ) = m a x ( O ( f ( n ) ) , O ( g ( n ) ) ) T1(n) + T2(n) = max (O(f(n)), O(g(n))) T1(n)+T2(n)=max(O(f(n)),O(g(n)))
    • (b) T 1 ( n ) ∗ T 2 ( n ) = O ( f ( n ) ∗ g ( n ) ) T1(n) * T2(n) = O(f(n) * g(n)) T1(n)T2(n)=O(f(n)g(n))
  • RULE 2:
    If T ( x ) T(x) T(x) is a polynomial of degree n, then T ( x ) = ( x n ) T(x) = (x^n ) T(x)=(xn)
    也就是多项式取最大幂次项作为该式子的增长率

  • RULE 3:

    l o g k n = O ( n ) log^k n = O(n) logkn=O(n)for any constant k k k. This tells us that logarithms grow very slowly
    也就是强调一下对数函数的增长速度非常缓慢,甚至比线性增长还要慢1,比较反直觉。

🟦模型

直接贴下原文:

In order to analyze algorithms in a formal framework, we need a model of computation. Our model is basically a normal computer, in which instructions are executed sequentially. Our model has the standard repertoire of simple instructions, such as addition, multiplication, comparison, and assignment, but, unlike real computers, it takes exactly one time unit to do anything (simple). To be reasonable, we will assume that, like a modern computer, our model has fixed-size (say 32-bit) integers and that there are no fancy operations, such as matrix inversion or sorting, that clearly cannot be done in one time unit. We also assume infinite memory.

This model clearly has some weaknesses. Obviously, in real life, not all operations take exactly the same time. In particular, in our model, one disk read counts the same as an addition, even though the addition is typically several orders of magnitude faster. Also, by assuming infinite memory, we never worry about page faulting, which can be a real problem, especially for efficient algorithms. This can be a major problem in many applications.

为了在一个形式化的框架中分析算法,我们需要一个计算模型。我们的模型基本上是一个正常的计算机,在其中指令按顺序执行。我们的模型具有标准的简单指令,如加法、乘法、比较和赋值,但与真实计算机不同的是,执行任何操作(简单操作)都需要完全一致的时间单位。为了合理起见,我们假设,与现代计算机一样,我们的模型具有固定大小(比如32位)的整数,并且没有像矩阵求逆或排序等复杂操作,这些操作显然不能在一个时间单位内完成。我们还假设内存是无限的。

这个模型显然有一些弱点。显然,在现实生活中,并非所有操作都需要完全相同的时间。特别是,在我们的模型中,一个磁盘读取操作与一次加法操作被视为同样的时间单位,尽管加法操作通常快了几个数量级。另外,通过假设内存是无限的,我们从不担心页面错误,尽管对于高效的算法来说,页面错误可能是一个真正的问题。这在许多应用中可能是一个主要问题。

当作英语阅读了,大概的意思是强调任意的算法分析都有个理论前提,是基于一定的模型概念上的。

🟦要分析的问题

仍然是再次强调算法概念的边界:

The most important resource to analyze is generally the running time. Several factors affect the running time of a program. Some, such as the compiler and computer used, are obviously beyond the scope of any theoretical model, so, although they are important, we cannot deal with them here. The other main factors are the algorithm used and the input to the algorithm.

也就是说抛开模型之外的因素,我们只需要聚焦算法的实现以及对算法的输入。(PS:真正的落地赋能还得结合真实业务和软件工程的辅佐)。

Typically, the size of the input is the main consideration.

算法的输入大小是主要的考虑因素。

  • T a v g ( n ) T_{avg}(n) Tavg(n) 平均运行时间

  • T w o r s t ( n ) T_{worst}(n) Tworst(n) 最坏运行时间

We remark that generally the quantity required is the worst-case time, unless otherwise specified. One reason for this is that it provides a bound for all input, including particularly bad input, that an average-case analysis does not provide. The other reason is that average-case bounds are usually much more difficult to compute. In some instances, the definition of “average” can affect the result. (For instance, what is average input for the following problem?)

我们注意到通常所需的数量是最坏情况的时间,除非另有规定。这样做的一个原因是它为所有输入提供了一个界限,包括特别糟糕的输入,在平均情况分析中没有提供。另一个原因是平均情况下的界限通常更难计算。在某些情况下,“平均” 的定义可以影响结果。(例如,对于以下问题,什么是平均输入?)

也就是分析的时候会考虑最坏情况,因为这个时候输入有确定性,并且结果能够得到一个上界,是有参考意义的。而平均情况则是既难而且参考意义不大,书中举了求最大子序列的例子来说明,这里就不赘述啦~~(懒)~~

🟦运行时间计算

说明了4个计算方式:

  • RULE 1-FOR LOOPS:
    The running time of a for loop is at most the running time of the statements inside the for loop (including tests) times the number of iterations.

  • RULE 2-NESTED FOR LOOPS:
    Analyze these inside out. The total running time of a statement inside a group of nested for loops is the running time of the statement multiplied by the product of the sizes of all the for loops. As an example, the following program fragment is O ( n 2 ) O(n^2) O(n2)

for( i=0; i<n; i++ )
 	for( j=0; j<n; j++ )
 		k++;
  • RULE 3-CONSECUTIVE STATEMENTS:
    These just add (which means that the maximum is the one that counts). As an example, the following program fragment, which has O ( n ) O(n) O(n) work followed by O ( n 2 ) O (n^2) O(n2) work, is also O ( n 2 ) O (n^2) O(n2)

     for( i=0; i<n; i++)
     	a[i] = 0;
    
     for( i=0; i<n; i++ )
     	for( j=0; j<n; j++ )
     		a[i] += a[j] + i + j;
    
  • RULE 4-lF/ELSE:
    For the fragment

    if( cond )
        S1 
    else 
        S2
    

    the running time of an if/else statement is never more than the running time of the test plus the larger of the running times of S 1 S1 S1 and S 2 S2 S2

最大子序列问题的解

测试的main函数:

int main() {
    int array[] = {1, -2, 3, 4, -5, 6, 7, -8, 9};
    int maxSubsequenceSum = MaxSubsequenceSum4(array, sizeof(array) / sizeof(array[0]));
    printf("Max subsequence sum: %d\n", maxSubsequenceSum);

    return 0;
}
1. 穷举法
// 1. 穷举法
int MaxSubsequenceSum(const int A[], int N) {
    int ThisSum, MaxSum, i, j, k;

    MaxSum = 0;
    for (i = 0; i < N; i++) {
        for (j = i; j < N; j++) {
            ThisSum = 0;
            for (k = i; k <= j; k++) {
                ThisSum += A[k];
            }
            if (ThisSum > MaxSum) {
                MaxSum = ThisSum;
            }
        }
    }
    return MaxSum;
}

这个算法的时间复杂度为 O ( N 3 ) O(N^3) O(N3)。这是因为它包含了三个嵌套的循环:外部循环从 i = 0 i = 0 i=0 N − 1 N-1 N1,中间循环从 j = i j = i j=i N − 1 N-1 N1,内部循环从 k = i k = i k=i j j j。因此,总共有 N 3 N^3 N3 次基本操作(比较和加法)。

因为这个算法的时间复杂度与输入大小的立方成正比,所以它在大型数据集上的运行时间可能会非常长。因此,它不适用于大型数据集的情况。

2. 优化的穷举(去掉一个for循环)
int MaxSubsequenceSum(const int A[], int N) {
    int ThisSum, MaxSum, i, j;

    MaxSum = 0;
    for (i = 0; i < N; i++) {
        ThisSum = 0;
        for (j = i; j < N; j++) {
            ThisSum += A[j];
            if (ThisSum > MaxSum) {
                MaxSum = ThisSum;
            }
        }
    }
    return MaxSum;
}

这个算法的时间复杂度为 O ( N 2 ) O(N^2) O(N2)。外部循环从 i = 0 i = 0 i=0 N − 1 N-1 N1,内部循环从 j = i j = i j=i N − 1 N-1 N1。每次内部循环执行的操作数与 j − i j - i ji 成正比,因此总共执行的基本操作(比较和加法)数量为:

∑ i = 0 N − 1 ( N − i ) = N ( N − 1 ) / 2 ≈ 1 2 N 2 \sum_{i=0}^{N-1} (N - i) = N(N-1)/2 \approx \frac{1}{2} N^2 i=0N1(Ni)=N(N1)/221N2
因此,这个算法的时间复杂度为 O ( N 2 ) O(N^2) O(N2)

3. 分而治之
// 返回三个整数中的最大值
int Max3(int a, int b, int c) {
    int max = a;
    if (b > max) {
        max = b;
    }
    if (c > max) {
        max = c;
    }
    return max;
}

// 返回跨越中点的最大子序列和
static int MaxSubSum(const int A[], int Left, int Right) {
    int MaxLeftSum, MaxRightSum; // 左子序列最大和、右子序列最大和
    int MaxLeftBorderSum, MaxRightBorderSum; // 跨越中点的左子序列最大和、右子序列最大和
    int LeftBorderSum, RightBorderSum; // 跨越中点的左边部分和、右边部分和
    int Center, i;

    if (Left == Right) { // 基本情况:子列只有一个元素
        if (A[Left] > 0) {
            return A[Left];
        } else {
            return 0;
        }
    }

    Center = (Left + Right) / 2;
    MaxLeftSum = MaxSubSum(A, Left, Center);
    MaxRightSum = MaxSubSum(A, Center + 1, Right);

    // 计算跨越中点的最大子序列和
    MaxLeftBorderSum = 0;
    LeftBorderSum = 0;
    for (i = Center; i >= Left; i--) {
        LeftBorderSum += A[i];
        if (LeftBorderSum > MaxLeftBorderSum) {
            MaxLeftBorderSum = LeftBorderSum;
        }
    }

    MaxRightBorderSum = 0;
    RightBorderSum = 0;
    for (i = Center + 1; i <= Right; i++) {
        RightBorderSum += A[i];
        if (RightBorderSum > MaxRightBorderSum) {
            MaxRightBorderSum = RightBorderSum;
        }
    }

    // 返回三种情况下的最大值
    return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum);
}

// 返回整个数组的最大子序列和
int MaxSubsequenceSum2(const int A[], int N) {
    return MaxSubSum(A, 0, N - 1);
}

这个算法的时间复杂度为 O ( N log ⁡ N ) O(N \log N) O(NlogN)

算法的主要部分是 MaxSubSum 函数,它采用了分治法来解决问题。该函数首先将问题划分为两个子问题,然后递归地解决这些子问题,最后合并子问题的解以得到原问题的解。

在每次递归调用中,算法将数组划分为两个大致相等大小的子数组。因此,算法的递归深度为 log ⁡ N \log N logN。在每个递归层级上,需要线性时间 O ( N ) O(N) O(N) 来计算跨越中点的最大子序列和,因此总的时间复杂度为 O ( N log ⁡ N ) O(N \log N) O(NlogN)

所以,该算法的时间复杂度为 O ( N log ⁡ N ) O(N \log N) O(NlogN)

4. 在线算法
// 计算数组的最大子序列和
int MaxSubsequenceSum(const int A[], int N) {
    int ThisSum, MaxSum, j;

    ThisSum = MaxSum = 0; // 初始化当前和和最大和为0
    for (j = 0; j < N; j++) {
        ThisSum += A[j]; // 将当前元素加到当前和中
        if (ThisSum > MaxSum) { // 如果当前和大于最大和
            MaxSum = ThisSum; // 更新最大和
        } else if (ThisSum < 0) { // 如果当前和为负数
            ThisSum = 0; // 重新开始计算当前和
        }
    }
    return MaxSum; // 返回最大和
}

This time bound is correct, but it takes a little thought to see why the algorithm actually works. This is left to the reader.

An extra advantage of this algorithm is that it makes only one pass through the data, and once a[i] is read and processed, it does not need to be remembered. Thus, if the array is on a disk or tape, it can be read sequentially, and there is no need to store any part of it in main memory. Furthermore, at any point in time, the algorithm can correctly give an answer to the subsequence problem for the data it has already read (the other algorithms do not share this property). Algorithms that can do this are called online algorithms. An online algorithm that requires only constant space and runs in linear time is just about as good as possible.

这个时间界限是正确的原因应该是清楚的,但要想明白这个算法为什么实际上有效,可能需要花一点时间思考。这留给读者自行思考。

这个算法的额外优点是它只需对数据进行一次遍历,一旦 a[i] 被读取并处理,就不需要记住它。因此,如果数组存储在磁盘或磁带上,可以按顺序读取它,并且无需在主内存中存储任何部分。此外,任何时候,该算法都可以正确地给出对已经读取的数据的子序列问题的答案(其他算法不具有此属性)。能够做到这一点的算法称为在线算法。一个只需要常量空间并且运行时间是线性的在线算法几乎是最佳的。

这个算法的时间复杂度为 O ( N ) O(N) O(N)

算法只需一次遍历数组,因此时间复杂度与数组的大小 N N N 成线性关系。在每次遍历中,只需要常数时间执行一些基本操作(比较、加法和赋值),因此总的时间复杂度为 O ( N ) O(N) O(N)

因此,该算法的时间复杂度为 O ( N ) O(N) O(N)

对数特点的算法

The most confusing aspect of analyzing algorithms probably centers around the logarithm. We have already seen that some divide-and-conquer algorithms will run in O ( n l o g n ) O(n log n) O(nlogn) time. Besides divide-and conquer algorithms, the most frequent appearance of logarithms centers around the following general rule:

An algorithm is $O(log n) i f ∗ ∗ i t t a k e s c o n s t a n t ( if **it takes constant ( ifittakesconstant(O(1)$) time to cut the problem size by a fraction (which is usually 1/2)**

用常数时间将问题的大小削减了一部分,通常是二分之一。

On the other hand, if constant time is required to merely reduce the problem by a constant amount (such as to make the problem smaller by 1), then the algorithm is O ( n ) O(n) O(n)

如果只是削减了一个常数那么就是 O ( n ) O(n) O(n)的。

列举三个具有对数特点的例子:

1. 对分查找

BINARY SEARCH:

Given an integer x x x and integers a 1 , a 2 , . . . , a n , a1, a2, . . . , a_n, a1,a2,...,an, which are presorted and already in memory, find i i i such that a i = x a_i = x ai=x, or return $ i = 0$ if x x x is not in the input.

给定一个整数 x x x 和整数 a 1 , a 2 , . . . , a n a_1, a_2, . . . , a_n a1,a2,...,an,这些整数已经按顺序排序并存储在内存中,找到满足 a i = x a_i = x ai=x i i i,如果 x x x 不在输入中则返回 i = 0 i = 0 i=0

#include <stdio.h>

typedef int ElementType;
#define NotFound (-1)

int BinarySearch(const ElementType A[], ElementType X, int N) {
    int Low, Mid, High;

    Low = 0;
    High = N - 1;
    
    while (Low <= High) {
        Mid = (Low + High) / 2;
        
        if (A[Mid] < X) {
            Low = Mid + 1;
        } else if (A[Mid] > X) {
            High = Mid - 1;
        } else {
            return Mid;  // Found
        }
    }

    return NotFound;  // NotFound is defined as -1
}

int main() {
    static int A[] = {1, 3, 5, 7, 9, 13, 15};
    const int SizeofA = sizeof(A) / sizeof(A[0]);
    int i;

    for (i = 0; i < 20; i++) {
        printf("BinarySearch of %d returns %d\n", i, BinarySearch(A, i, SizeofA));
    }

    return 0;
}

这个算法是二分查找算法,用于在已经排好序的数组中查找目标元素。下面是算法的主要步骤:

  1. 初始化 Low 和 High 分别为数组的第一个和最后一个元素的索引。
  2. 在 while 循环中,不断缩小搜索范围,直到 Low 大于 High。
  3. 在循环内部,计算中间元素的索引 Mid。
  4. 如果目标元素 X 大于 Mid 所指向的元素,则将 Low 更新为 Mid + 1,即将搜索范围缩小为右半部分。
  5. 如果目标元素 X 小于 Mid 所指向的元素,则将 High 更新为 Mid - 1,即将搜索范围缩小为左半部分。
  6. 如果目标元素 X 等于 Mid 所指向的元素,则找到了目标元素,返回 Mid。
  7. 如果循环结束时还未找到目标元素,则返回 NotFound。

这个算法的时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),因为每次迭代都将搜索范围缩小一半,直到找到目标元素或搜索范围为空。

2. 欧几里得算法

Euclid’s Algorithm:

A second example is Euclid’s algorithm for computing the greatest common divisor. The greatest common divisor (gcd) of two integers is the largest integer that divides both. Thus, gcd (50, 15) = 5

第二个例子是欧几里得算法,用于计算最大公约数。两个整数的最大公约数(gcd)是能够同时整除它们的最大整数。因此,gcd(50, 15) = 5。

#include <stdio.h>

/* START: fig2_10.txt */
        unsigned int
        Gcd( unsigned int M, unsigned int N )
        {
            unsigned int Rem;

/* 1*/      while( N > 0 )
            {
/* 2*/          Rem = M % N;
/* 3*/          M = N;
/* 4*/          N = Rem;
            }
/* 5*/      return M;
        }
/* END */

int main( )
{
    printf( "Gcd( 45, 35 ) = %d\n", Gcd( 45, 35 ) );
    printf( "Gcd( 1989, 1590 ) = %d\n", Gcd( 1989, 1590 ) );
    return 0;
}

欧几里得算法的运行时间取决于余数序列的长度。尽管 log n 似乎是一个很好的答案,但并不明显余数的值必须以常数因子减小,因为我们看到在示例中,余数从 399 减少到了 393。实际上,在一次迭代中,余数并不以常数因子减小。但是,我们可以证明,在两次迭代后,余数至多是其原始值的一半。这将表明迭代次数至多为 2 log n = O(log n)

这也是负责对数特性的算法(第一次迭代虽然没有减半但是后续会将问题规模减半,也就是说不会完全遵守常数因子减小)。

3. 幂运算

Efficient exponentiation:(高效幂运算)

#include <stdio.h>

#define IsEven( N ) ( ( N ) % 2 == 0 )

/* START: fig2_11.txt */
        long int
        Pow( long int X, unsigned int N )
        {
/* 1*/      if( N == 0 )
/* 2*/          return 1;
/* 3*/      if( N == 1 )
/* 4*/          return X;
/* 5*/      if( IsEven( N ) )
/* 6*/          return Pow( X * X, N / 2 );
            else
/* 7*/          return Pow( X * X, N / 2 ) * X;
        }
/* END */

int main( )
{
    printf( "2^21 = %ld\n", Pow( 2, 21 ) );
    return 0;
}

这段代码实现了一个函数 Pow,用于计算一个整数的幂。算法采用了分治法,利用了指数的奇偶性质来优化计算。

  1. 如果指数 N 等于 0,则返回 1。
  2. 如果指数 N 等于 1,则返回底数 X
  3. 如果指数 N 是偶数,则递归计算 XN/2 次幂的平方。
  4. 如果指数 N 是奇数,则递归计算 XN/2 次幂的平方,然后再乘以 X 一次。

main 函数中,我们使用示例来计算 2 的 21 次幂,并打印结果。

The straightforward algorithm for exponentiation would require about 10200 multiplications, whereas the algorithm presented requires only about 1,200.

🟫小结

终于结束了这章的学习,本次学习的收获率仅有30%,数学公式太狠了,而且似乎耐心也不足够我去细细推到,很多地方用了AI来偷懒。尽管这样敷衍我也想尽量完成这个系列,起码通过这种方式见识见识都有怎么样的算法。还是那句话对于现在的我来说,完成比完美更重要。

Peace and Love


🌐参考链接


  1. 这个地方可以参考一个博客讲述的更细致。两种技能增长曲线:对数增长曲线(先快后慢)和指数增长曲线(先慢后快) ↩︎

  • 24
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值