WikiMili The Free Encyclopedia

This article includes a list of references, but its sources remain unclear because it has insufficient inline citations .(March 2010) (Learn how and when to remove this template message) |

In computer science, the **analysis of algorithms** is the process of finding the computational complexity of algorithms – the amount of time, storage, or other resources needed to execute them. Usually, this involves determining a function that relates the length of an algorithm's input to the number of steps it takes (its time complexity) or the number of storage locations it uses (its space complexity). An algorithm is said to be efficient when this function's values are small, or grow slowly compared to a growth in the size of the input. Different inputs of the same length may cause the algorithm to have different behavior, so best, worst and average case descriptions might all be of practical interest. When not otherwise specified, the function describing the performance of an algorithm is usually an upper bound, determined from the worst case inputs to the algorithm.

**Computer science** is the study of processes that interact with data and that can be represented as data in the form of programs. It enables the use of algorithms to manipulate, store, and communicate digital information. A computer scientist studies the theory of computation and the practice of designing software systems.

In computer science, the **computational complexity**, or simply **complexity** of an algorithm is the amount of resources required for running it. The computational complexity of a problem is the minimum of the complexities of all possible algorithms for this problem.

In mathematics, a **function** is a relation between sets that associates to every element of a first set exactly one element of the second set. Typical examples are functions from integers to integers or from the real numbers to real numbers.

- Cost models
- Run-time analysis
- Shortcomings of empirical metrics
- Orders of growth
- Empirical orders of growth
- Evaluating run-time complexity
- Growth rate analysis of other resources
- Relevance
- Constant factors
- See also
- Notes
- References

The term "analysis of algorithms" was coined by Donald Knuth.^{ [1] } Algorithm analysis is an important part of a broader computational complexity theory, which provides theoretical estimates for the resources needed by any algorithm which solves a given computational problem. These estimates provide an insight into reasonable directions of search for efficient algorithms.

**Donald Ervin Knuth** is an American computer scientist, mathematician, and professor emeritus at Stanford University. He is the 1974 recipient of the ACM Turing Award, informally considered the Nobel Prize of computer science.

**Computational complexity theory** focuses on classifying computational problems according to their inherent difficulty, and relating these classes to each other. A computational problem is a task solved by a computer. A computation problem is solvable by mechanical application of mathematical steps, such as an algorithm.

In theoretical computer science, a **computational problem** is a mathematical object representing a collection of questions that computers might be able to solve. For example, the problem of **factoring**

In theoretical analysis of algorithms it is common to estimate their complexity in the asymptotic sense, i.e., to estimate the complexity function for arbitrarily large input. Big O notation, Big-omega notation and Big-theta notation are used to this end. For instance, binary search is said to run in a number of steps proportional to the logarithm of the length of the sorted list being searched, or in O(log(n)), colloquially "in logarithmic time". Usually asymptotic estimates are used because different implementations of the same algorithm may differ in efficiency. However the efficiencies of any two "reasonable" implementations of a given algorithm are related by a constant multiplicative factor called a *hidden constant*.

**Big O notation** is a mathematical notation that describes the limiting behavior of a function when the argument tends towards a particular value or infinity. It is a member of a family of notations invented by Paul Bachmann, Edmund Landau, and others, collectively called **Bachmann–Landau notation** or **asymptotic notation**.

In mathematical analysis, **asymptotic analysis**, also known as **asymptotics**, is a method of describing limiting behavior.

**Implementation** is the realization of an application, or execution of a plan, idea, model, design, specification, standard, algorithm, or policy.

Exact (not asymptotic) measures of efficiency can sometimes be computed but they usually require certain assumptions concerning the particular implementation of the algorithm, called model of computation. A model of computation may be defined in terms of an abstract computer, e.g., Turing machine, and/or by postulating that certain operations are executed in unit time. For example, if the sorted list to which we apply binary search has *n* elements, and we can guarantee that each lookup of an element in the list can be done in unit time, then at most log_{2}*n* + 1 time units are needed to return an answer.

In computer science, and more specifically in computability theory and computational complexity theory, a **model of computation** is a model which describes how an output of a mathematical function is computed given an input. A model describes how units of computations, memories, and communications are organized. The computational complexity of an algorithm can be measured given a model of computation. Using a model allows studying the performance of algorithms independently of the variations that are specific to particular implementations and specific technology.

An **abstract machine**, also called an **abstract computer**, is a theoretical model of a computer hardware or software system used in automata theory. Abstraction of computing processes is used in both the computer science and computer engineering disciplines and usually assumes a discrete time paradigm.

A **Turing machine** is a mathematical model of computation that defines an abstract machine, which manipulates symbols on a strip of tape according to a table of rules. Despite the model's simplicity, given any computer algorithm, a Turing machine capable of simulating that algorithm's logic can be constructed.

Time efficiency estimates depend on what we define to be a step. For the analysis to correspond usefully to the actual execution time, the time required to perform a step must be guaranteed to be bounded above by a constant. One must be careful here; for instance, some analyses count an addition of two numbers as one step. This assumption may not be warranted in certain contexts. For example, if the numbers involved in a computation may be arbitrarily large, the time required by a single addition can no longer be assumed to be constant.

Two cost models are generally used:^{ [2] }^{ [3] }^{ [4] }^{ [5] }^{ [6] }

- the
**uniform cost model**, also called**uniform-cost measurement**(and similar variations), assigns a constant cost to every machine operation, regardless of the size of the numbers involved - the
**logarithmic cost model**, also called**logarithmic-cost measurement**(and similar variations), assigns a cost to every machine operation proportional to the number of bits involved

The latter is more cumbersome to use, so it's only employed when necessary, for example in the analysis of arbitrary-precision arithmetic algorithms, like those used in cryptography.

In computer science, **arbitrary-precision arithmetic**, also called **bignum arithmetic**, multiple-precision arithmetic, or sometimes **infinite-precision arithmetic**, indicates that calculations are performed on numbers whose digits of precision are limited only by the available memory of the host system. This contrasts with the faster fixed-precision arithmetic found in most arithmetic logic unit (ALU) hardware, which typically offers between 8 and 64 bits of precision.

**Cryptography** or **cryptology** is the practice and study of techniques for secure communication in the presence of third parties called adversaries. More generally, cryptography is about constructing and analyzing protocols that prevent third parties or the public from reading private messages; various aspects in information security such as data confidentiality, data integrity, authentication, and non-repudiation are central to modern cryptography. Modern cryptography exists at the intersection of the disciplines of mathematics, computer science, electrical engineering, communication science, and physics. Applications of cryptography include electronic commerce, chip-based payment cards, digital currencies, computer passwords, and military communications.

A key point which is often overlooked is that published lower bounds for problems are often given for a model of computation that is more restricted than the set of operations that you could use in practice and therefore there are algorithms that are faster than what would naively be thought possible.^{ [7] }

Run-time analysis is a theoretical classification that estimates and anticipates the increase in * running time * (or run-time) of an algorithm as its * input size * (usually denoted as *n*) increases. Run-time efficiency is a topic of great interest in computer science: A program can take seconds, hours, or even years to finish executing, depending on which algorithm it implements. While software profiling techniques can be used to measure an algorithm's run-time in practice, they cannot provide timing data for all infinitely many possible inputs; the latter can only be achieved by the theoretical methods of run-time analysis.

Since algorithms are platform-independent (i.e. a given algorithm can be implemented in an arbitrary programming language on an arbitrary computer running an arbitrary operating system), there are additional significant drawbacks to using an empirical approach to gauge the comparative performance of a given set of algorithms.

Take as an example a program that looks up a specific entry in a sorted list of size *n*. Suppose this program were implemented on Computer A, a state-of-the-art machine, using a linear search algorithm, and on Computer B, a much slower machine, using a binary search algorithm. Benchmark testing on the two computers running their respective programs might look something like the following:

n (list size) | Computer A run-time (in nanoseconds) | Computer B run-time (in nanoseconds) |
---|---|---|

16 | 8 | 100,000 |

63 | 32 | 150,000 |

250 | 125 | 200,000 |

1,000 | 500 | 250,000 |

Based on these metrics, it would be easy to jump to the conclusion that *Computer A* is running an algorithm that is far superior in efficiency to that of *Computer B*. However, if the size of the input-list is increased to a sufficient number, that conclusion is dramatically demonstrated to be in error:

n (list size) | Computer A run-time (in nanoseconds) | Computer B run-time (in nanoseconds) |
---|---|---|

16 | 8 | 100,000 |

63 | 32 | 150,000 |

250 | 125 | 200,000 |

1,000 | 500 | 250,000 |

... | ... | ... |

1,000,000 | 500,000 | 500,000 |

4,000,000 | 2,000,000 | 550,000 |

16,000,000 | 8,000,000 | 600,000 |

... | ... | ... |

63,072 × 10^{12} | 31,536 × 10^{12} ns,or 1 year | 1,375,000 ns, or 1.375 milliseconds |

Computer A, running the linear search program, exhibits a linear growth rate. The program's run-time is directly proportional to its input size. Doubling the input size doubles the run time, quadrupling the input size quadruples the run-time, and so forth. On the other hand, Computer B, running the binary search program, exhibits a logarithmic growth rate. Quadrupling the input size only increases the run time by a constant amount (in this example, 50,000 ns). Even though Computer A is ostensibly a faster machine, Computer B will inevitably surpass Computer A in run-time because it's running an algorithm with a much slower growth rate.

Informally, an algorithm can be said to exhibit a growth rate on the order of a mathematical function if beyond a certain input size *n*, the function times a positive constant provides an upper bound or limit for the run-time of that algorithm. In other words, for a given input size *n* greater than some *n*_{0} and a constant *c*, the running time of that algorithm will never be larger than . This concept is frequently expressed using Big O notation. For example, since the run-time of insertion sort grows quadratically as its input size increases, insertion sort can be said to be of order *O*(*n*^{2}).

Big O notation is a convenient way to express the worst-case scenario for a given algorithm, although it can also be used to express the average-case — for example, the worst-case scenario for quicksort is *O*(*n*^{2}), but the average-case run-time is *O*(*n* log *n*).

Assuming the execution time follows power rule, *t ≈ k n ^{a}*, the coefficient

n (list size) | Computer A run-time (in nanoseconds) | Local order of growth (n^_) | Computer B run-time (in nanoseconds) | Local order of growth (n^_) |
---|---|---|---|---|

15 | 7 | 100,000 | ||

65 | 32 | 1.04 | 150,000 | 0.28 |

250 | 125 | 1.01 | 200,000 | 0.21 |

1,000 | 500 | 1.00 | 250,000 | 0.16 |

... | ... | ... | ||

1,000,000 | 500,000 | 1.00 | 500,000 | 0.10 |

4,000,000 | 2,000,000 | 1.00 | 550,000 | 0.07 |

16,000,000 | 8,000,000 | 1.00 | 600,000 | 0.06 |

... | ... | ... |

It is clearly seen that the first algorithm exhibits a linear order of growth indeed following the power rule. The empirical values for the second one are diminishing rapidly, suggesting it follows another rule of growth and in any case has much lower local orders of growth (and improving further still), empirically, than the first one.

The run-time complexity for the worst-case scenario of a given algorithm can sometimes be evaluated by examining the structure of the algorithm and making some simplifying assumptions. Consider the following pseudocode:

1get a positive integer from input2ifn > 10 3fori = 1ton 5forj = 1toi 6

A given computer will take a discrete amount of time to execute each of the instructions involved with carrying out this algorithm. The specific amount of time to carry out a given instruction will vary depending on which instruction is being executed and which computer is executing it, but on a conventional computer, this amount will be deterministic.^{ [9] } Say that the actions carried out in step 1 are considered to consume time *T*_{1}, step 2 uses time *T*_{2}, and so forth.

In the algorithm above, steps 1, 2 and 7 will only be run once. For a worst-case evaluation, it should be assumed that step 3 will be run as well. Thus the total amount of time to run steps 1-3 and step 7 is:

The loops in steps 4, 5 and 6 are trickier to evaluate. The outer loop test in step 4 will execute ( *n* + 1 ) times (note that an extra step is required to terminate the for loop, hence n + 1 and not n executions), which will consume *T*_{4}( *n* + 1 ) time. The inner loop, on the other hand, is governed by the value of j, which iterates from 1 to *i*. On the first pass through the outer loop, j iterates from 1 to 1: The inner loop makes one pass, so running the inner loop body (step 6) consumes *T*_{6} time, and the inner loop test (step 5) consumes 2*T*_{5} time. During the next pass through the outer loop, j iterates from 1 to 2: the inner loop makes two passes, so running the inner loop body (step 6) consumes 2*T*_{6} time, and the inner loop test (step 5) consumes 3*T*_{5} time.

Altogether, the total time required to run the inner loop body can be expressed as an arithmetic progression:

which can be factored ^{ [10] } as

The total time required to run the outer loop test can be evaluated similarly:

which can be factored as

Therefore, the total running time for this algorithm is:

which reduces to

As a rule-of-thumb, one can assume that the highest-order term in any given function dominates its rate of growth and thus defines its run-time order. In this example, n^{2} is the highest-order term, so one can conclude that f(n) = O(n^{2}). Formally this can be proven as follows:

Prove that

Letkbe a constant greater than or equal to [T_{1}..T_{7}]

Therefore

A more elegant approach to analyzing this algorithm would be to declare that [*T*_{1}..*T*_{7}] are all equal to one unit of time, in a system of units chosen so that one unit is greater than or equal to the actual times for these steps. This would mean that the algorithm's running time breaks down as follows:^{ [11] }

The methodology of run-time analysis can also be utilized for predicting other growth rates, such as consumption of memory space. As an example, consider the following pseudocode which manages and reallocates memory usage by a program based on the size of a file which that program manages:

whilefile is still open:letn =size of fileforevery 100,000 kilobytes of increase in file sizedouble the amount of memory reserved

In this instance, as the file size n increases, memory will be consumed at an exponential growth rate, which is order O(2^{n}). This is an extremely rapid and most likely unmanageable growth rate for consumption of memory resources.

Algorithm analysis is important in practice because the accidental or unintentional use of an inefficient algorithm can significantly impact system performance. In time-sensitive applications, an algorithm taking too long to run can render its results outdated or useless. An inefficient algorithm can also end up requiring an uneconomical amount of computing power or storage in order to run, again rendering it practically useless.

Analysis of algorithms typically focuses on the asymptotic performance, particularly at the elementary level, but in practical applications constant factors are important, and real-world data is in practice always limited in size. The limit is typically the size of addressable memory, so on 32-bit machines 2^{32} = 4 GiB (greater if segmented memory is used) and on 64-bit machines 2^{64} = 16 EiB. Thus given a limited size, an order of growth (time or space) can be replaced by a constant factor, and in this sense all practical algorithms are O(1) for a large enough constant, or for small enough data.

This interpretation is primarily useful for functions that grow extremely slowly: (binary) iterated logarithm (log^{*}) is less than 5 for all practical data (2^{65536} bits); (binary) log-log (log log *n*) is less than 6 for virtually all practical data (2^{64} bits); and binary log (log *n*) is less than 64 for virtually all practical data (2^{64} bits). An algorithm with non-constant complexity may nonetheless be more efficient than an algorithm with constant complexity on practical data if the overhead of the constant time algorithm results in a larger constant factor, e.g., one may have so long as and .

For large data linear or quadratic factors cannot be ignored, but for small data an asymptotically inefficient algorithm may be more efficient. This is particularly used in hybrid algorithms, like Timsort, which use an asymptotically efficient algorithm (here merge sort, with time complexity ), but switch to an asymptotically inefficient algorithm (here insertion sort, with time complexity ) for small data, as the simpler algorithm is faster on small data.

- Amortized analysis
- Analysis of parallel algorithms
- Asymptotic computational complexity
- Best, worst and average case
- Big O notation
- Computational complexity theory
- Master theorem (analysis of algorithms)
- NP-Complete
- Numerical analysis
- Polynomial time
- Program optimization
- Profiling (computer programming)
- Scalability
- Smoothed analysis
- Termination analysis — the subproblem of checking whether a program will terminate at all
- Time complexity — includes table of orders of growth for common algorithms
- Information-based complexity

- ↑ "Knuth: Recent News".
*web.archive.org*. 28 August 2016. - ↑ Alfred V. Aho; John E. Hopcroft; Jeffrey D. Ullman (1974).
*The design and analysis of computer algorithms*. Addison-Wesley Pub. Co., section 1.3 - ↑ Juraj Hromkovič (2004).
*Theoretical computer science: introduction to Automata, computability, complexity, algorithmics, randomization, communication, and cryptography*. Springer. pp. 177–178. ISBN 978-3-540-14015-3. - ↑ Giorgio Ausiello (1999).
*Complexity and approximation: combinatorial optimization problems and their approximability properties*. Springer. pp. 3–8. ISBN 978-3-540-65431-5. - ↑ Wegener, Ingo (2005),
*Complexity theory: exploring the limits of efficient algorithms*, Berlin, New York: Springer-Verlag, p. 20, ISBN 978-3-540-21045-0 - ↑ Robert Endre Tarjan (1983).
*Data structures and network algorithms*. SIAM. pp. 3–7. ISBN 978-0-89871-187-5. - ↑ Examples of the price of abstraction?, cstheory.stackexchange.com
- ↑ How To Avoid O-Abuse and Bribes, at the blog "Gödel's Lost Letter and P=NP" by R. J. Lipton, professor of Computer Science at Georgia Tech, recounting idea by Robert Sedgewick
- ↑ However, this is not the case with a quantum computer
- ↑ It can be proven by induction that
- ↑ This approach, unlike the above approach, neglects the constant time consumed by the loop tests which terminate their respective loops, but it is trivial to prove that such omission does not affect the final result

In computer science, **binary search**, also known as **half-interval search**, **logarithmic search**, or **binary chop**, is a search algorithm that finds the position of a target value within a sorted array. Binary search compares the target value to the middle element of the array. If they are not equal, the half in which the target cannot lie is eliminated and the search continues on the remaining half, again taking the middle element to compare to the target value, and repeating this until the target value is found. If the search ends with the remaining half being empty, the target is not in the array.

A **fast Fourier transform** (**FFT**) is an algorithm that computes the discrete Fourier transform (DFT) of a sequence, or its inverse (IDFT). Fourier analysis converts a signal from its original domain to a representation in the frequency domain and vice versa. The DFT is obtained by decomposing a sequence of values into components of different frequencies. This operation is useful in many fields, but computing it directly from the definition is often too slow to be practical. An FFT rapidly computes such transformations by factorizing the DFT matrix into a product of sparse factors. As a result, it manages to reduce the complexity of computing the DFT from , which arises if one simply applies the definition of DFT, to , where is the data size. The difference in speed can be enormous, especially for long data sets where *N* may be in the thousands or millions. In the presence of round-off error, many FFT algorithms are much more accurate than evaluating the DFT definition directly. There are many different FFT algorithms based on a wide range of published theories, from simple complex-number arithmetic to group theory and number theory.

In computer science, a **sorting algorithm** is an algorithm that puts elements of a list in a certain order. The most frequently used orders are numerical order and lexicographical order. Efficient sorting is important for optimizing the efficiency of other algorithms that require input data to be in sorted lists. Sorting is also often useful for canonicalizing data and for producing human-readable output. More formally, the output of any sorting algorithm must satisfy two conditions:

- The output is in nondecreasing order ;
- The output is a permutation of the input.

**Shellsort**, also known as **Shell sort** or **Shell's method**, is an in-place comparison sort. It can be seen as either a generalization of sorting by exchange or sorting by insertion. The method starts by sorting pairs of elements far apart from each other, then progressively reducing the gap between elements to be compared. Starting with far apart elements, it can move some out-of-place elements into position faster than a simple nearest neighbor exchange. Donald Shell published the first version of this sort in 1959. The running time of Shellsort is heavily dependent on the gap sequence it uses. For many practical variants, determining their time complexity remains an open problem.

**Exponential growth** is a specific way that a quantity may increase over time. It occurs when the instantaneous rate of change of a quantity with respect to time is proportional to the quantity itself. Described as a function, a quantity undergoing exponential growth is an exponential function of time, that is, the variable representing time is the exponent.

In computer science, the **Akra–Bazzi method**, or **Akra–Bazzi theorem**, is used to analyze the asymptotic behavior of the mathematical recurrences that appear in the analysis of divide and conquer algorithms where the sub-problems have substantially different sizes. It is a generalization of the master theorem for divide-and-conquer recurrences, which assumes that the sub-problems have equal size. It is named after mathematicians Mohamad Akra and Louay Bazzi.

In computer science, the **time complexity** is the computational complexity that describes the amount of time it takes to run an algorithm. Time complexity is commonly estimated by counting the number of elementary operations performed by the algorithm, supposing that each elementary operation takes a fixed amount of time to perform. Thus, the amount of time taken and the number of elementary operations performed by the algorithm are taken to differ by at most a constant factor.

A **randomized algorithm** is an algorithm that employs a degree of randomness as part of its logic. The algorithm typically uses uniformly random bits as an auxiliary input to guide its behavior, in the hope of achieving good performance in the "average case" over all possible choices of random bits. Formally, the algorithm's performance will be a random variable determined by the random bits; thus either the running time, or the output are random variables.

In the analysis of algorithms, the **master theorem for divide-and-conquer recurrences** provides an asymptotic analysis for recurrence relations of types that occur in the analysis of many divide and conquer algorithms. The approach was first presented by Jon Bentley, Dorothea Haken, and James B. Saxe in 1980, where it was described as a "unifying method" for solving such recurrences. The name "master theorem" was popularized by the widely used algorithms textbook *Introduction to Algorithms* by Cormen, Leiserson, Rivest, and Stein.

In computer science, **parameterized complexity** is a branch of computational complexity theory that focuses on classifying computational problems according to their inherent difficulty with respect to *multiple* parameters of the input or output. The complexity of a problem is then measured as a function of those parameters. This allows the classification of NP-hard problems on a finer scale than in the classical setting, where the complexity of a problem is only measured by the number of bits in the input. The first systematic work on parameterized complexity was done by Downey & Fellows (1999).

The **Euclidean minimum spanning tree** or **EMST** is a minimum spanning tree of a set of *n* points in the plane, where the weight of the edge between each pair of points is the Euclidean distance between those two points. In simpler terms, an EMST connects a set of dots using lines such that the total length of all the lines is minimized and any dot can be reached from any other by following the lines.

A **comparison sort** is a type of sorting algorithm that only reads the list elements through a single abstract comparison operation that determines which of two elements should occur first in the final sorted list. The only requirement is that the operator forms a total preorder over the data, with:

- if
*a*≤*b*and*b*≤*c*then*a*≤*c*(transitivity) - for all
*a*and*b*,*a*≤*b*or*b*≤*a*(connexity).

**Quicksort** is an efficient sorting algorithm, serving as a systematic method for placing the elements of a random access file or an array in order. Developed by British computer scientist Tony Hoare in 1959 and published in 1961, it is still a commonly used algorithm for sorting. When implemented well, it can be about two or three times faster than its main competitors, merge sort and heapsort.

In computer science, an algorithm is said to be **asymptotically optimal** if, roughly speaking, for large inputs it performs at worst a constant factor worse than the best possible algorithm. It is a term commonly encountered in computer science research as a result of widespread use of big-O notation.

A **double exponential** function is a constant raised to the power of an exponential function. The general formula is (where *a*>1 and *b*>1), which grows much more quickly than an exponential function. For example, if *a* = *b* = 10:

Because matrix multiplication is such a central operation in many numerical algorithms, much work has been invested in making **matrix multiplication algorithms** efficient. Applications of matrix multiplication in computational problems are found in many fields including scientific computing and pattern recognition and in seemingly unrelated problems such as counting the paths through a graph. Many different algorithms have been designed for multiplying matrices on different types of hardware, including parallel and distributed systems, where the computational work is spread over multiple processors.

In computer science, the **median of medians** is an approximate (median) selection algorithm, frequently used to supply a good pivot for an exact selection algorithm, mainly the quickselect, that selects the *k*th largest element of an initially unsorted array. Median of medians finds an approximate median in linear time only, which is limited but an additional overhead for quickselect. When this approximate median is used as an improved pivot, the worst-case complexity of quickselect reduces significantly from quadratic to *linear*, which is also the asymptotically optimal worst-case complexity of any selection algorithm. In other words, the median of medians is an approximate median-selection algorithm that helps building an asymptotically optimal, exact general selection algorithm, by producing good pivot elements.

- Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L. & Stein, Clifford (2001).
*Introduction to Algorithms*. Chapter 1: Foundations (Second ed.). Cambridge, MA: MIT Press and McGraw-Hill. pp. 3–122. ISBN 0-262-03293-7. - Sedgewick, Robert (1998).
*Algorithms in C, Parts 1-4: Fundamentals, Data Structures, Sorting, Searching*(3rd ed.). Reading, MA: Addison-Wesley Professional. ISBN 978-0-201-31452-6. - Knuth, Donald.
*The Art of Computer Programming*. Addison-Wesley. - Greene, Daniel A.; Knuth, Donald E. (1982).
*Mathematics for the Analysis of Algorithms*(Second ed.). Birkhäuser. ISBN 3-7643-3102-X. - Goldreich, Oded (2010).
*Computational Complexity: A Conceptual Perspective*. Cambridge University Press. ISBN 978-0-521-88473-0.

This page is based on this Wikipedia article

Text is available under the CC BY-SA 4.0 license; additional terms may apply.

Images, videos and audio are available under their respective licenses.

Text is available under the CC BY-SA 4.0 license; additional terms may apply.

Images, videos and audio are available under their respective licenses.