Algorithms are the bedrock of computer science and programming, providing a systematic approach to solving problems. This topic introduces what algorithms are, how they are described using pseudocode, and fundamental methods for analyzing their efficiency.
An algorithm is a finite sequence of well-defined, unambiguous, and executable instructions for solving a problem or completing a task. It serves as a blueprint for program execution, detailing the logical steps required to transform input into desired output.
Every algorithm must satisfy the following fundamental criteria:
Algorithms can be expressed in various ways, each with its own advantages:
When writing pseudocode, specific conventions help maintain clarity and consistency:
// and continue until the end of the line, explaining specific parts of the algorithm.{ }, similar to C/C++/Java.=).
Example: variable = value or variable = expression.True and False are used for Boolean values. These are often produced by logical operators (AND, OR, NOT) and relational operators (<, >, , , ).Algorithm Max(A, n)
// A is an array of size n
{
Result = A[1]; // Assume 1-based indexing for this pseudocode example
for i = 2 to n do
{
if A[i] > Result then
{
Result = A[i];
}
}
return Result;
}A recursive algorithm is an algorithm that solves a problem by calling itself as a subroutine, usually with smaller instances of the same problem. This approach often leads to elegant and concise solutions for problems that can be broken down into self-similar subproblems.
There are two main types of recursive algorithms:
Algorithm abc(int a)
{
// ... some operations ...
abc(10); // Directly calls itself
// ... some more operations ...
}A is indirectly recursive if it calls another algorithm B, which in turn calls algorithm A.
Algorithm abc(int a)
{
// ...
xyz(20, 30); // Calls xyz
// ...
}
Algorithm xyz(int x, int y)
{
sum = x + y;
abc(sum); // Calls abc, making it indirectly recursive
}The Tower of Hanoi is a classic mathematical game or puzzle that beautifully illustrates recursion. It consists of three rods and a number of disks of different sizes, which can slide onto any rod. The puzzle starts with the disks stacked on one rod in decreasing size from bottom to top, forming a conical shape.
The objective of the puzzle is to move the entire stack from the starting rod to a destination rod, obeying the following rules:
The problem can be solved recursively by breaking it down into three steps:
Algorithm TowerOfHanoi(n, source, auxiliary, destination)
// n: number of disks
// source: starting rod
// auxiliary: temporary rod
// destination: target rod
{
if (n >= 1)
{
// Move n-1 disks from source to auxiliary, using destination as temporary
TowerOfHanoi(n - 1, source, destination, auxiliary);
// Move the n-th disk from source to destination
write("Move top disk from tower", source, "to top of tower", destination);
// Move n-1 disks from auxiliary to destination, using source as temporary
TowerOfHanoi(n - 1, auxiliary, source, destination);
}
}The number of moves required to solve the Tower of Hanoi puzzle for disks is . For example, with 3 disks, it takes moves.
Performance analysis involves determining the amount of computational resources (like time and memory) an algorithm needs to run to completion. The goal is to develop a skill for evaluating and making judgments about an algorithm's efficiency, typically focusing on two key aspects:
The space complexity of an algorithm is the total amount of memory it needs to run to completion. It quantifies the storage required to execute an algorithm and produce its desired output. This space requirement can be broken down into two main components:
The total space complexity for a program can be expressed as:
Algorithm abc(a, b, c)
{
return a + b * c + (a + b - c);
}In this example, space is required to store variables a, b, and c. If each variable takes 1 "word" of memory:
a => 1 Wordb => 1 Wordc => 1 Word
The fixed part would represent the space for instructions. The variable part (instance) in this case is constant, as it doesn't depend on input size, but on the number of parameters. So, Words. If we focus on the variable part related to input, it's typically .Algorithm sum(a, n)
{
S = 0.0;
for i = 1 to n do
{
S = S + a[i];
}
return S;
}To analyze the space complexity:
a[]: words (proportional to input size n)n: 1 wordS: 1 wordi: 1 wordThus, the variable part (instance) words. The total space complexity . As grows, the dominant factor is , so the space complexity is approximately .
The time complexity of an algorithm is the amount of computational time it takes to produce the desired result. It's the sum of the compile time and the run (or execution) time. Since compile time is independent of input characteristics and occurs only once, we primarily concentrate on the run time of the program when analyzing time complexity.
Methods to compute the step count (to determine run time):
count: Add a counter variable that increments with each "significant" operation.Consider the sum(a, n) algorithm again:
Algorithm Sum(a, n)
{
S = 0.0; // Statement 1
for i = 1 to n do // Statement 2 (loop condition and increment)
{
S = S + a[i]; // Statement 3 (inside loop)
}
return S; // Statement 4
}Let's use the frequency table method:
| Statement | Steps/Execution (s/e) | Frequency | Total Steps |
| :-------- | :-------------------- | :-------- | :---------- |
| S = 0.0; | 1 | 1 | 1 |
| for i = 1 to n do | 1 | | |
| S = S + a[i]; | 1 | | |
| return S; | 1 | 1 | 1 |
| | | | |
The time complexity is . As grows, the dominant term is , making the time complexity .
Here's the sum algorithm implemented in C++, illustrating how operations translate to steps:
#include <iostream> // For input/output operations
#include <vector> // For using std::vector to store numbers
// Function to calculate the sum of n numbers in a vector
// This directly mirrors the 'Algorithm sum(a, n)' pseudocode.
double calculateSum(const std::vector<double>& arr, int n) {
// S_val variable to store the cumulative sum, initialized to 0.0
// This statement executes once.
double S_val = 0.0;
// Loop through the array elements from index 0 to n-1 (inclusive).
// C++ vectors are 0-indexed.
// The loop condition (i < n) is checked (n+1) times (n times true, 1 time false).
// The increment (i++) runs n times.
for (int i = 0; i < n; ++i) {
// The addition and assignment operation (S_val = S_val + arr[i])
// executes n times, once for each element in the loop.
For a recursive function that sums elements, like rsum(list, n) from the source:
| Statement | Steps/Execution (s/e) | Frequency | Total Steps |
| :-------- | :-------------------- | :-------- | :---------- |
| if (n) | 1 | | |
| return rsum(...) + list[n-1]; | 1 | | |
| return list[0]; | 1 | 1 | 1 |
| Total | | | |
This also results in a time complexity of .
Asymptotic notations are mathematical tools used to describe the limiting behavior of an algorithm's time or space complexity as the input size () grows towards infinity. They allow us to classify algorithms based on their growth rates, focusing on how their performance scales with large inputs rather than exact execution times.
The three primary asymptotic notations are:
Big O notation describes the upper bound of an algorithm's running time. It represents the longest amount of time an algorithm could possibly take to complete, providing a guarantee that the algorithm will not perform worse than this bound. It specifies the worst-case time complexity.
Definition: A function is (read as " of is big oh of of ") if and only if there exist positive constants and such that: where and .
Examples:
Omega notation describes the lower bound of an algorithm's running time. It represents the shortest amount of time an algorithm could possibly take, providing a guarantee that it will perform at least as fast as this bound. It specifies the best-case time complexity.
Definition: A function is (read as " of is omega of of ") if and only if there exist positive constants and such that: where and .
Examples:
Theta notation describes the tight bound (both upper and lower) of an algorithm's running time. It implies that the algorithm's performance is bounded both from above and below by the same function (up to a constant factor). It specifies the average-case time complexity.
Definition: A function is (read as " of is theta of of ") if and only if there exist positive constants , , and such that: where and .
Example:
Here are common time complexities and their typical performance implications:
A recurrence relation is an equation or inequality that describes a function in terms of its values on smaller inputs. They are particularly useful for determining the running time of recursive programs, as they naturally mirror the recursive structure of the algorithm. Solving a recurrence relation means finding a closed-form expression (a non-recursive formula) for the function.
There are four common methods for solving recurrence relations:
The substitution method involves guessing a solution and then using mathematical induction to prove that the guess is correct. It often requires some intuition to make an initial good guess.
power(x, n) AlgorithmConsider a recursive power(x, n) algorithm:
Algorithm power(x, n)
{
if (n == 0)
return 1;
else
return x * power(x, n - 1);
}The recurrence relation for this algorithm's time complexity, , would be:
The recursive Fibonacci sequence function is a classic example to analyze with recurrence relations.
Algorithm Fibonacci(n)
{
if (n <= 1)
return n;
else
return Fibonacci(n - 1) + Fibonacci(n - 2);
}The recurrence relation for its time complexity, , is:
To approximate , we can simplify by assuming for large : Approximation:
Expanding this: Following this pattern, we get: If we set , then : Since is a constant, . This shows that the time complexity of the recursive Fibonacci algorithm is approximately , which is exponential.
The recursion tree method is a visual technique that models the costs (time) of a recursive execution of an algorithm. It's particularly good for generating educated guesses for the substitution method. Each node in the tree represents the cost of a single subproblem, and the total cost is the sum of costs at all levels of the tree.
Let's visualize the cost distribution for :
n^2 --- Level 0, Cost: n^2
/ \
/ \
/ \
T(n/4) T(n/2)
/ \ / \
/ \ / \
(n/4)^2 (n/2)^2 --- Level 1, Cost: (n/4)^2 + (n/2)^2 = n^2(1/16 + 1/4) = (5/16)n^2
/ \ / \ / \
... ... ... ... ... ... --- Level 2, Cost: (n/16)^2 + (n/8)^2 + (n/8)^2 + (n/4)^2 = (25/256)n^2
.
.
.
Total Cost = n^2 + (5/16)n^2 + (25/256)n^2 + ...The total cost is the sum of costs at each level: This is a geometric series with and common ratio . Since , the series converges to . Therefore, .
A finite set of unambiguous, step-by-step instructions for solving a problem or completing a task, satisfying criteria like input, output, definiteness, finiteness, and effectiveness.
An informal high-level description of an algorithm's operating principle, allowing concise and unambiguous representation without strict programming language syntax.
A measure of the amount of computational time an algorithm needs to run to completion, typically expressed as a function of the input size, focusing on the runtime.
A measure of the amount of memory an algorithm needs to run to completion, encompassing both fixed and variable parts of memory usage.
Mathematical tools (Big O, Omega, Theta) used to describe the limiting behavior of an algorithm's time or space complexity as the input size grows, providing upper, lower, and tight bounds.
Test your understanding with 5 questions
Which of the following is NOT a required criterion for a valid algorithm?
In pseudocode, how is an assignment of values to a variable typically done?
An algorithm that calls itself within its own body is known as a:
What does Big O notation ($O$) primarily represent in algorithm analysis?
The space complexity of an algorithm `sum(a, n)` that calculates the sum of `n` elements in an array `a` (where `a` is stored) and uses additional variables `S`, `i`, and `n` would be approximately:
<=>===A[i], where A is the array name and i is the location number (index).while <condition> do
{
Statement1...
Statement2...
}for variable = value1 to value2 step step_value do
{
Statement1...
Statement2...
}if <condition> then <statement>if <condition> then <statement1> else <statement2>read (for input) and write (for output).Algorithm Name(<Parameter list>)nn6 Modules
6 Modules