When it comes to evaluating algorithms, there are two key aspects to consider: **correctness** and **execution speed**. Understanding these two dimensions allows us to assess how well an algorithm performs its task and how efficiently it can handle various inputs. Beyond mere intuition, comparing algorithms requires rigorous, mathematically proven methods to ensure universal guarantees about their behavior.

## Correctness of an Algorithm

**Correctness** means that the algorithm consistently produces the expected result for all valid inputs. An algorithm is said to be correct if it terminates with the desired output every time it is executed. Correctness is the foundation of an algorithm’s reliability. Even the fastest algorithm is useless if it doesn’t solve the problem at hand.

To **prove correctness**, we rely on mathematical reasoning, typically involving:

**Inductive Proofs:**These are used to demonstrate that the algorithm works for a base case and every subsequent step. This is common in recursive algorithms.**Invariants:**These are conditions or properties that remain true during every step of the algorithm’s execution. Proving that these invariants hold guarantees the algorithm behaves as expected throughout its run.**Post-Conditions:**These verify that the output after the algorithm completes is what we expect. The algorithm starts from a defined set of**pre-conditions**(initial input) and should always arrive at the desired**post-condition**(the correct output).

### Example of Correctness Proof: Binary Search

To show an example, consider the **binary search** algorithm:

**Pre-condition:**A sorted array and a target value.**Invariant:**At every step, the target value must lie within the current search interval.**Post-condition:**Either the target is found, or the algorithm correctly concludes the target is not present.

By maintaining the invariant (i.e., ensuring the target is in the search interval at each step), we can prove that binary search is correct.

## Execution Speed (Efficiency)

Correctness alone doesn’t suffice—**execution speed** (or efficiency) is another critical factor. Efficient algorithms complete their tasks in a reasonable time, even as the input size grows. Philosophically one can argue that an answer doesn’t exist if we cannot get it within reasonable time. Here, we examine the **time complexity** and **space complexity** of algorithms, which are central to understanding their performance.

### Key Terms in Algorithm Analysis:

**Time Complexity:**Describes how the running time of an algorithm scales with input size, usually expressed using Big-O notation, such as $O(n)$, $O(gn)$, or $O(n_{2})$.**Space Complexity:**Describes how much memory the algorithm uses as the input size increases.**Worst-Case Analysis:**Determines the maximum time or space the algorithm might require. This is critical for proving universal guarantees.**Best-Case and Average-Case Analysis:**These give insight into how the algorithm behaves under ideal or typical conditions, but worst-case analysis is what guarantees performance under all conditions.

## Aim: Universal Guarantees, Mathematically Proven

The goal in comparing algorithms is to arrive at **universal guarantees**—mathematical proofs that ensure the algorithm behaves as expected, regardless of variations in input or execution environments. These guarantees are crucial because algorithms may be run in diverse contexts:

- Different types and sizes of input data.
- Varying hardware specifications (e.g., processors, memory capacity).
- Different programming languages or compilers, which may affect execution speed.

Thus, **abstraction** is important in algorithm analysis: we abstract away from the details of the machine and programming language to focus on the algorithm itself, measured by how many fundamental steps (e.g., comparisons, swaps) it performs.

## The Challenge of Different Inputs and Execution Environments

When evaluating algorithms, it’s essential to account for the fact that:

- Inputs vary in size and nature (e.g., a sorted list versus a random list).
- Execution environments differ in terms of hardware (CPU speed, available memory), operating systems, and compilers, all of which influence performance.

However, we can bypass these variations by focusing on **mathematical models of computation**, such as the **RAM model**, which measures how many operations an algorithm requires independent of the actual hardware. In this way, we abstract away the machine specifics and focus on how many basic operations the algorithm performs.

## Handling Inaccuracies and Algorithm Stability

When comparing algorithms, particularly in real-world applications, it’s important to consider **algorithm stability**. These are crucial factors in ensuring that an algorithm performs consistently and reliably in practice, especially when working with numerical computations.

### Floating-Point Inaccuracy

Most computers use floating-point arithmetic to represent real numbers, which can lead to **inaccuracies** due to rounding errors. These errors arise because certain real numbers cannot be represented exactly in binary form, leading to small discrepancies in calculations. Over time, as computations build on one another, these tiny inaccuracies can accumulate, potentially leading to significant deviations from the true result.

For example, consider the sum of a large set of floating-point numbers. Due to rounding errors, the sum may slightly differ from what you would expect based on exact arithmetic. These inaccuracies can affect the performance and output of algorithms, particularly those involving numerical methods, such as:

**Matrix computations****Root-finding algorithms****Optimization problems**

### Stability of Algorithms

**Algorithm stability** refers to an algorithm’s resilience to small perturbations in the input, such as those caused by floating-point inaccuracies. A **stable algorithm** produces results that do not significantly deviate from the correct solution, even when there are small errors in input or intermediate calculations. On the other hand, **unstable algorithms** can amplify these small errors, leading to unreliable results.

A good example of this can be found in **numerical linear algebra**:

**Gaussian Elimination**is a commonly used algorithm for solving systems of linear equations. In its basic form, it can be numerically unstable when applied to certain matrices, meaning that small rounding errors in floating-point arithmetic can lead to large inaccuracies in the final solution.- In contrast, a modified version of Gaussian elimination, known as
**LU decomposition with partial pivoting**, is more stable because it reduces the amplification of rounding errors.

### Algorithm Stability in Action: Sorting Algorithms

Let’s take another example of **sorting algorithms**. While sorting typically does not involve floating-point arithmetic, algorithmic stability plays a role in determining whether sorting algorithms preserve the relative order of equal elements:

- A sorting algorithm is
**stable**if, when two elements are equal, they maintain their relative order after sorting. - Algorithms like
**merge sort**and**insertion sort**are stable, whereas**quicksort**is not stable in its basic form.

### Stability Despite Floating-Point Inaccuracies

Even when floating-point inaccuracies are present, many algorithms are designed to be **numerically stable**, meaning they provide reliable results within a small margin of error. In these cases, the inaccuracies from floating-point arithmetic do not significantly affect the final outcome.

Examples include:

**Iterative methods**(e.g.,**conjugate gradient**and**Jacobi method**) for solving systems of linear equations. While they operate on approximations of the solution at each step, their stability ensures that they converge to the correct solution even in the presence of rounding errors.**Stable numerical algorithms**in scientific computing, such as those for computing eigenvalues or matrix factorizations, are specifically designed to mitigate the effects of floating-point errors.

When analyzing and comparing algorithms, we must consider not only their **correctness** and **efficiency**, but also their **stability** in the presence of floating-point inaccuracies. Stable algorithms are essential in domains like scientific computing, finance, and engineering, where numerical precision and reliability are critical. A fast and theoretically correct algorithm may still fail in practice if it is unstable or overly sensitive to rounding errors. Thus, a comprehensive algorithm analysis must account for both worst-case performance and the real-world challenges of inexact computation.

## Mathematical Proofs and Worst-Case Analysis

One of the most reliable ways to compare algorithms is through **worst-case analysis**. This approach gives us a guaranteed upper bound on the algorithm’s performance, ensuring that it won’t take more than a certain amount of time or space, no matter what the input is. Worst-case analysis is critical because it ensures that the algorithm will still function effectively even under the most challenging conditions.

For example, consider **sorting algorithms** like quicksort, mergesort, and bubblesort. Through mathematical analysis:

- Quicksort has an average time complexity of $O(ngn)$ but a worst-case time complexity of $O(n_{2})$.
- Mergesort consistently guarantees $O(ngn)$, even in the worst case.
- Bubblesort, meanwhile, has $O(n_{2})$ time complexity both in the average and worst case, making it much slower for large inputs.

By using worst-case analysis, we can make informed decisions about which algorithms to choose, depending on the problem size and the guarantee of performance we need.

## Abstractions and Models in Algorithm Analysis

To effectively compare algorithms, we rely on abstractions such as:

**Big-O Notation:**This gives an upper bound on an algorithm’s running time or space requirement, helping us understand how it scales as the input grows. For example, an algorithm with $O(n)$ time complexity is faster than one with $O(n_{2})$ for large inputs.**Asymptotic Analysis:**This focuses on the behavior of algorithms as input size approaches infinity, helping us evaluate which algorithm will be more efficient for large data sets.**Machine Models:**The**RAM (Random Access Machine)**model is commonly used in theoretical computer science to measure the number of basic operations an algorithm performs, independent of the hardware or execution environment.