Skip to content

Taylor series and Horner’s rule

A few terms of a Taylor series make a polynomial, and Horner’s nesting evaluates it with one multiplication and one addition per term, by recursion or by a loop.

Helps to know firstRecursion

Helps to know first
This topic

First, the picture

How does a computer work out cos 1? It has no circle to measure. What it can do is add and multiply, and cos x can be written, as closely as we like, using only those:

cos⁡x≈1−x22!+x44!−x66!+x88!−x1010!.\cos x \approx 1 - \frac{x^2}{2!} + \frac{x^4}{4!} - \frac{x^6}{6!} + \frac{x^8}{8!} - \frac{x^{10}}{10!}.

That is the start of its Taylor series; the sum goes on for ever, and we keep the first six terms. The question for this page is how to add them up with as little work as possible. The instrument does it three ways and counts the multiplications.

Building cosine from a few terms

The first terms of cos x, evaluated from the inside out

static const double coef[6] = { 1.0, -1.0 / 2, 1.0 / 24, -1.0 / 720, 1.0 / 40320, -1.0 / 3628800 }; double horner_rec(int k, int n, double u){    if (k == n - 1)        return coef[k];    double rest = horner_rec(k + 1, n, u);    mults++;    return coef[k] + u * rest;} double horner_loop(int n, double u){    double r = coef[n - 1];    for (int k = n - 2; k >= 0; k--) {        r = r * u + coef[k];        mults++;    }    return r;}int main(void){    double x = 1.0;    double u = x * x;    double r = horner_rec(0, TERMS, u);    printf("recursive: %.10f in %d multiplications\n", r, mults);    mults = 0;    double l = horner_loop(TERMS, u);    printf("loop:      %.10f in %d multiplications\n", l, mults);    mults = 0;    double s = cos_naive(x, TERMS);    printf("naive:     %.10f in %d multiplications\n", s, mults);    return 0;}

cos x from its first 6 terms, at x = 1.

multiplications: recursive
0
loop
0
naive
0
6

Output

0.00 / 29.80 s
Describe this picture

Lines 9 to 28 and 48 to 61 of cosine.c (the coefficients, horner_rec, horner_loop and main) with the line about to run marked, beside a picture in two parts. At the top, under the words “Brackets: each = a + u × the one below”, is a column of six rows, one per term, the outermost bracket at the top. Row k reads “a0 = 1”, “a1 = −1/2”, “a2 = 1/24”, “a3 = −1/720”, “a4 = 1/40320” and “a5 = −1/3628800”, with ”+ u ×” after it on every row but the last, and on the right that bracket’s value, a ”?” until it is worked out. The row being worked on is edged in the accent colour. Under the column is a plot from −2π to 2π across and −1.5 to 1.5 up: a thin muted curve for cos x, and a thick accent curve for the approximation with the chosen number of terms, labelled “cos x, and 6 terms”. The clip plays once, about 30 seconds. The calls of horner_rec go down the rows, 0 to 5. Then the values fill in from the bottom up: a5 first, −0.0000002756, then 0.0000245260, −0.0013643629, 0.0403023038, −0.4596976962 and, at the top, 0.5403023038. A dot then appears on the curve at x = 1. Then horner_loop works through the same rows, bottom to top, and cos_naive adds its six terms one by one. Under the picture are one sentence on the current step and three readouts, “multiplications: recursive”, “loop” and “naive”, which end at 5, 5 and 60. Then a slider, “terms”, from 1 to 6, set to 6, and the output: three lines, each ending in 0.5403023038. Moving the slider shows the run with that many terms already finished: fewer rows, a new curve, and new counts. With 1 or 2 terms the curve is a flat line or a parabola that leaves the plot; with 6 it lies on cos x from about −3 to 3 and peels away beyond that. The transport plays and pauses, steps back or forward, and scrubs; the number of terms and a paused step are kept in the page address as horner.terms and horner.step.

A polynomial, nested

Every term has an even power of x. So write u for x², and the six terms become a polynomial in u:

cos⁡x≈a0+a1u+a2u2+a3u3+a4u4+a5u5,ak=(−1)k(2k)!.\cos x \approx a_0 + a_1 u + a_2 u^2 + a_3 u^3 + a_4 u^4 + a_5 u^5, \qquad a_k = \frac{(-1)^k}{(2k)!}.

So a₀ = 1, a₁ = −1/2, a₂ = 1/24, a₃ = −1/720, a₄ = 1/40320, a₅ = −1/3628800. These are the six numbers in the program’s array coef.

Now take u out of every term after the first, then again, and again:

a0+u(a1+u(a2+u(a3+u(a4+u a5)))).a_0 + u\Big(a_1 + u\big(a_2 + u(a_3 + u(a_4 + u\, a_5))\big)\Big).

This is Horner’s rule. To evaluate it, start in the innermost bracket and work outwards. Each bracket is its own coefficient plus u times the bracket inside it: one multiplication and one addition. Watch the column in the clip fill from the bottom: the innermost bracket is just a₅, and every row above uses the value of the row below. At x = 1, so u = 1, the top row ends at 0.5403023038.

Key idea

Horner’s rule evaluates a polynomial from the inside out: start with the last coefficient, then repeatedly multiply by u and add the next coefficient. Each term costs one multiplication and one addition.

The code and its cost

By recursion

Each bracket is “my coefficient plus u times the rest”, and the rest is the same kind of thing, one bracket further in. That is a recursive function:

double horner_rec(int k, int n, double u)
{
    if (k == n - 1)
        return coef[k];
    double rest = horner_rec(k + 1, n, u);
    mults++;
    return coef[k] + u * rest;
}

horner_rec(k, n, u) evaluates the brackets from aka_k inwards. The base case is the innermost bracket, an−1a_{n-1} alone. main calls horner_rec(0, 6, u). As in Recursion (4.1), the calls go down first, and the work happens on the way back, which is exactly the inside-out order Horner’s rule needs.

By a loop

The work on the way back is the same at every step, so a loop can do it from the inside out directly:

double horner_loop(int n, double u)
{
    double r = coef[n - 1];
    for (int k = n - 2; k >= 0; k--) {
        r = r * u + coef[k];
        mults++;
    }
    return r;
}

r starts at the innermost coefficient and passes through the same values as the column: −0.0000002756, 0.0000245260, and so on up to 0.5403023038. It uses one frame instead of six.

Counting the multiplications

With n terms, both Horner versions make n − 1 multiplications: one for each bracket but the innermost. Six terms: 5. (Squaring x to get u is one more, done once in main; the program does not count it.)

The naive way, cos_naive in the program, builds each term from scratch: for term k it multiplies x by itself 2k times for the power and multiplies 1 × 2 × … × 2k for the factorial, then divides. That is 4k multiplications for term k, and over n terms 4(0 + 1 + … + (n − 1)) = 2n(n − 1). Six terms: 60. Drag the slider and read the counts: Horner’s grow by one per term, the naive count grows with the square of n.

How good is it?

The six terms give 0.5403023038; the true cos 1 is 0.5403023059. The difference is about 2.1 × 10⁻⁹. The first term left out is 1/12! ≈ 2.09 × 10⁻⁹, nearly the same. That is no accident: once the terms of a series like this one, with alternating signs, are getting smaller, stopping early leaves an error no larger than the first term left out.

More terms do not make it good everywhere at once. Look at the curve with 6 terms: near 0 it lies on top of cos x, but towards ±2π it peels away, because x¹⁰ grows huge there. Drag down to 3 or 4 terms and watch the good region shrink towards 0. A maths library first moves x into a small range near 0, using cos’s repeats, and only then uses a few terms.

Running it yourself

gcc -std=c11 -Wall -Wextra -pedantic cosine.c -o cosine
./cosine
gcc -std=c11 -Wall -Wextra -pedantic -DTERMS=3 cosine.c -o cosine3
./cosine3

The first prints the three lines with 0.5403023038, in 5, 5 and 60 multiplications.

Worked example

Six terms at x = 1, by hand. With u = 1, each step is just “add the next coefficient”. Start inside:

BracketCoefficient× u, + coefficientValue
innermosta₅ = −1/3628800−0.0000002756
nexta₄ = 1/40320−0.0000002756 + 0.00002480160.0000245260
nexta₃ = −1/7200.0000245260 − 0.0013888889−0.0013643629
nexta₂ = 1/24−0.0013643629 + 0.04166666670.0403023038
nexta₁ = −1/20.0403023038 − 0.5−0.4596976962
outermosta₀ = 1−0.4596976962 + 10.5403023038

Before you read on: what does the program print with two terms? (0.5: the polynomial is 1 − u/2, and u = 1.)

Three terms at x = 0.5. Now u = 0.25, so 1 − 0.25/2 + 0.25²/24 = 1 − 0.125 + 0.0026041667 = 0.8776041667. The true cos 0.5 is 0.8775825619, an error of 2.2 × 10⁻⁵. The first term left out is 0.5⁶/720 ≈ 2.17 × 10⁻⁵: again the error is just under it.

A polynomial of degree 20. Horner’s rule takes 20 multiplications. Computing each power of u separately, u¹ needs none, u² one, and so on up to u²⁰, nineteen: 0 + 1 + … + 19 = 190, and then 20 more to multiply each power by its coefficient, 210 in all.

Where it’s used

Maths libraries. The cos, sin and exp in a C library compute a short polynomial for x near 0, evaluated by Horner’s rule, after moving x into that small range. The polynomial’s coefficients are chosen a little differently from the Taylor ones, to spread the error evenly, but the evaluation is the same nesting.

Reading numbers and hashing text. Turning the digits “4”, “0”, “3” into the number 403 is Horner’s rule with u = 10: ((4 × 10) + 0) × 10 + 3. String hashing (18.1) and CRC checksums use the same nesting over the characters of a text.

One DFT value. The Goertzel algorithm computes a single frequency of a signal by the same nesting, with the signal’s samples as the coefficients; see also Sinusoids.

Reference card

IdeaIn this lesson
cos x as a polynomiala₀ + a₁u + … + a₅u⁵, u = x², ak = (−1)k / (2k)!
Horner’s rulea₀ + u(a₁ + u(a₂ + …)), from the inside out
one stepvalue = coefficient + u × the value inside
recursive formthe rest first, then coef[k] + u × rest
loop formr = coef[n − 1]; then r = r × u + coef[k] for k down to 0
multiplicationsHorner n − 1; naive 2n(n − 1); 6 terms: 5 against 60
errorat most the first term left out, once the terms shrink
cos 10.5403023038 from six terms; 0.5403023059 exactly to 10 places

Platform: C11, gcc on x86-64 Linux, double 8 bytes; the program prints 10 decimal places.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look