Understanding Algorithms in Computer Science
Understanding Algorithms in Computer Science
This chapter introduces one of the most fundamental concepts in computer science:
the algorithm. Understanding algorithms is essential not only for programming, but also
for problem-solving in everyday life. We begin by asking a simple but important question:
What exactly is an algorithm? From there, we will define the concept precisely, examine
its role in solving computational problems, and explore how algorithms can be represented
in clear and structured ways. Different representation methods will be presented—such as
natural language, pseudo-code, flowcharts, and actual code implementations—so that
students can learn to express algorithmic solutions effectively and unambiguously. Finally,
we will turn to the process of designing algorithms, focusing on key principles like problem
decomposition, abstraction, and iterative refinement. These skills are vital in translating
problem requirements into executable instructions, ensuring that algorithms are not only
correct but also efficient and adaptable. By the end of this chapter, you should be able to:
• Understand why algorithms are central to computer science and everyday problem-
solving.
• Input = ingredients.
In computer science, an algorithm is like a recipe for a computer: a finite list of clear
instructions that, if followed, produce the correct result.
Definition
An algorithm is a finite, ordered sequence of precise instructions designed to solve a
problem or perform a task.
4. Finiteness: If we trace out the instructions of an algorithm, then for all cases, the
algorithm terminates after a finite number of steps.
• Input: A and B.
• Output: S = A + B.
• Steps:
1. Read A and B.
2. Compute S = A + B.
3. Display S.
4. Stop.
With the exponential growth of data and computing, robust algorithms are essential
for building efficient IT systems. As industry demand for optimized software continues to
grow, it’s imperative that we improve our algorithm design skills. Programming languages
provide the tools for implementation, but an understanding of algorithmic concepts and
data structures is essential for writing high-performance programs.
The Algorithm development combines both art and skill, playing a crucial role in the
software creation process. Before a program is actually implemented, the fundamental step
of designing an algorithm takes center stage. Algorithms can be compared to step-by-step
problem-solving plans. These systematic procedures do not present the answers themselves,
but provide explicit instructions on how to get there. This strong stress on precisely defined
constructive procedures is a defining characteristic of computer science, distinguishing it
from other fields of study. This distinction is particularly evident when compared with
theoretical mathematics. In theoretical mathematics, practitioners are often content to
demonstrate the existence of a solution to a problem and, possibly, to study the properties
of this solution. Thus, the practical and methodical essence of algorithm development
makes computer science a discipline deeply rooted in pragmatic problem-solving, ensuring
that it provides tangible solutions to real-world challenges.
3 Importance of Algorithms
Algorithms are the fundamental building blocks of computer programs and systems. They
define the sequence of logical steps and operations required to complete tasks effectively
and efficiently. In computer science, algorithms are not just tools—they are the very
essence of problem-solving and innovation. Their importance can be highlighted in several
ways:
4. Scalability: As data sizes and user demands grow, efficient algorithms enable systems
to scale gracefully. An algorithm designed with scalability in mind ensures that
performance remains acceptable even as the workload increases dramatically.
largest search engines, handles billions of searches daily. Thanks to advanced search and
ranking algorithms, results are delivered in milliseconds. Without these algorithms, users
would be left waiting for hours, making the service unusable. Algorithms, therefore, form
Once these methodological steps are in place, the next challenge is choosing an appro-
priate paradigm—a general strategy of reasoning tailored to the problem’s structure. Algo-
rithm designers rarely start from scratch; instead, they adapt well-established paradigms.
9 return S
Divide & Conquer. Split the problem into independent subproblems, solve each
recursively, and combine results. Examples: Merge Sort, Quick Sort, Strassen’s matrix
multiplication, Closest Pair of Points.
Branch & Bound. Extends backtracking with bounding functions to prune large parts
of the search space. Widely used for solving NP-hard problems such as TSP or integer
programming.
12 return best
These paradigms are complementary rather than competing. Each arises from a distinct
intuition: greedy methods aim for immediacy by always taking the locally best option,
divide & conquer relies on breaking a problem into smaller pieces, dynamic programming
takes advantage of repeated subproblems, backtracking explores all possibilities system-
atically, branch & bound reduces this exploration by using bounds to cut off hopeless
paths, and randomized algorithms bring in chance to simplify design or improve average
performance. None of these strategies is universally superior—their effectiveness depends
very much on the structure of the problem at hand.
To understand their differences more clearly, let us discuss them in everyday terms:
• Greedy algorithms are fast and simple because they always choose what seems best
in the moment. For example, if you want to give change with the fewest coins, you
might always pick the largest coin available first. This works well in many currency
systems, but it can fail if local choices do not add up to the truly best overall solution.
The strength of greedy methods is speed and simplicity, but the weakness is that
they can miss the global optimum.
• Divide & Conquer splits a problem into smaller subproblems, solves each one, and
then combines the results. A classic example is sorting a list by dividing it in half,
sorting each half, and then merging. This approach is powerful because smaller tasks
are easier to handle, and together they solve the bigger one. However, repeatedly
splitting and combining adds extra work, and recursion may lead to overhead in
implementation.
• Dynamic Programming (DP) is useful when the same subproblems appear again
and again. Instead of solving them repeatedly, DP remembers solutions in a table
and reuses them. Imagine climbing stairs where at each step you can take one or
two stairs: many paths overlap, and DP avoids recalculating them. This makes
problems that would otherwise take a very long time (growing explosively with input
size) solvable in reasonable time. The tradeoff is that DP usually requires significant
memory and careful design of the table.
• Backtracking explores the entire space of possible solutions step by step. When a
choice leads to a dead end, it goes back (“backtracks”) and tries another. Think
of solving a Sudoku puzzle: you fill in numbers until you get stuck, then erase and
try a different option. Backtracking is complete—it can always find a solution if
one exists—but it may take an extremely long time for large problems because the
number of possibilities grows very quickly.
• Branch & Bound builds upon backtracking by adding a pruning idea. It estimates
whether a partial solution could ever lead to a better result than the best one found
so far. If not, it abandons that path immediately. This can save enormous amounts of
work in practice, but its success depends on how good the estimation (the “bound”)
is. Weak bounds lead to almost the same work as plain backtracking.
In summary, each paradigm embodies a distinct way of thinking about problem solving.
Greedy algorithms are like sprinters: they move quickly by always grabbing the best
immediate option, but sometimes this shortsightedness prevents them from reaching the
true goal. Divide & Conquer is more like a strategist: it breaks the battle into smaller,
manageable fights, solves each one, and then combines the results. Dynamic Programming
is the careful planner: instead of redoing the same work, it remembers past results and
builds on them, turning otherwise impossible tasks into manageable ones. Backtracking is
the explorer who tries every path through a maze; it guarantees that a way out will be
found if one exists, but may take a very long time. Branch & Bound is a smarter explorer:
it uses estimates to avoid going down paths that are clearly hopeless, saving time while
still ensuring correctness. Finally, Randomization is the gambler: it introduces chance
into the process, which may sound risky, but often leads to surprisingly fast and elegant
solutions in practice. As summarized in Table 1, each paradigm has its own assumptions,
strengths, and limitations.
Backtracking Assumes cheap feasibility checks. Strength: can find all exact
solutions. Issue: exponential blow-up in the worst case.
Branch & Bound Relies on admissible bounds for pruning. Strength: significant reduc-
tion of search space. Issue: weak bounds lead to little improvement.
5 Algorithm presentation
Once we have conceived an algorithm, the next step is to express it in a way that is
understandable to both humans and, eventually, machines. The manner in which an
algorithm is presented depends heavily on the target audience and the intended use. For
instance, a computer scientist designing a new sorting algorithm for a research paper might
favor pseudocode, while a beginner student might understand the same algorithm more
clearly if represented through a flowchart. Professional programmers, on the other hand,
must eventually translate these representations into executable program code. There are
four major modes of representation we will discuss here: natural language, pseudocode,
flowcharts, and program code. Each offers a unique perspective and serves a specific
purpose.
Boil water. Place a tea bag in a cup. Pour the boiling water into the cup.
Allow the tea to steep for 3 minutes. Remove the tea bag. Add sugar or
milk if desired. Serve.
Although natural language descriptions are highly intuitive and easily comprehensible
for human readers, they suffer from inherent imprecision. Phrases such as “steep”
or “if desired” are open to multiple interpretations, and crucial details like exact
quantities or precise durations are often omitted. As a result, natural language
cannot be directly interpreted or executed by a computer, which requires instructions
that are unambiguous and formally defined. The principal advantage of natural
language lies in its accessibility, simplicity, and the absence of any need for prior
technical knowledge; however, these strengths are offset by its weaknesses, namely
its susceptibility to ambiguity, its lack of precision, and its unsuitability for direct
translation into executable code.
2. Pseudocode
Pseudocode constitutes an intermediate representation between natural language and
actual programming code, providing a structured yet flexible means of describing al-
gorithms. It employs English-like words combined with programming-style constructs
such as assignments, conditionals, and loops, thereby allowing designers to articulate
the logical steps of an algorithm without being constrained by the strict syntactic
rules of any particular programming language. This approach eliminates much of
the ambiguity present in natural language while remaining language-independent
and easy to refine during the design process. For instance, the task of finding the
maximum of three numbers can be expressed in pseudocode as follows:
2 max ← b
3 if c > max then
4 max ← c
5 return max;
These example illustrates how pseudocode combines readability with a clear logical
structure that mirrors actual programming practices, thereby making translation into
any formal programming language straightforward. Despite its advantages of acces-
sibility, clarity, and adaptability, pseudocode remains non-executable by computers
and presupposes a basic understanding of programming constructs. Nevertheless, its
balance between simplicity and rigor has established pseudocode as the most widely
used method for presenting algorithms in textbooks, academic publications, and
early stages of system design.
Symbol Meaning
Oval Start or End point
Rectangle Process or instruction
Diamond Decision point
Parallelogram Input or Output action
This visual approach makes the control flow of an algorithm easy to grasp at a glance,
offering an intuitive means of understanding both simple and complex processes.
Start
Read a, b, c
m←a
Yes
b > m? m←b
No
Yes
c > m? m←c
No
Print m
End
Python C
These representations are executable and produce concrete outputs, ensuring un-
ambiguous interpretation by the computer. Moreover, program code can be tested,
debugged, and integrated into larger systems, which makes it indispensable for
practical applications. However, this precision comes at the cost of accessibility.
Writing code requires familiarity with the syntax and semantics of a specific pro-
gramming language, and it is often less readable for non-technical audiences. In
addition, program code is tied to the rules of the chosen language, which reduces its
portability across different programming environments. Despite these limitations,
coding remains the essential step that transforms abstract algorithmic ideas into
actionable instructions that a machine can execute.
These representations are executable and produce concrete outputs, ensuring un-
ambiguous interpretation by the computer. Moreover, program code can be tested,
debugged, and integrated into larger systems, which makes it indispensable for
practical applications.
However, this precision comes at the cost of accessibility. Writing code requires
familiarity with the syntax and semantics of a specific programming language, and
it is often less readable for non-technical audiences. In addition, program code is
tied to the rules of the chosen language, which reduces its portability across different
programming environments. Despite these limitations, coding remains the essential
step that transforms abstract algorithmic ideas into actionable instructions that a
machine can execute.
Algorithms can be expressed in multiple forms, each suited to a different purpose and
audience. No single representation is universally superior; rather, each has distinct
strengths and limitations that make it more appropriate in specific contexts. Natural
language provides an intuitive and accessible description that anyone can understand,
making it ideal for initial brainstorming or informal communication. Pseudocode, on
the other hand, offers a structured and language-independent way to design and refine
algorithms, striking a balance between readability and formality. Flowcharts emphasize
visualization and are particularly effective when teaching, documenting, or communicating
the logic of an algorithm to audiences with diverse technical backgrounds. Finally, program
code is the only representation that can be directly executed by a computer, but it requires
technical expertise and adherence to strict syntactic rules.
Table 3 summarizes the main advantages and disadvantages of these four methods of
algorithm representation.
In practice, these representations are often used in sequence. An algorithm may first
be described informally in natural language, then refined into pseudocode, illustrated with
a flowchart to aid understanding, and finally translated into program code for execution.
This layered approach ensures that the algorithm is both conceptually clear and practically
implementable. For example, when teaching a new algorithm, a teacher might begin with
a natural language explanation to introduce the idea, use pseudocode to demonstrate the
logical steps, reinforce comprehension with a flowchart, and finally present program code
to show how the algorithm is realized in practice.
header, the environment, and the body. These three pillars form the universal skeleton of
structured programming.
• Avoid vague or cryptic names such as A1, Test, or XYZ, which hinder readability.
2. The Environment
The environment sets up the context in which the algorithm operates. It is like preparing
a kitchen before cooking: you must gather the right ingredients (variables) and tools
(constants or data types) before starting.
The environment typically contains:
• Custom types (optional): defined with the keyword type, e.g., a Student type with
fields Name, Age, and Grade.
• Constants: fixed values that never change during execution, such as π = 3.14159
or a maximum buffer size. Constants provide clarity and avoid repeating “magic
numbers” in the body.
• Variables: named memory locations where data is stored, modified, and reused.
Each variable should be declared with its type (integer, real, string, Boolean, etc.)
to ensure correctness and avoid ambiguity.
Variables
x, y, m : Real
Constants
PI = 3.14159
Pedagogical note: In compiled languages like C, Pascal, or Java, explicit declarations are
mandatory because the compiler needs to allocate memory and verify valid operations.
Even in flexible languages like Python, planning your variables and constants in advance
is a sign of disciplined and maintainable algorithm design.
Algorithm 9: ComputeAverage
Input : Three real numbers a, b, c
Output : The average of a, b, c
1 begin
// Environment
2 Variables
3 a, b, c, avg : Real
// Body
4 Begin
5 avg ← (a + b + c)/3
6 write avg
7 End
• The body carries out the computation and produces the result.
instructions is therefore essential for every student of algorithmics. It not only enables the
understanding of how to translate an abstract idea into a clear sequence of actions, but
also helps in grasping the universal logic that underpins computer programs. Fundamental
instructions are generally grouped into three categories: sequence instructions, which
are executed in a specific order; selection instructions, which direct the flow based on a
condition; and repetition instructions, which allow tasks to be handled iteratively. Before
moving on to more elaborate data structures and complex design paradigms, it is necessary
to master these basic components. They form the foundation on which all algorithms rest,
from the simplest to the most sophisticated.
• Store values in memory (assignment): give names to data so that they can be
reused later,
• Read data from the outside world (input): capture information from the user or
another system,
• Display results (output): communicate the outcome of the computation back to the
user.
Assignment
Rule: Always evaluate the right-hand side expression first, then assign the result to the
left-hand side variable.
This is different from mathematics: in math, x = x + 2 makes no sense, but in
programming, it means “take the current value of x, add 2, and store the result back into
x.”
Programs are not useful unless they interact with their environment.
This simple pair of instructions transforms a passive program into an interactive tool.
Let us illustrate assignment, input, and output with a concrete problem: computing the
average of two numbers.
To better understand how pseudocode maps into real programming languages, let us
translate the previous algorithm into Python and C.
int main() {
double x, y
printf("Ent
scanf("%lf"
Listing (1) Python Implementation printf("Ent
# Average of two numbers scanf("%lf"
x=float(input("Enter first number: "))
y=float(input("Enter second number: ")) m = (x + y)
printf("The
m = (x + y) / 2 return 0;
print("The average is:", m) }
• Assignment: computing m,
This side-by-side comparison shows that while the syntax may differ between languages,
the underlying concepts remain universal.
This idea is very intuitive because we apply it constantly in everyday life. For example,
if it rains, we take an umbrella; otherwise, we go without. At a traffic light, if the light is
green, we move forward; if it is red, we stop. Algorithms mimic this reasoning through
conditional structures, allowing them to adapt dynamically to different scenarios. Broadly
speaking, conditionals come in several forms—complete, reduced, nested, multiple-choice,
and compound—but the essence is always the same: make a decision, then act accordingly.
The most common form of branching is the complete conditional, expressed as if/else.
In this structure, a test determines which of two mutually exclusive blocks is executed: if
the condition is true, one block runs; if false, the other block runs.
Start
Yes No
Condition?
End
2 if x > 0 then
3 write “Positive”
4 else if x = 0 then
5 write “Zero”
6 else
7 write “Negative”
Start
Read x
Yes
x > 0? Write “Positive”
No
Yes
Write “Zero” x = 0?
No
Write “Negative”
End
This step-by-step process exemplifies the general principle: exactly one block is executed
depending on the outcome of the condition. The flowchart in Figure 3 visually confirms
this behavior: the diamond-shaped decision node splits the execution into two distinct
paths—“Yes” and “No”—that rejoin after the block is executed. Sometimes there is no
need for an alternative block; we only want to act if a condition is true. This leads to
the reduced conditional, or simple if. If the condition holds, a block of instructions is
Input :n ∈ Z
1 begin
2 if n mod 2 = 0 then
3 write “Even”
The flowchart in Figure 5 reflects the same logic: if the condition evaluates to true,
the block runs; if not, the program bypasses the block entirely and continues.
Start
No
Condition?
Yes
Execute Block
End
In real-world problems, two branches are often insufficient. We may need to test several
alternatives in sequence. This situation calls for nested conditionals, or the if / else if
/ else chain. Algorithm 13 shows a typical case where a grade is classified as Excellent,
Pass, or Fail depending on thresholds.
1 begin
2 if grade ≥ 16 then
3 write “Excellent”
4 else if grade ≥ 10 then
5 write “Pass”
6 else
7 write “Fail”
The flowchart in Figure 6 makes the branching explicit: the program checks each condi-
tion in order until one is satisfied, at which point execution continues in the corresponding
block.
Start
Yes No
Grade ≥ 16?
Yes No
Excellent Grade ≥ 10?
Pass Fail
End
When the program must select one action among many based on the value of a single
variable, a multiple-choice structure is more efficient. In C, this appears as a switch/case
statement. Algorithm 14 illustrates how a month number is mapped to its name.
2 switch m
3 otherwise 1
4 write “January”
5 otherwise 2
6 write “February”
7 . . . otherwise 12
8 write “December”
9
10 write “Invalid”
The flowchart in Figure 7 shows this fan-out clearly: the decision branches into many
cases depending on the variable’s value, with one optional default branch.
Start
Read variable v
Which value?
End
Finally, conditions themselves may be composed using logical operators. With AND
(&&), all sub-conditions must be true; with OR (||), at least one must be true. This
allows complex decision criteria.
Algorithm 15 shows a student admission rule: admitted if the average is at least 10
and both fundamental subjects are above 5.
The flowchart in Figure 8 captures this logic: execution proceeds only if both conditions
are satisfied; otherwise, the program branches to the failure block.
Start
No
avg ≥ 10?
Yes
No Yes
note1 > 5 and note2 > 5?
Failed Admitted
End
Across these five variants, the unifying theme is that conditionals allow programs to
adapt to data and circumstances. Pseudocode provides a compact formal description,
while flowcharts give an intuitive visual picture of the branching logic. Together, they
highlight how conditional instructions form the essential building blocks of decision-making
in algorithms.
a block of steps until a certain condition is met (for example, "repeat while there are
still items in the list" or "repeat until the number becomes zero"). This makes loops
powerful tools for handling repetitive tasks such as summing numbers, processing arrays,
or searching through data. In this subsection, we will explore the main types of loops,
how they are expressed in pseudocode, and how they are implemented in programming
languages. We will also see how loops connect directly to the idea of algorithm efficiency,
since repetition often dominates the cost of computation.
A For loop is the clearest way to say “repeat this action a fixed number of times.” You
choose a start value for a counter (often i = 1), specify an end test (for example i ≤ n),
perform the body once for that counter value, and then update the counter by a fixed step
(usually +1 when counting up or −1 when counting down) before checking the test again;
the loop stops as soon as the test becomes false. This pattern fits tasks where the number
of repetitions is known in advance—printing the numbers from 1 to n, accumulating a
total, or producing a neat countdown. To anchor the idea, Algorithm 16 computes the
sum 1 + 2 + · · · + n. This simple algorithm demonstrates how a for-loop can accumulate
values step by step until the final result is obtained.
1 begin
2 S←0
3 for i ← 1 to n do
4 S ←S+i
5 write “Sum = ”, S
Notice how the loop structure provides a systematic way to repeat an operation (adding
i) until the condition is no longer satisfied. Algorithm 17 shows a countdown variant,
where the same looping mechanism works in reverse. Instead of incrementing, the index
decrements until it reaches the stopping condition.
2 for i ← n ; i ≥ 1 ; i ← i − 1 do
3 write i
Start
Initialize i ← 1
Yes
i≤n? write i
No
i←i+1
End
Finally, Listings 3 and 4 translate the same logic into executable Python and standard
C. Placing them side by side helps students directly compare syntax and style across
languages.
The while loop is a flexible control structure that allows repeated execution of a block of
instructions as long as a given condition holds. Its main strength lies in situations where
the number of repetitions is not known in advance. Figure 10 illustrates its use in both C
and Python, where a simple counter is incremented until the condition fails.
int i = 1;
while (i <= n) { i = 1
printf("%d ", i); while i <= n:
i = i + 1; // progress print(i, end=" ")
} i = i + 1 # pro
The power of the construct becomes clear when it is combined with pseudocode and
flowcharts, which help to solidify the intuition behind this essential programming tool.
Several common applications highlight the importance of the while loop: input
validation (“while the entered value is not in the allowed range, ask again”), state-driven
processing (“while the file still has data,” “while the connection is open”), and cases where
the loop may legitimately do nothing (if the initial state already violates the condition,
the loop never executes). Example 18 demonstrates input validation, where the program
repeatedly asks for a grade until a value in [0, 20] is given.
The C implementation, shown below the pseudocode, makes the same structure explicit
with a test and repeated reads inside the loop. In contrast, Example 19 illustrates the
sentinel pattern: numbers are read and their squares are printed until the user enters 0.
Because the loop checks the condition first, a priming read is performed before the
loop begins, and each iteration ensures progress by reading again inside the loop.
Reasoning about a while loop requires attention to two aspects: termination and
correctness. Termination is guaranteed if a quantity in the loop moves steadily toward
ending the process (for instance, a counter approaching a bound, or successive inputs
eventually matching a sentinel). Correctness can be understood through informal invariants,
such as “i is the next number to print” or “all previously printed squares correspond to
earlier inputs.” Tracing three or four iterations on paper is a powerful way to make these
invariants visible. Nevertheless, the construct is prone to common pitfalls. A missing
update step inside the body (e.g., forgetting i = i + 1 or omitting the update read)
results in an infinite loop. An incorrect initial state may cause the loop to skip or overshoot,
while a wrongly formulated condition can make the loop never run. In C, one must also
be careful not to confuse = (assignment) with == (comparison), since writing while (i
= 0) accidentally assigns zero to i and prevents the loop from executing. In summary,
the while loop embodies the principle of “look first, act later.” Its correct use depends on
starting with a valid initial state, writing a condition that will eventually become false, and
ensuring clear progress inside the loop body. By combining careful reasoning, pseudocode,
actual implementations, and supporting figures, one can develop a robust understanding
of this versatile programming construct.
The repeat–until loop is a post-test control structure, which means the body of the loop
is always executed at least once before any condition is checked. This property makes
it fundamentally different from the while loop, which evaluates its condition first and
may skip the body entirely if the test fails immediately. To build intuition, imagine being
told to “do the task once, then check if it is time to stop; if not, repeat.” This model is
especially appropriate for situations where an action must happen at least once, such as
prompting a user for input, displaying a menu, or generating initial output before deciding
whether to continue.
Algorithm 20 illustrates this with a simple example: reading integers and printing their
squares until the user enters 0. Notice how the loop’s stop condition (n = 0) is tested after
the body executes, guaranteeing at least one read and one print.
Algorithm 20: Read integers and print their squares until the user enters 0
1 begin
2 repeat
3 read n
4 write “Square = ”, n × n
5 until n = 0
int n;
do {
scanf("%d", &n);
printf("Square = %d\n", n*n);
} while (n != 0); // continue while n != 0 (stop when n == 0)
The same behavior could be simulated with a while loop, but it would require a
priming read before the loop starts, which makes the structure slightly less natural:
The flow of execution for a repeat–until loop is captured visually in Figure 11. The
diagram emphasizes that the sequence begins with the body (read and compute the square),
followed by testing the condition. If the condition is false, control flows back to repeat
the body; if true, the loop terminates. This visual reinforcement is useful for students to
grasp why at least one iteration is always guaranteed.
Start
Read n
Write “Square = ” n × n
n=0?
No
Yes
End
Figure 11: Repeat–Until: at least one iteration; stop when condition becomes true.
moves toward the stop condition (for example, the user eventually enters 0, or a counter
decreases). Invariants are properties that remain true after each iteration, such as “every
square printed corresponds to a previously read value.” By tracing a few iterations by
hand, students can verify both termination and invariants.
However, there are common pitfalls to avoid. A frequent mistake is getting the
condition polarity wrong: writing ‘do ... while (stop);‘ produces the opposite of repeat
until stop. Another issue is forgetting that the body always runs at least once, meaning
any variables needed in the first iteration must be initialized or read inside the loop body.
Finally, complex or side-effecting expressions in the stop test should be avoided to keep
the logic clear.
In many algorithms, two simple but extremely powerful programming patterns appear
again and again: the counter and the accumulator. A counter is a variable whose purpose is
to keep track of how many iterations have been executed (commonly named i, k, or count).
An accumulator, on the other hand, is a variable used to aggregate values step by step,
such as computing a running sum or a product. These two patterns, often used together,
form the backbone of loops that process sequences of values. A very classical example is
the computation of the sum of n numbers.
The general idea is straightforward: initialize the sum to zero, then for each of the
n values read from the input, add it to the accumulator. At the end, the accumulator
contains the desired total. Algorithm 21 illustrates this process in pseudocode and listing
5 its C equivalent implementation.
1 S ← 0
2 for i ← 1 to n do
3 read x
4 S ←S+x
5 write “Sum = ”, S
Listing 5: C implementation with counter (loop index) and accumulator (running sum)
long long S = 0, x;
for (int i = 1; i <= n; ++i) {
scanf("%lld", &x);
S += x;
}
printf("Sum = %lld\n", S);
One of the most recurring patterns in algorithm design is the combined use of a counter
and an accumulator. A counter is a variable whose role is to keep track of the number of
iterations performed, often written as i, k, or count. An accumulator, on the other hand,
is a variable used to store and update a running value across iterations, such as a sum or
a product. These two mechanisms appear so often that mastering them is essential for
building correct and efficient loops.
Input : n ∈ N
Output : S = ni=1 xi
P
1 begin
long long S
2 S←0
for (int i
3 for i ← 1 to n do sca
4 read x S +
5 S ←S+x }
6 write “Sum = ”, S printf("Sum
Figure 12: Two complementary views of the same idea: counters (loop index) and
accumulators (running sum).
• Always initialize your accumulator before entering the loop (here, S ← 0). Without
this, the final result would be meaningless.
• Use the counter variable to ensure that the loop executes exactly n times, no more
and no less, which guarantees correctness.
consistently initializing the accumulator, designing the loop with a reliable counter, and
updating both correctly at each step, students gain a robust foundation for solving a wide
range of algorithmic problems with confidence.
In programming, it is not only possible but also very common to place one loop inside
another. This situation is called a nested loop. The outer loop controls the broader
repetition, while the inner loop completes all its iterations every time the outer loop
executes once. A useful mental image is that of a school timetable: for each day of the
week (outer loop), you attend all the scheduled lessons (inner loop). Only when the day is
over does the counter move to the next day. A simple example is printing a rectangle of
stars. Instead of storing anything in a matrix or special data structure, we can directly
use two loops: the outer loop for the rows, and the inner loop for the columns. Figure 13
illustrates this with pseudocode on the left and C code on the right. In both cases, the
logic is the same: for each row, print c stars, then move to the next line.
Input : r, c ∈ N
1 begin for (int i
2 for i ← 1 to r do for
3 for j ← 1 to c do
4 write “*” }
pri
5 write “\n” // move to
}
next line
To understand the flow of control, look at the conceptual flowchart in Figure 14. The
outer loop variable i controls how many rows are printed. For each value of i, the inner
loop variable j runs from 1 to c, printing one star at a time. Once the inner loop finishes,
a newline is written and i increments to move to the next row. This cycle continues until
all r rows have been printed.
Start
i←1
Yes
i≤r? j←1
No
No
j≤c?
write “*”
i←i+1
j ←j+1
End
Figure 14: Flowchart of nested loops: the inner loop repeats fully for each iteration of the
outer loop.
For students, the key to nested loops is to imagine the inner loop as a complete task
that runs to completion before the outer loop can continue. In this rectangle example,
every row is finished (all c stars printed and a newline written) before the next row begins.
By tracing a small case, say r = 2 and c = 3, you can see exactly how six stars are printed
in the first line, then another three in the second line, giving a 2 × 3 block.
Nested loops let us build structured repetitions within repetitions. They are the
foundation for processing grids, matrices, tables, and more advanced algorithmic patterns
such as searching in two dimensions or generating combinations. By mastering the
interaction of counters in both the outer and inner loops, students gain a powerful toolset
for tackling problems where repetition naturally occurs on multiple levels.
The general strategy is simple: initialize an accumulator fact to 1, then loop across
the required values, updating fact each time. At the end of the process, the accumulator
contains the final factorial value. Algorithm 22 expresses this reasoning in pseudocode,
using a for loop to emphasize the initialization, repetition, and accumulation steps.
Input :n ∈ N
Output : n!
1 f act ← 1
2 for i ← 2 to n do
3 f act ← f act × i
4 write f act
Start
Read n
i←2
f act ← 1
No
Write f act
End
In real programming practice, factorial can be implemented with different loop con-
structs. Figure 16 demonstrates three variants in both Python and C: using a for loop,
using a while loop, and using a repeat–until style (in C: do...while, in Python: while
True with a break). The syntax differs, but the underlying algorithmic pattern remains
identical: start with fact = 1, repeat multiplication, and stop once the loop condition is
no longer satisfied.
C (for loop)
long long factorial
Python (for loop) long long f
for (int i
def factorial_for(n):
fac
fact = 1
}
for i in range(2, n+1):
return fact
fact *= i
}
return fact
C (while loop)
Python (while loop)
long long factorial
def factorial_while(n):
long long f
fact, i = 1, 2
int i = 2;
while i <= n:
while (i <=
fact *= i
fac
i += 1
i++
return fact
}
return fact
Python (repeat–until style) }
def factorial_repeat(n):
fact, i = C1,(do–while
2 loop)
if n == 0: # 0! = 1
long long factorial
return 1
long long f
while True:
int i = 2;
fact *= i
if (n == 0)
i += 1
do {
if i > n: # stop condition
fac
break
i++
return fact
} while (i
return fact
}
Figure 16: Factorial implemented with three different loop types in Python and C.
and C. This tabular view reinforces the idea that although the syntax differs across
languages and constructs, the essence of the algorithm is constant.
while
fact, i = 1, 2 fact = 1
while i <= n: while (i
fact *= i
i += 1
}
repeat–until
fact, i = 1, 2 fact = 1
while True: do {
fact *= i
i += 1
if i > n: break } while
In summary, computing factorial provides an excellent case study that bridges abstract
algorithmic thinking and concrete programming practice. Across the different loop con-
structs we studied—for, while, and repeat–until—the essential algorithmic pattern
is always the same: initialize, repeat, accumulate, and stop. What changes is not the
underlying idea, but the placement of the test and the style of iteration. Recognizing this
fact helps students transfer their reasoning seamlessly from pseudocode, to flowcharts, to
actual implementations in C or Python. This transfer builds confidence and flexibility:
you learn to choose the right tool for the situation, while keeping the algorithm clear and
correct.
To summarize the lessons in a compact framework that you can reuse for many problems
involving repetition:
• For loop: use a counter with explicit bounds and step — ideal when the number of
iterations is known in advance.
• While loop: test at the beginning — allows zero, one, or many iterations, depending
on the initial condition.
• Repeat–Until loop: test at the end — guarantees at least one iteration before
stopping.
• Nested loops: repeat one loop completely inside another to construct multi-line
patterns or solve multidimensional problems.
This unified view of loops provides a practical toolkit: you can describe repetition clearly,
select the structure that matches your problem, visualize the process with simple diagrams,
and translate it unambiguously from pseudocode into a working program in standard C
(or any other language).
Together, these methods form the backbone of many algorithmic strategies, from
mathematical computations (factorials, Fibonacci) to constraint satisfaction problems
(Sudoku, N-Queens).
8.1 Recursion
In the previous chapters, we studied loops and iterative structures that allow the repetition
of instructions. These tools are powerful for many tasks, but some problems are more
naturally described by recurrence, such as computing mathematical sequences, evaluating
factorials, or traversing hierarchical structures like trees. In such cases, the concept of
recursion becomes indispensable. Recursion consists of defining an algorithm that calls
itself until a base case is reached, which ensures termination and prevents infinite regress.
This makes recursion one of the most elegant and fundamental ideas in computer science.
At its heart, a recursive algorithm mirrors the self-similar nature of many problems:
it solves a larger instance by reducing it into smaller, simpler ones of the same type,
eventually combining their solutions into the final answer.
(1) the base case, which directly solves the simplest instance of the problem and
guarantees termination; and
(2) the recursive step, which defines how a larger problem can be decomposed into
one or several smaller subinstances, solved recursively, and then combined.
Without a base case, recursion would never stop; without a recursive step, it would
not progress.
Classic examples illustrate these principles clearly. The factorial function is defined
mathematically as
1 if n = 0,
n! =
n · (n − 1)!
if n > 0,
which directly translates into recursive pseudocode (Algorithm 23). Each call to fact(n)
invokes a smaller problem fact(n-1) until the base case 0! = 1 is reached. The call stack
records each pending multiplication, which are resolved in reverse order as the recursion
unwinds.
2 return 1
3 else
4 return n × f act(n − 1)
which can also be expressed recursively (Algorithm 24). Here, each term is defined in terms
of the two preceding ones, and the algorithm makes two recursive calls at every step. This
leads to an elegant but computationally inefficient implementation, as many subproblems
are recomputed. This inefficiency naturally motivates the study of optimization techniques
such as memoization or iterative formulations.
Input :n ∈ N
Output : F (n)
1 if n = 0 then
2 return 0
3 else if n = 1 then
4 return 1
5 else
6 return F (n − 1) + F (n − 2)
trol returns to the previous one. This gradual reversal is known as unwinding the recursion.
This stack-based execution model explains why recursion feels so natural for many
mathematical and hierarchical problems. It allows us to write programs that are compact,
elegant, and very close to the way problems are defined in mathematics (for example,
factorials or the Fibonacci sequence). However, the same mechanism also brings certain
costs. Each new recursive call consumes additional memory on the stack. If the recursion
is very deep (for example, when processing a huge input without careful design), this can
lead to significant overhead or even a stack overflow error, where the program runs out
of memory reserved for the stack. That is why understanding both the beauty and the
limitations of recursion is important for every programmer.
To guide the design of recursive algorithms, computer scientists often rely on a general
recursive pattern. This template captures the essence of most recursive solutions and serves
as a checklist when writing your own algorithms:
1. Base case test. Check whether the current instance is a simplest possible case. If
it is, return the direct answer immediately. This ensures termination and prevents
infinite recursion.
2. Decomposition. If the problem is not a base case, divide it into smaller subproblems
I1 , I2 , . . . , Ik that resemble the original problem in structure.
3. Recursive calls. Solve each subproblem by calling the same algorithm recursively.
At this stage, new stack frames are created and pushed, one for each call.
4. Combination of results. Once the recursive calls finish and their answers are returned,
combine these partial results to produce the solution to the original problem.
This structure can be visualized in a flowchart (see Figure 17), where the algorithm first
checks for a base case, otherwise decomposes the instance, calls itself recursively, and finally
merges the results. Thinking in terms of this general template helps students develop
recursive solutions systematically, and also makes it easier to reason about correctness,
efficiency, and termination.
START
NO
Decompose into subproblems
I1 , I2 , . . . , Ik
(Step 2)
END
• File systems. A folder may contain both files and other subfolders, which in turn
may contain even more files and folders. A recursive algorithm to list all files simply
processes the current folder, and whenever it encounters a subfolder, it calls itself on
that subfolder. This continues until the recursion reaches an empty folder, which is
• Trees. Trees are inherently recursive structures: each tree is made up of smaller
subtrees. Many fundamental operations—such as computing the height of a tree,
counting the total number of nodes, or searching for a specific value—are easily
described recursively. For example, the height of a tree is one plus the maximum of
the heights of its left and right subtrees, with the base case being an empty tree of
height zero.
– Mergesort: divide the list into two halves, sort each half recursively, then merge
them.
– Quicksort: partition the list around a pivot, recursively sort the two partitions,
and combine the results.
– Binary search: compare the target element to the middle of the sorted array;
depending on the result, recursively search either the left or right half.
In all these cases, the recursion continues until the subproblems become trivial (such
as a list of length 1 in sorting), which serves as the base case.
These examples highlight why recursion is not just a programming trick, but a natural
way of thinking about problems whose structure is inherently recursive. By identifying
the base case and recursive step, we can often translate real-world hierarchies directly into
elegant algorithms.
8.2 Backtracking
Backtracking is essentially recursion with the addition of systematic trial and error.
The algorithm incrementally builds a candidate solution and, whenever it detects that
the current partial solution P cannot possibly be extended to a valid full solution, it
backtracks to explore a different branch. This makes backtracking a refined form of
exhaustive search: still exponential in theory, but drastically reduced in practice by pruning.
Start with
partial solution P
No
Backtrack(P )
No
Undo(choice, P )
End
Figure 18: Backtracking flow: enumerate candidates, test feasibility, recurse, and undo.
Backtracking is widely used to solve problems where the solution space is extremely
large and cannot be explored exhaustively in a naive way. The key idea is to build a
solution step by step, and whenever the current partial solution is found to be invalid or
unpromising, the algorithm backtracks—it undoes the last choice and tries a different
path. This makes backtracking a systematic form of trial-and-error search that remains
practical thanks to pruning.
• Sudoku. Filling a 9 × 9 grid so that every row, column, and 3 × 3 box contains digits
1 to 9 exactly once can be framed as a constraint satisfaction problem. Backtracking
chooses an empty cell, tries candidate numbers, and recursively fills the grid. If no
number works, it backtracks to a previous cell and changes the assignment.
• Graph coloring. The goal is to assign colors to graph vertices so that no two adjacent
vertices share the same color. Backtracking selects a vertex, assigns a color, and
recurses to the next vertex. If a conflict arises, it backtracks and tries a different
color. This is widely used in scheduling and register allocation problems.
• Subset sum. Given a set of numbers, the task is to determine whether a subset sums
to a target value. Backtracking decides for each element whether to include it or
not, recursively exploring both possibilities. If the partial sum exceeds the target or
cannot possibly reach it, the algorithm backtracks early and prunes the branch.
In each of these examples, the recursive pseudocode and the corresponding flowchart
(see Figure 18) help students visualize the decision-making process: the algorithm tries a
choice, explores deeper when feasible, and retreats when blocked. This combination of
recursion, pruning, and systematic search illustrates why backtracking is such a versatile
tool for solving complex combinatorial problems.
• Recursion provides a natural and elegant way to express problems that exhibit
self-similarity or hierarchical structure. By breaking a complex task into smaller
instances of the same task, recursive algorithms mirror mathematical definitions and
lead to concise code. However, they come at the cost of extra memory usage due to
repeated function calls and deep call stacks, which may cause inefficiency or stack
overflows if not carefully managed.
• Backtracking extends recursion with the ability to explore large solution spaces
systematically. By pruning unpromising paths and undoing choices when necessary,
backtracking transforms what would otherwise be an infeasible brute-force search
into a practical algorithmic technique. It is particularly powerful in combinatorial
problems such as puzzles, constraint satisfaction, and optimization.
• Both techniques share a common drawback: in the worst case, their time complexity
can still grow exponentially with the problem size. Yet with thoughtful design, effec-
tive pruning, and heuristics (such as memoization, bounding, or ordering strategies),
they become not only usable but also powerful tools that underpin many real-world
applications, from file system traversal to game AI and scheduling.
In summary, recursion gives us a way to think clearly about problems defined in terms
of themselves, while backtracking equips us with a disciplined method to search and
explore possibilities. Together, they form a cornerstone of algorithmic thinking and lay the
foundation for more advanced strategies like dynamic programming and branch-and-bound.
9 Summary
This chapter introduces and formalizes the notion of an algorithm as a finite, ordered
sequence of precise instructions designed to solve a problem. An algorithm must satisfy
essential properties—input, output, definiteness, finiteness, and effectiveness—which dis-
tinguish it from vague or informal strategies. Algorithms are not limited to programming;
they represent a way of reasoning systematically about problem-solving in both computing
and everyday life. To make algorithms concrete and accessible, the chapter surveys four
process rather than a random activity. It begins with problem analysis, proceeds through
decomposition and abstraction, and culminates in iterative refinement for clarity, correct-
ness, and efficiency. This layered workflow—informal description, pseudocode, flowchart,
and program code—ensures solutions are both understandable and implementable. In
essence, algorithms embody the logic and structure that underlie every program. Good
programming is not about memorizing syntax but about thinking algorithmically: starting
from a clear idea, refining it into precise steps, representing it in multiple forms, and finally
translating it into efficient code. This mindset enables the creation of solutions that are
correct, effective, and scalable.
10 Exercises
1. Write an algorithm in natural language to compute the average of three numbers.
2. Draw a flowchart that takes an integer as input and prints whether it is even or odd.
3. Give one example of an algorithm that always terminates and one that may result
in an infinite loop. Explain why.
4. Write pseudocode for finding the maximum of two numbers. Count the number of
elementary steps and determine the time complexity.
6. Write an algorithm to read n integers from the user and print their sum and average.
Algorithm ComputeSquare
Input: n
Output: n^2
Begin
result <- n * n
return result
End
8. Write an algorithm that takes three numbers and prints the largest. Implement it in
pseudocode and explain the control structures used.
10. Write a recursive algorithm to compute the factorial of a number n. Show the
recursion tree for n = 5.
11. Translate the following natural language algorithm into pseudocode: “Start with 0.
For each integer from 1 to n, add it to the total. Print the total.”
12. Discuss the difference between an algorithm and a program. Give an example where
the same algorithm can be implemented in two different programming languages.