Data Structures & Algorithms in C++
Data Structures & Algorithms in C++
Using C++
● Natural Language
● Pseudocode
● Flowchart
2.1.3 Algorithm Complexity
The complexity of an algorithm is the amount of Time or Space required by the algorithm to process
the input and produce the output. Algorithm designers strive to develop algorithms with the lowest
possible time and space/memory complexities, since this makes them more efficient and scalable.
Space-Complexity is represented by function S(n) - memory used versus the input size n.
An algorithm is analyzed using Time Complexity and Space Complexity. Writing an efficient algorithm
help to consume the minimum amount of time for processing the logic.
Experimental Analysis
Write code for every solution, record the time and plot the time on the
graph. Compile and run the following program for both Merge Sort and
selection sort for 10 million elements:
[Link] Selection sort: 10.000.000 elements
In most cases, we are interested in the order of growth of the algorithm instead of the
exact time required for running an algorithm. This time is also known as Asymptotic
Running Time.
The performance of an algorithm can change with a change in the input size. That is
where Asymptotic Notations like Big O Notation comes into play. Asymptotic
Notations can describe an algorithm's run time when the input tends toward a specific or
limiting value. Asymptotic analysis helps to analyze the algorithm performance change in
the order of input size.
Big O Notation
Formal definition: "f(n) is big-O of g(n)" or f(n) = O(g(n)), if
there are two constants c and n1 such that f(n) <=c.g(n) for
all n >= n1; in other words, c.g(n) is an upper bound for f(n)
for all n>= n0. The function f(n) growth is slower than c.g(n).
For a sufficiently large value of input n, the (c.g(n)) will
always be greater than f(n).
If we allow our function g(n) to be n², we can find a constant c = 1, and a N₀ = 0, and so long as N > N₀, N² will
always be greater than N²/2-N/2. We can easily prove this by subtracting N²/2 from both functions, then we can
easily see N²/2 > -N/2 to be true when N > 0. Therefore, we can come up with the conclusion that f(n) = O(n²), in
the other selection sort is “big O squared”.
You might have noticed a little trick here. That is, if you make g(n) grow super fast, way faster than anything,
O(g(n)) will always be great enough. For example, for any polynomial function, you can always be right by saying
that they are O(2ⁿ) because 2ⁿ will eventually outgrow any polynomials.
Mathematically, you are right, but generally when we talk about Big O, we want to know the tight bound of the
function.
Big O Notation for Time Complexity:
Example
Consider the case of Bubble Sort. If the array of N is not sorted, To swap two elements it
take k amount of time, and this must happen N-1 time; the first element will be swapped
with N-1 and take k(N-1) time, the second must be swapped with N-2 elements which
means k(N-2), the last element will be swapped with N-N=0=K amount of time.
1 2 3 4 5 … .. N
T(N)=k(N-1)+k(N-2)+k(N-3)+.....+K
T(N)=K(1+2+3+4+.......+n-1) // As per PMI, the sum of n first numbers is ∑n=n(n+1)/2
T(N)=K(N-1)*N/2
T(N)=KN2/2-K(N)
The constant K/2 will be ignored. And keep N2,
This means that the time complexity for Bubble Sort is O(N 2).
Complexity analysis of algorithms
The complexity of an algorithm are analysed in three categories:
➤Best-Case Complexity: The best-case complexity represents the minimum number of steps
required to execute an algorithm. It provides us with the lower bound of an algorithm.
Note: Worst-case complexity is used to find the guarantee in how much time some particular
algorithm will finish. This is the most important time complexity. If the type of complexity is
not mentioned, then always consider Worst-Case time complexity.
Graphical representation of the Three categories
Growth Functions
Let us look at these growth rates of various functions. The size of input is n
Constant Time, O(1)
An algorithm is said to run in constant time if the output is produced in constant
time, regardless of the input size.
Examples: Accessing an nth element of an Array
Linear Time, O(n)
An algorithm is said to run in linear time if the execution time of the algorithm is
directly proportional to the input size.
Examples:
1. Array operations like search element, find min, find max etc.
[Link] list operations like traversal, find min, find max etc.
Note: If we need to traverse all the nodes of a data structure for some task, then
complexity cant be
less than O(n)
Growth Functions
Logarithmic Time, O(log(n))
An algorithm is said to run in logarithmic time if the execution time of the algorithm
is proportional to the logarithm of the input size. In each step of an algorithm, a
significant portion (e.g. half portion) of the input is pruned/rejected out without
traversing it.
An example is the Binary search algorithm. We will read about this algorithm later.
[Link](n) Time, O([Link](n))
An algorithm is said to run in n*log(n) time if the execution time of an algorithm is
proportional to the product of input size and logarithm of the input size. In these
algorithms, each time the input is divided into half (or some proportion) and each
portion is processed independently.
Examples: Merge-Sort, Quick-Sort (average case), Heap-Sort etc.
Growth Functions
Quadratic Time, O(n^2)
An algorithm is said to run in quadratic time if the execution time of an algorithm is
proportional to th square of the input size. In these algorithms, each element is
compared with all the other elements.
Examples: Bubble-Sort, Selection-Sort, Insertion-Sort
Linear O(n)
Examples
[Link](n) O([Link](n)) 1. 7n2+5n+8=>O(n2)
Quadratic O(n^2) Because n2>>>>n
Polynomial O(n^c) c is a constant and c>1 2. n3+n2=> O(n3)
Exponential O(c^m) c is a constant and c>1 3. 5nn+n3=>O(nn)
Factorial O(n!)
4. 5n2+[Link](n)=>O(n2)
5. n+n log(n)=>O([Link](n))
N power N O(n*n)
Functions Growth
TheRate(
time takenApproximation)
by certain algorithms to run varies dramatically with the size of the
input. Some algorithms take minutes or even seconds to run on huge input, whereas
others may take days to complete their execution. To understand how the rate of
growth changes with the size of the input in different functions, the following table
presents the approximate number of steps required to run an algorithm
Deriving/Hints an Algorithm's Runtime Function
Constants
If any line of code is a statement with basic operations, e.g., comparisons, assignments, or reading a variable, they take constant
time each. Thus, the time complexity of each statement is O(1).
Loops
In loop, a repetition of a particular code for n times, where n is the size of the loop. Every statement inside the loop has a runtime
of O(1). The running time of a loop is a product of the running time of the statement inside a loop and the number of iterations in
the loop. Time Complexity is O(n)
Nested Loops
The running time of nested loops is a product of the running time of the statements inside the loop multiplied by a product of the
size of all the loops. Time Complexity is O(n^c). Where c is the number of loops. For two loops, it will be O(n^2)
Consecutive Statements
In this case, we add the running time of all the consecutive lines of code.
If-Else Statement
In this case, either "if" will run or "else" will run. So, the block with larger runtime will be considered.
Deriving an Algorithm's Runtime Function
Logarithmic Statement
In this case, each iteration will cut the input size into b pieces and consider one of the pieces for the next iteration. Time
complexity in this situation will be O(log(n)).
Time Complexity Examples Example 2.2: Nested loops
#include <iostream>
using namespace std;
Example 2.1: Single loop int fun6 (int n) {
int fun1(int n) { int m = 0;
int m= : 0; for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
for (int i = 0; i<n; i++) {
m += 1;
m += 1; }
} }
} return m;
}
return m; // Testing Code
} int main() {
// Testing Code std::cout << "N = 100, Number of instructions
O(n^2)::fun2(100);
int main() {
return 0;
std::cout<<"N = 100, Number of instructions 0(n) :: " << }
fun1(100); Output:
return 0; N = 100, Number of instructions in 0(n^2)::10000
} Time Complexity: O(n^2), two nested loop, takes
quadratic time. Both the loop is executed in number
Output:
of times, so the internal statement executed n^2 number
N = 100, Number of instructions in 0(n): :100 of times
Time Complexity: O(n), single loop takes linear time
Time Complexity Examples Example 2.4: Arithmetic Progression
#include <iostream>
Example 2.3: Nested Loop using namespace std;
Example 1.3: Nested loops int fun6 (int n) {
int fun3(int n) { int i,j, m = 0;
} for(i=0;i<n;i++){
int i, j, k, m = 0; for(j=0;j<i;j++){
for (i = 0; i < n; i++) { m += 1;
}
for (j= 0; j < n; j++) {
}
} return m;
for (k = 0; k < n; k++) { }
m += 1; // Testing Code
} int main() {
return m; std::cout << "N = 100, Number of instructions O(n^2) :: << fun4 (100);
} return 0;
// Testing Code }
int main() { Output:
N = 100, Number of instructions in O(n^2)::4950
std::cout << "N = 100, Number of instructions 0(n^3) :: " <<
fun3(100); Time Complexity: Statement inside inner loop executes for 1 time in
return 0; first iteration then 2 times then 3 times and so on for n iterations. Total
Output: number of times the inner statement is executed = 1+ 2+ 3 +..... + n.
N = 100, Number of instructions in 0(n^3) ::1000000
<< fun2(100); This series is an arithmetic progression, which sums to n(n+1)/2. So
Time Complexity: O(n^3), All the three nested loops run for n the final time complexity is O(n(n+1)/2) ignoring the constant factors,
number of iterations. So the statement time complexity will be O(n^2).
Time Complexity Examples Example 2.6: Double the iteration variable
#include <iostream>
using namespace std;
Example 2.5 Geometric Progression int fun6 (int n) {
int fun5(int n) {
int i = 1, m = 0;
int i, j, m = 0;
for (i = 1; i <= n; i *= 2) { while (i < n) {
for (j = 0; j <= i; j++) { m += 1;
m += 1; i = i* 2;
} }
return m; return m;
} }
// Testing Code
0122 int main() {
} printf("N = 100, Number of instructions 0(log(n)) :: %d \n", fun6
// Testing Code (100));
int main() { return 0;
std::cout << "N = 100, Number of instructions::”<< fun5 }
(100); 0(n) Output:
return 0; N = 100, Number of inst in 0(log(n)) ::7
} In each iteration, i value is doubled. So the value of i after k
iterations will be 2^k.
Output: 2^k = n... Will be the condition at the time of exit.
N = 100, Number of instructions in 0(n):: 134 log(2^k) = log(n) ....Taking log both sides.
k = log(n)
Time Complexity: The inner loop will run for 1, 2, 4, 8,... Time Complexity: O(log(n))
n times in successive iteration of the outer loop.
T(n) = 0(1+ 2+ 4+...+n/2+n) = O(n)
Time Complexity Examples Problem: Given a value N find N!. Where N! = N* (N-1).... 2*1. Use
Example 2.7: Consecutive Statements recursion to solve the problem.
Example 2. 8: Factorial Calculation.
int fun7(int n) {
int i, j, k, m = 0; int factorial (int i) {
for (i = 0; i<n; i++) { /* Base Case or Termination Condition */
if (i <= 1)
for (j = 0; j<n; j++) { }
m += 1; return 1;
} /* Body, Recursive Expansion */
} return i *factorial (i - 1);
for(i = 0; i < n; i++) { }
for (k= 0; k <n; k++) {
m += 1; int main() {
} std::cout << "Factorial: " << factorial (5) << std::endl;
} return 0;
return m; }
} Output:
// Testing Code Factorial:120
int main() { Analysis: We calculate factorial(i) as i*factorial (i-1) recursively.
std::cout << "N = 100, Number of instructions 0(n^2):: “<< Function F(n) calls F(n-1)
fun7(100);
return 0; T(n) = T(n-1) + 1
Output: T(n-1) = T(n-2) + 1
N = 100, Number of instructions in O(n^2)::20000 T(n-2) = T(n-3) + 1
T(n) = T(n-1) + 1 = (T(n-2) + 1) + 1 = T(n-2) + 2 = (T(n-3) + 1) + 2 =
These two groups of loops are consecutive, so their T(n-3) + 3 Similarly, for kth term T(n) = T(n-k) + k
for base case (n-k) = 1 or n-1=k
complexity will add up to form the final complexity of the program. T(n) = T(1) + n-1 = n
Time Complexity: O(n^2)+ O(n^2) =O(n^2) Time Complexity :O(n)
Time Complexity Examples
Fibonacci number
Problem: Given N, find the Nth number in the Fibonacci
series.
Solution: Fibonacci numbers are calculated by adding
the sum of the previous two numbers.
Example 1.10:
int fibonacci(int n) { Analysis: Recurrence Relation: T(n) = 1 + T(n-1) + T(n-2)
if (n < 2) {
return n; T(n) = 1 + 2T(n-1) // Approximately
} T(n) = 1 + 2T(n-1)
return fibonacci(n-1) + fibonacci(n - 2); T(n-1) = 1 + 2T(n-2)
}
int main() { T(n-2) = 1 + 2T(n-3)
} T(n) = 1 + 2(1+2T(n-2)) = 1 + 2 + 4T(n-2) = 1 +2 +4 (1 +
std::cout << fibonacci (10) << std::endl;
return 0; 2(T(n-3)) = 1+ 2+ 2²+ 2³T(n-3)
} = 1+ 2+2².... + 2nT(0) = 1(2n+1-1)/2-1 = 2n+1 -1
Output:
55 Time complexity is O(2^n), ignoring the constants.
Time Complexity Examples
Problem: n-digit cast iron padlock
FOR i←0 To 10
FOR j←0 To 10
……………….
…………….
nth loop
IF code = combination
BREAK
ENDFOR (nth loop)
ENDFOR
……..
……….
ENDFOR
Time Complexity Examples
Omega Notation – Ω
it gives the best-case scenario of an algorithm’s complexity, opposite to big-O
notation. We can say that: “the amount of Time/space this algorithm takes will grow
no more slowly than this fix), but it could grow more quickly.”
Theta Notation – θ
Theta notation encloses the function from above and below. Since it
represents the upper and the lower bound of the running time of an
algorithm, it is used for analyzing the average-case complexity of an
algorithm
Theta (Average Case) You add the running times for each possible input
combination and take the average in the average case.
2.1.4 Space Complexity
Space complexity refers to the total amount of memory space used by an algorithm/program, including the space of input
values for execution. Calculate the space occupied by variables in an algorithm/program to determine space
[Link] best algorithm/program should have a low level of space complexity. The less space required, the faster it
executes.
Notation
There are different notations we can use to express space complexity measurements. The most widely used is
Big-O Notation describes an asymptotic upper bound. It represents the algorithm’s scalability and performance.
It gives the worst-case scenario of an algorithm’s growth rate. We can say that: “the amount of space this algorithm
takes will grow no more quickly than this f(x), but it could grow more slowly.”
Let’s see a few examples of expressing space complexity using big-O notation, starting from slowest space growth (best)
to fastest (worst):
O(1) – constant complexity – takes the same amount of space regardless of the input size
O(log n) – logarithmic complexity – takes space proportional to the log of the input size
O(n) – linear complexity – takes space directly proportional to the input size
O(n log n) – log-linear/quasilinear complexity – also called “linearithmic”, its space complexity grows proportionally to
the input size and a logarithmic factor
O(n^2) – square/polynomial complexity – space complexity grows proportionally to the square of the input size
Analyzing Space Complexity of Algorithms
Above all, it’s necessary to mention that space complexity depends on a variety
of things such as the programming language, the compiler, or even the
machine running the algorithm.
We’ll use C++ for our examples, although, each of them could be easily applied to
any technology. To apply them to other programming languages, we can simply
replace the C++ data types with language-specific ones. Then in our calculations,
we’d use the sizes that correspond to the language-specific data types.
Example1: Space Complexity
Consider the simple program that sums two integers (numbers without a fractional part):
In this particular method, three variables are used and allocated in memory:
● the first int argument, a
● the second int argument, b
● and the returned sum result which is also an int like a and b
A single integer variable occupies four bytes of memory. In this example, we have three integer
variables. Therefore, this algorithm always takes 12 bytes of memory to complete (3*4 bytes).
We can clearly see that the space complexity is constant, so, it can be expressed in big-O notation as
O(1).
Example2: Space Complexity
Determine the space complexity of a program
that sums all integer elements in an array. #include <iostream>
using namespace std;
int sumArray(int a[], int size) {
Let’s list all variables present in the above code:
int sum = 0;
● array – the function’s only argument – the for (int iterator = 0; iterator < size;
iterator++)
space taken by the array is equal 4n bytes
{
where n is the length of the array sum += a[iterator];
● size – a 4-byte integer }
● sum – a 4-byte integer return sum;
● iterator – a 4-byte integer }
int main() {
int arr[5]={10,20,30,40,50};
The total space needed for this algorithm to
int size = sizeof(a)/sizeof(a[0]);
complete is 4n + 4 + 4 + 4 (bytes). The highest cout << "The sum is
order of n in this equation is just n. Thus, the "<<sumArray(arr,size)<<endl;
return 0;
space complexity of that operation is O(n). }
Conclusion
1. Time Complexity:
Time complexity is a measure of the amount of
time an algorithm takes with respect to the size
of its input. It helps in understanding how the
running time of an algorithm increases as the
size of the input increases. The common
notations used for denoting time complexity are:
Conclusion(Cont)
● Constant Time (O(1)): The algorithm's running time is constant and does
not depend on the size of the input.
● Linear Time (O(n)): The running time of the algorithm increases linearly
with the size of the input.
● Logarithmic Time (O(log n)): The running time of the algorithm increases
logarithmically with the size of the input.
● Quadratic Time (O(n^2)): The running time of the algorithm increases
quadratically with the size of the input.
● Exponential Time (O(2^n)): The running time of the algorithm increases
Conclusion(Cont)
2. Space Complexity:
Space complexity refers to the amount of
memory space that an algorithm requires to solve
a problem. It's a measure of the maximum
amount of memory space used by the algorithm
for a given problem size. Similar to time
complexity, space complexity can also be
denoted using Big O notation.
Conclusion(Cont)
1. [Link]
s-and-algorithms
2. [Link]
3. [Link]
uctures-and-algorithms-c4f9be147a54
2.2 Introduction to Data
Structure analysis
Observe the figures and describe what you
see;
2.2.1 Introduction to Data Structures
Data Structure is a logical or mathematical model for collecting and organizing data in such a
way that we can perform operations on these data in an effective way. Data Structures is about
rendering data elements in terms of some relationship, for better organization and Storage.
Data structures provide a framework for storing and accessing data that
algorithms can operate on.
Data structures and algorithms are often used together to create efficient and effective
software solutions. For example, a sorting algorithm can be used to sort data in an array,
while a search algorithm can be used to find a particular item in a linked list.
Efficient data structures and algorithms are critical to the performance of software systems.
The choice of data structure and algorithm depends on the type of data being processed and
the nature of the problem to be solved. By selecting the most appropriate data structure and
algorithm, software developers can create high-performance solutions that are both efficient
and effective.
Difference and Relationship between Data Structures vs Algorithms
Aspect Data Structures Algorithms
Definition The way data is organized, stored, and retrieved A set of instructions used to solve a specific problem
Importance Essential for efficient data storage and retrieval Crucial for developing efficient software solutions
Performanc The efficiency of data structures determines the The choice of algorithm can have a significant impact on
e efficiency of algorithms that operate on them the performance of the software solution
Array, Linked List, Stack, Queue, Tree, Graph, Sorting, Searching, Graph Traversal, Dynamic
Examples
Hash Table, etc. Programming, Divide and Conquer, etc.
Relationship between Data Structures vs Algorithms Time
Complexity
● Data structures are the foundation of efficient algorithms and play a crucial role in
determining the time complexity of a piece of code.
● Different data structures may have different time and space complexity characteristics
for various operations, which can impact the performance of an algorithm. Time
complexity refers to the amount of time it takes for an algorithm to complete, and it is
a measure of how the runtime of an algorithm grows as the input size increases.
● The choice of data structure is often determined by the specific algorithm being
implemented and can significantly impact the time complexity of a solution, making it
faster or slower.
Example: Arrays are the simplest and most basic data structure. They are contiguous
blocks of memory that store elements in a linear fashion. Accessing, inserting, and deleting
elements from an array have a constant time complexity of O(1), meaning the time taken is
independent of the size of the array. However, inserting or deleting elements from the
middle of an array can be expensive, as it requires shifting all the elements after the
insertion or deletion point.
2.2.2 Abstract Data Type
An abstract data type (ADT) is a logical description of the data and its operations. An ADT is known as a
user's point of view of data. An ADT is concerned about the possible values of data and interfaces exposed
by it. An ADT is not concerned about the actual implementation of the data structure.
Abstract Data Type is a mathematical model of a data structure that specifies the type of data stored, the
operations supported on them and the types of parameters of the operations with no implementation.
Each programming language has its own way to implement abstract data type.
Differently to Data structures, Data Structures are a concrete representations of data and implementation.
Data structures serve as the basis for abstract data types (ADT). The ADT defines the logical form of the data
type. The data structure implements the physical form of the data type
For example:
A user wants to store some integers and find their mean value. ADT for this data structure would have two
functions, one for adding integers and another to get the mean value. ADT for this data structure does not
talk about how exactly it will be implemented .ADT is more of a logical description,
Each programming language has its own way to implement abstract data type.
2.2.3 Basic types of Data Structures
Anything that can store data can be called as a data structure, hence Integer,
Float, Boolean, Char etc., all are data structures. They are known as Primitive Data Structures. And we also have some complex
Data Structures, which are used to store large and connected data and are called non primitive data structure or Abstract Data
Structure. Example: Array, Linked List, Tree, Graph, Stack, Queue etc
Linear vs No-linear
A linear data structure is a type of Data Structure where the data elements get linearly or sequentially
arranged. The elements attach themselves to their next and previous adjacents. The structure involves only
a single level- allowing a user to traverse all its components in a single run.
The linear data structure is very easy to understand and implement due to its linear arrangement, for
example, stack, array, linked list, queue, etc.
Non-Linear Data Structure is a form of data structure where the data elements don’t stay arranged
linearly or sequentially. Since the data structure is non-linear, it does not involve a single level. Therefore, a
user can’t traverse all of its elements in a single run.
The non-linear data structure is not very easy to implement as compared to the linear data structure. The
utilization of computer memory is more efficient in this case, for example, graphs and trees.
Linear vs No-linear
Parameter Linear Data Structure Non-Linear Data Structure
Arrangement In a linear data structure, the data elements The data elements connect to each other hierarchically.
connect to each other sequentially.
Complexity of The linear data structures are comparatively easier The non-linear data structures are comparatively difficult
Implementation to implement. to implement and understand
Levels A user can find all of the data elements at a single One can find all the data elements at multiple levels in a
level in a linear data structure. non-linear data structure.
Traversal You can traverse a linear data structure in a single The users need multiple runs to traverse them
run. completely.
Utilization of It is not very memory-friendly. It means that the The data structure is memory-friendly. It means that it
Memory linear data structures can’t utilize memory very uses memory very efficiently.
efficiently.
Complexity of The time complexity of this data structure is Time complexity often remains the same with an increase
Time directly proportional to input size. in its input size.
Applications Linear data structures work well mainly in the Non-linear data structures work mainly well in image
development of application software. processing and Artificial Intelligence.