## The Mastery of Computer Programming: Primary Algorithms - Sykalo Eugene 2023

# Asymptotic notation

Algorithmic Complexity and Information Theory

## Introduction to Asymptotic Notation

Asymptotic notation is a mathematical way of describing the performance of an algorithm in terms of its input size. It is a tool that allows us to compare different algorithms and determine which one is more efficient for a particular problem.

In computer programming, the performance of an algorithm is often measured in terms of its time complexity and space complexity. Time complexity refers to the amount of time an algorithm takes to complete as a function of the size of its input, while space complexity refers to the amount of memory an algorithm requires to execute.

Asymptotic notation provides a way of expressing the time and space complexity of an algorithm in terms of mathematical functions. The most commonly used forms of asymptotic notation are Big O, Omega, and Theta. These notations provide upper, lower, and tight bounds on the performance of an algorithm, respectively.

The main benefit of using asymptotic notation is that it allows us to compare the efficiency of different algorithms without having to analyze their exact performance for every possible input. Instead, we can simply analyze the performance of an algorithm as its input size grows to infinity.

## Big O Notation

Big O notation is the most commonly used form of asymptotic notation. It provides an upper bound on the performance of an algorithm, meaning that it gives us an estimate of the worst-case scenario for the algorithm's time complexity.

To use Big O notation, we first need to express the time complexity of an algorithm as a mathematical function of its input size, usually denoted by the symbol n. For example, if an algorithm takes 2n^2 + 3n + 4 operations to complete, we can express its time complexity as O(n^2).

The reason we can express the time complexity as O(n^2) is that as the input size grows to infinity, the leading term of the function (in this case, 2n^2) will dominate the overall performance of the algorithm. Thus, we can ignore the lower-order terms (3n + 4) and any constant factors (such as the 2 in front of the n^2 term) because they become insignificant compared to the dominant term as n grows larger.

In general, we express the time complexity of an algorithm using Big O notation as follows:

- O(1) for constant time algorithms, meaning that the algorithm takes the same amount of time to complete regardless of the size of its input.
- O(log n) for logarithmic time algorithms, meaning that the algorithm's time complexity grows logarithmically with the size of its input.
- O(n) for linear time algorithms, meaning that the algorithm's time complexity grows linearly with the size of its input.
- O(n log n) for algorithms with a time complexity that is proportional to n times the logarithm of n.
- O(n^2) for quadratic time algorithms, meaning that the algorithm's time complexity grows quadratically with the size of its input.
- O(2^n) for exponential time algorithms, meaning that the algorithm's time complexity grows exponentially with the size of its input.

By using Big O notation, we can easily compare the time complexity of different algorithms and determine which one is more efficient for a particular problem. However, it is important to note that Big O notation only provides an upper bound on the performance of an algorithm, meaning that it does not give us an exact measure of the algorithm's time complexity.

## Omega Notation

Omega notation, also known as "Big Omega" notation, is another form of asymptotic notation that is used to provide a lower bound on the performance of an algorithm. While Big O notation gives us an estimate of the worst-case scenario for an algorithm's time complexity, Omega notation gives us an estimate of the best-case scenario.

To use Omega notation, we again need to express the time complexity of an algorithm as a mathematical function of its input size, usually denoted by the symbol n. For example, if an algorithm takes n^2 + 3n + 4 operations to complete, we can express its time complexity as Ω(n^2).

The reason we can express the time complexity as Ω(n^2) is that as the input size grows to infinity, the leading term of the function (in this case, n^2) will dominate the overall performance of the algorithm. Thus, we can ignore the lower-order terms (3n + 4) and any constant factors because they become insignificant compared to the dominant term as n grows larger.

In general, we express the time complexity of an algorithm using Omega notation as follows:

- Ω(1) for constant time algorithms, meaning that the algorithm takes the same amount of time to complete regardless of the size of its input.
- Ω(log n) for logarithmic time algorithms, meaning that the algorithm's time complexity grows logarithmically with the size of its input.
- Ω(n) for linear time algorithms, meaning that the algorithm's time complexity grows linearly with the size of its input.
- Ω(n log n) for algorithms with a time complexity that is proportional to n times the logarithm of n.
- Ω(n^2) for quadratic time algorithms, meaning that the algorithm's time complexity grows quadratically with the size of its input.
- Ω(2^n) for exponential time algorithms, meaning that the algorithm's time complexity grows exponentially with the size of its input.

Using Omega notation, we can establish a lower bound on the performance of an algorithm. This is particularly useful when we want to prove that a particular algorithm cannot be significantly improved. For example, if we have an algorithm that we know has a lower bound of Ω(n^2), we can conclude that no other algorithm can have a better time complexity than Ω(n^2) for that particular problem.

In general, Omega notation is not as commonly used as Big O notation, but it can be a useful tool in certain situations. By using both Big O and Omega notation together, we can establish a range of possible time complexities for an algorithm, which can give us a more accurate picture of its performance.

## Theta Notation

Theta notation, also known as "Big Theta" notation, is a form of asymptotic notation that provides a tight bound on the performance of an algorithm. In other words, Theta notation gives us an estimate of the average-case scenario for an algorithm's time complexity.

To use Theta notation, we need to express the time complexity of an algorithm as a mathematical function of its input size, usually denoted by the symbol n. For example, if an algorithm takes 2n^2 + 3n + 4 operations to complete, we can express its time complexity as Θ(n^2).

The reason we can express the time complexity as Θ(n^2) is that as the input size grows to infinity, the leading term of the function (in this case, 2n^2) will dominate the overall performance of the algorithm. However, unlike Big O notation, Theta notation also takes into account the lower-order terms (3n + 4) and any constant factors (such as the 2 in front of the n^2 term) that may affect the algorithm's performance.

In general, we express the time complexity of an algorithm using Theta notation as follows:

- Θ(1) for constant time algorithms, meaning that the algorithm takes the same amount of time to complete regardless of the size of its input.
- Θ(log n) for logarithmic time algorithms, meaning that the algorithm's time complexity grows logarithmically with the size of its input.
- Θ(n) for linear time algorithms, meaning that the algorithm's time complexity grows linearly with the size of its input.
- Θ(n log n) for algorithms with a time complexity that is proportional to n times the logarithm of n.
- Θ(n^2) for quadratic time algorithms, meaning that the algorithm's time complexity grows quadratically with the size of its input.
- Θ(2^n) for exponential time algorithms, meaning that the algorithm's time complexity grows exponentially with the size of its input.

By using Theta notation, we can establish a tight bound on the performance of an algorithm, which gives us a more accurate estimate of its time complexity compared to Big O notation. In general, Theta notation is most useful for algorithms that have a well-defined average-case performance that is significantly different from their worst-case performance.

In practice, we often use Big O notation to provide an upper bound on the performance of an algorithm, and Theta notation to provide a tight bound on its performance. By combining these two forms of asymptotic notation, we can get a more complete picture of an algorithm's time complexity and make better-informed decisions about its efficiency.

## Space Complexity

In addition to time complexity, the space complexity of an algorithm is also an important factor to consider when analyzing its efficiency. Space complexity refers to the amount of memory or storage space an algorithm requires to execute, as a function of the size of its input.

Similar to time complexity, we can express the space complexity of an algorithm using asymptotic notation. In general, there are two types of space complexity: auxiliary space complexity and total space complexity.

Auxiliary space complexity refers to the amount of additional memory an algorithm requires beyond the space needed to store its input. In contrast, total space complexity refers to the total amount of memory an algorithm requires to execute, including the space needed to store its input.

To analyze the space complexity of an algorithm, we can use the same forms of asymptotic notation that we use for time complexity, including Big O, Omega, and Theta notation. For example, an algorithm that has a space complexity of O(n) means that its space requirements grow linearly with the size of its input.

It is important to note that space complexity can be just as important as time complexity, especially in situations where memory is limited or when dealing with large datasets. In some cases, we may need to sacrifice time complexity for better space complexity, or vice versa, depending on the specific requirements of the problem we are trying to solve.

In practice, when analyzing the efficiency of an algorithm, we usually consider both its time complexity and space complexity together to get a more accurate picture of its overall performance. By doing so, we can make better-informed decisions about which algorithm to use for a particular problem or application.

## Practical Applications

Asymptotic notation has many practical applications in the field of computer programming. By understanding the performance of different algorithms, we can make better-informed decisions about which algorithm to use for a particular problem.

For example, suppose we have two algorithms that solve the same problem. Algorithm A has a time complexity of O(n^2), while Algorithm B has a time complexity of O(n log n). If we are dealing with a small dataset, Algorithm A may be more efficient because its constant factors are likely to be smaller than those of Algorithm B. However, if we are dealing with a large dataset, Algorithm B may be more efficient because its time complexity grows more slowly than Algorithm A.

Asymptotic notation is also important for optimizing code. By analyzing the performance of different parts of a program, we can identify areas that are slowing down the program and optimize them for better performance. For example, suppose we have a program that is taking too long to complete because of a loop that has a time complexity of O(n^2). By optimizing the loop to have a time complexity of O(n log n), we can significantly improve the program's performance.

Asymptotic notation is also important for designing algorithms. By understanding the performance characteristics of different algorithms, we can design algorithms that are more efficient for specific problems. For example, suppose we have a problem that involves sorting a large dataset. By using an algorithm with a time complexity of O(n log n), we can ensure that the sorting process is efficient even for large datasets.

Finally, asymptotic notation is important for predicting the performance of future hardware. As hardware becomes more powerful, the performance characteristics of algorithms may change. By understanding the asymptotic behavior of algorithms, we can predict how they will perform on future hardware and design algorithms that take advantage of new hardware capabilities.