Day 2 - Algorithms
Day 2 - Algorithms
and Algorithms
▪Algorithms
Objectives ▪ Big O Notations
Algorithms
▪ A set of steps or instructions for completing a task
Example:
A recipe
A todo-list
driving algorithms
Classic river-crossing puzzle
▪ ቀበሮ፣ ፍየል እና ጎመን puzzle environment
3 Priests and 3 devils Puzzle
▪ There are 3 devils and 3 Priests. They all must cross a river in a boat. Boat can
only carry two people at a time.
▪ As long as there are equal number of devils and priests, then devils will not eat
Priest. If the number of devils are greater than the number of priests on the
same side of the river then devils will eat the priests. So how can we make all
the 6 peoples to arrive to the other side safely?
Algorithmic thinking
▪ Algorithmic thinking is a problem-solving approach that involves breaking down
complex tasks into smaller, more manageable steps or procedures.
▪ It is the process of designing and implementing algorithms to solve specific problems
efficiently and effectively.
▪ Pattern Recognition
Identifying common patterns or structures within the problem that can be
exploited to develop a solution.
▪ Abstraction
Focusing on the essential details while ignoring unnecessary or irrelevant
information to create a simplified representation of the problem.
▪ Efficiency Consideration
Striving to design algorithms that are efficient in terms of time complexity
(execution time) and space complexity (memory usage).
counting-out game [Josephus problem]
1. All of you Stand Up and Think of Number 1:
Each person initially stands up and thinks of the number 1.
2. Pair Off and Add Numbers:
Each person pairs off with someone else standing nearby. They then add
their number to the number of their partner.
3. Sit Down:
After adding numbers, one person from each pair sits down. The decision
of who sits down can be arbitrary or based on certain rules.
4. Repeat:
If there are still people standing, the process repeats. Those who are still
standing pair off with someone else standing nearby, add their numbers,
and one person from each pair sits down.
Let’s play a game
▪ I'm going to play a guessing game with two of you.
▪ [Students are instructed to close their eyes or cover their faces for a
moment so they don't see the next part.]
Alright, now keep that number in mind.
Let's proceed.
First Number: 3
Second Number: 10
Terminologies during our games
▪ Best case scenario
▪ Worst case scenarios
▪ Number of attempts/tries/times or run time
▪ Linear search
▪ Binary search
▪ “random” search
Linear Search during the game
▪ Tries vs N (max value, worst case)
Binary Search during our games
Order of Growth
▪ (In Computers) A set of steps a program takes to finish a task
▪ a finite sequence of instructions, each of which has a clear meaning and can be
performed with a finite amount of effort in a finite length of time
▪ Example:
Ethiopian binary math:
a Shaman in Ethiopian villages:1941
int result = 0;
while(n1 != 1) {
if(n1 % 2 != 0) {
result += n2;
}
n1 = n1 / 2;
n2 = n2 * 2;
}
return result;
}
▪ Algorithms provide step by step instructions on solving specific problems. They
help you solve problems using efficient, standard, and reusable steps
▪ Characteristics of an Algorithm:
Input/Output: defined problem statement, input and output
Determinism: For a given set of inputs, the algorithm should always produce the same output.
Definiteness: Each step of an algorithm must be precisely defined and unambiguous to avoid confusion or misinterpretation.
Finiteness: it doesn't run indefinitely clearly
Effectiveness
Generality: applicable to a wide range of inputs
Feasibility
Correctness: correct output for all valid inputs
Fundamental questions about algorithms
▪ What is it supposed to do? [specification]
▪ Does it really do what it is supposed to do? [verification]
▪ How efficiently does it do it? [performance analysis | efficiency]
▪ It takes a set of input(s) and produces the desired output.
For example:
▪ Time Complexity
▪ Space Complexity
▪ Time complexity of an algorithm quantifies the amount of time taken by an
algorithm to run as a function of the length of the input.
Instruction Space: Its the space required to store the executable version
of
the program. This space is fixed but varies depending upon the
number of lines of code in the program.
Data Space: Its the space required to store all the constants and
variables(including temporary variables) value.
Environment Space: Its the space required to store the environment
information needed to resume the suspended function.
restatement
1.Instruction Space
It's the amount of memory used to save the compiled version of instructions.
2.Environmental Stack
Sometimes an algorithm(function) may be called inside another
algorithm(function). In such a situation, the current variables are pushed onto the
system stack, where they wait for further execution and then the call to the
inside algorithm(function) is made.
For example, If a function A() calls function B() inside it, then all the
variables of the function A() will get stored on the system stack temporarily,
while the function B() is called and executed inside the funciton A().
3.Data Space
Amount of space used by the variables and constants.
Calculating the Space Complexity
▪ For calculating the space complexity, we need to know the value of memory
used by different type of datatype variables, which generally varies for different
operating systems, but the method for calculating the space complexity remains
the same.
Example:
▪ However, it is not a measure of the actual time taken to run an algorithm but a
measure of how the time taken grows with respect to change in size of the
input.
Are we using “Time” timers? No
▪ Hardware dependency
Time measurements can vary based on the hardware the algorithm is
executed on.
▪ External interference
Other processes running on the system can interfere with timer
measurements, affecting accuracy.
▪ Granularity
Timer granularity may be too fine to accurately measure algorithms with
lower time complexity. Using seconds and milliseconds might not
accurately measure algorithms with lower time complexity
▪ Algorithm variation
Timer measurements may not reflect the growth rate of an algorithm's
running time, but rather absolute time for specific inputs.
Solution? Theoretical analysis
▪ So then what do we use if not time? The simple answer is counting operations.
Let’s explore some examples of what this means.
▪ This involves analyzing the algorithm's code and determining the number
of basic operations (such as comparisons and assignments) executed as
a function of the input size. This gives an understanding of the
algorithm's growth rate and thus its time complexity.
Example: Traverse an array: Does x exist in Array
A?
Each of the operation in computer take approximately constant
for i : 1 to length of A time.
if A[i] is equal to x Let each operation takes c time. The number of lines of code
return True executed is actually depends on the value of x.
return False
During analyses of algorithm, mostly we will consider worst
case scenario, i.e., when x is not present in the array A.
If the length of the array will increase the time of execution will also increase.
Find the square of a number
Method 1: Which one do you prefer?
for i=1 to n
do n = n + n Looping n times or just multiplying
// when the loop ends n will hold its square the number by itself?
return n
Hope you choose the second one.
But why?
Method 2:
return n*n
Pseudocode /ˈso͞ odōˌkōd/
▪ A structured representation of algorithmic steps.
▪ Human-readable format, independent of programming syntax.
▪ Acts as a bridge between high-level algorithmic descriptions and code
implementation.
▪ Utilizes natural language, basic control structures, and algorithm-specific
notation.
Linear Search pseudocode
LinearSearch(array, target):
for each element in array:
if element equals target:
return index of element
return "Not found"
▪ The loop runs N times, where N is the length of the array.
▪ If an operation has an O(n) (pronounced ‘O of N’), what this means is
that, as ’n’ increases, so does the amount of operations needed to
complete the problem.
Binary search pseudocode
BinarySearch(array, target):
left = 0
right = length of array – 1
▪ Takeaway
Big O Notation is NOT a measure of how quick your algorithm is, rather a
measure of how much more work an algorithm will have to perform as the
size of its input gets larger.
Order of growth (asymptotic notation, growth
rate)
▪ Order of growth is how the time of execution depends on the length of the
input. In the Array Traverse example, we can clearly see that the time of
execution is linearly depends on the length of the array. Order of growth will
help us to compute the running time with ease.
▪ We use different notation to describe limiting behavior of a function.
▪ You compare space and time complexity using asymptotic analysis. It compares
two algorithms based on changes in their performance as the input size is
increased or decreased.
▪ Asymptotic notation is a mathematical notation used to describe the behavior
and performance of algorithms and data structures as their input size
approaches infinity.
▪ It's a way to analyze the efficiency of algorithms in terms of how their running
time or space requirements grow relative to the size of the input. In data
structures, asymptotic notation is often used to discuss the time complexity of
various operations.
Example: Two algorithms to construct long
strings in Java
String repeat1(char c, int n) {
String answer = "";
for(int j = 0; j < n; j++) {
answer += c;
}
return answer;
}
String repeat2(char c, int n) {
StringBuilder sb = new StringBuilder();
for(int j = 0; j < n; j++) {
sb.append(c);
}
return sb.toString();
}
Results of timing experiment on the methods
Chart of the results of the timing experiment
from Code
Observation
▪ The most striking outcome of these experiments is how much faster the
repeat2 algorithm is relative to repeat1. While repeat1 is already taking more
than 3 days to compose a string of 12.8 million characters, repeat2 is able to do
the same in a fraction of a second.
▪ We also see some interesting trends in how the running times of the algorithms
each depend upon the size of n. As the value of n is doubled, the running time
of repeat1 typically increases more than fourfold, while the running time of
repeat2 approximately doubles.
Asymptotic notations
▪ Mathematical tools used in algorithm analysis to describe the behavior of
functions as their input size approaches infinity.
▪ Asymptotic analysis studies the behavior of functions as their input size
approaches infinity.
▪ It focuses on understanding the limiting behavior of functions rather than their
exact values.
▪ Asymptotic notations, including Big O notation, are used to express the growth
rates of functions in terms of simple, easy-to-understand formulas.
▪ Omega Notation (Ω): This represents a lower bound on the growth rate of an
algorithm's running time. It describes the best-case scenario. If an algorithm has a
time complexity of Ω(g(n)), it means that the running time of the algorithm won't
grow slower than a constant multiple of g(n) for sufficiently large input sizes.
▪ Theta Notation (Θ): This represents both upper and lower bounds on the growth
rate of an algorithm's running time. It indicates the average bound of an
algorithm. It represents the average case of an algorithm's time complexity.
Big O
▪ It indicates the maximum required by an algorithm for all input values. It
represents the worst case of an algorithm's time complexity.
▪ The time complexity of algorithms is most commonly expressed using the big O
notation. It's an asymptotic notation to represent the time complexity.
Example: accessing, Insertion in an Array
▪ You can access any element
using its index with a
constant time.
That is, for any argument n, the constant function f (n) assigns the value c. In other words, it does
not matter what the value of n is; f (n) will always be equal to the constant value c
Because we are most interested in integer functions, the most fundamental constant function is
g(n) = 1, and this is the typical constant function we use in this
session.
Time taken will be proportional to the size of the list or Big O(n), n being the
size of the list.
Big O of operations on Arrays
▪ Time Complexity is most commonly estimated by counting the number of
elementary steps performed by any algorithm to finish execution.
▪ Like in the previous example, for the first code the loop will run n number of
times, so the time complexity will be n at least and as the value of n will
increase the time taken will also increase.
▪ While for the second code, time complexity is constant, because it will never be
dependent on the value of n, it will always give the result in 1 step.
Calculating time complexity
▪ Now the most common metric for calculating time complexity is Big O notation.
This removes all constant factors so that the running time can be estimated in
relation to N, as N approaches infinity.
statement;
▪ Above we have a single statement. Its Time Complexity will be Constant. The
running time of the statement will not change in relation to N.
loop
for(i=0; i < N; i++)
{
statement;
}
▪ It's important to note that the constant factors or lower-order terms are not
considered in big O notation. Whether the loop does a simple operation like
printing or a more complex operation, as long as the number of iterations is
linearly related to the input size, the time complexity will remain O(n).
Linear search example
This time, the time complexity for the above code will be Quadratic.
The running time of the two loops is proportional to the square of N.
When N doubles, the running time increases by N * N.
while(low <= high)
{
mid = (low + high) / 2;
if (target < list[mid])
high = mid - 1;
else if (target > list[mid])
low = mid + 1;
else break;
}
This is an algorithm to break a set of numbers into halves, to search a particular field(we will
study this in detail later). Now, this algorithm will have a Logarithmic Time Complexity.
The running time of the algorithm is proportional to the number of times N can be divided by
2(N is high-low here). This is because the algorithm divides the working area in half with each
iteration.
public void continuallySplit(int n) {
while(n > 0) {
System.out.println(n);
n /= 2;
}
}
Time Complexity:
In each iteration of the loop, the value of n is halved.
The number of loop iterations depends on how many times n can be divided by 2 until it
becomes less than or equal to 0.
Let's denote the initial value of n as n 0.
The number of loop iterations required until n becomes 0 can be calculated as the
logarithm base 2 of n0 , rounded down to the nearest integer.
Therefore, the time complexity of this function is O(log n), where n represents the input
size.
When n = 1, the loop will run once.
When n = 2, the loop will run twice.
When n = 4, the loop will run three times.
When n = 8, the loop will run four times.
And so on, until n = 2^k, the loop will run k times.
In general, the loop will run k times when n = 2 ^ K, where K is the integer
logarithm base 2 of n.
So, the time complexity of the continuallySplit function is O(log n), where n
represents the input size.
▪ NOTE: In general, doing something with every item in one dimension is linear,
doing something with every item in two dimensions is quadratic, and dividing
the working area in half is logarithmic.
Question 1: Calculate the time complexity of the
following code snippet
int count = 0 ;
for (int i = n; i > 0; i /= 2)
for (int j = 0; j < i; j++)
count++;
Solution
▪ Think about how many times count++ will run.
int findSum(int n) {
int sum = 0;
for(int i = 1; i <= n; ++i) {
sum += i;
}
return sum;
}
Solution
Total time taken = time taken by all the statments to
execute
▪ So, the time complexity of the findSum function is O(n), where n represents the
input size.
▪ The big O notation of the above code is O(c0*n) + O(c), where c and c0 are
constants. So, the overall time complexity can be written as O(n) .
▪ In this code, the loop runs from 1 to n (inclusive), and in each iteration, it
performs a constant amount of work: incrementing i and adding its value to the
sum. The loop runs n times because it starts from 1 and goes up to n.
▪ The work done in each iteration is constant, and the number of iterations is
directly proportional to the value of n.
▪ Therefore, the time complexity of this code snippet is O(n), where n is the input
to the findSum function. As n increases, the number of iterations grows linearly,
and the time taken by the function also increases linearly.
Question 4: Finding the sum of the first n
numbers.
int findSum(int n) {
int sum = 0;
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= i; ++j) {
sum++; #NB: this will run [n * (n + 1) / 2]
}
}
return sum;
}
▪ Total time taken = time taken by all the statments to
execute
the statement that is being executed most of the time
is "sum++" i.e. n * (n + 1) / 2