Download as pdf or txt
Download as pdf or txt
You are on page 1of 147

Telegram - https://t.

me/campusdrive
LaunchPad

1 Introduction to
Programming

Thought process to solve a problem :


To solve a problem, what do we usually do?
1. The first thing is to understand the question.
2. Next is to see what values we are already given.
3. Next we think of an approach on how we could use the inputs given into the problem to bring out the
solution. This approach can formally be expressed in two forms- We can use diagrams (aka flowcharts)
to define the solution, or we can use pseudocode to define our solution.
4. Finally, if we have a solution, we code it using a Programming language.

Figure 1 Steps to write a computer program

Using a Computer to solve a problem :


So, how can a computer solve a problem? As far as many of us know, it is a machine that can do some
repetitive calculations. Ok, so lets’ take this problem of finding whether the number 61 is prime or not?
Our approach could be like…
Is 61 divided by 2? No
Is 61 divided by 3? No
.
.
.
Is 61 divided by 2? No
Now, none of the numbers except 1 and 61 seems to completely divide 61. So, 61 is a prime number. Good!
But, there is a lot of repetitive task going on. The task of dividing 61 with another number. For us,
its tedious. Why not engage the computer into it?
So, we will tell the computer to go on dividing 61 by the numbers between 1 and 61. If any of the number
divides, we say that 61 isn’t prime. If none of them divides, then 61 is prime. So, this way we found a
legitimate solution to the given problem.

1
Telegram - https://t.me/campusdrive
LaunchPad

But, an approach in natural language could be vague. For example consider I say, numbers between
1 and 61.
(a) It could be misunderstood by as the numbers including 1 and 61.
(b) Same problem in case of excluding 1 and including 61 (or) including 1 and excluding 61.
(c) Again, we need to tell what is meant by go on dividing by numbers.
So, natural language may lead to ambiguity and hence we need better ways to present out approach.
To express thoughts unambiguously, flowcharts and pseudocodes are used.

Flowcharts :
A flowchart is a diagrammatic representation of an approach, where each step is put into a shape element
(box, circle, etc.) and these elements are connected in the order of their execution.
Let’s look back at the same problem of finding if a number ‘n’ is a prime number or not? We have understood
that the approach would be to divide all numbers between 1 and n, by n. Now, let us draw a flowchart for
it :

Start

Read N

i=2

No Print
Is i < N ?
"IS PRIME"

i=i+1 Yes

Is N
No Yes Print
divisible
by i? "NOT PRIME"

End

Pseudocode :
It resembles programming language, but is more generic in syntax. Its advantage is that programmers who
code in different languages can explain their approach using pseudocode. Let’s see some pseudocodes
and their syntax:

2
Telegram - https://t.me/campusdrive
LaunchPad

Examples of Pseudocode :
1. Find Circumference and Area of a circle.
(i) Read R
(ii) PI = 3.14
(iii) Circumference = 2*PI*R
(iv) Area = PI*R*R
(v) Print Circumference
(vi) Print Area

2. Write a program to add N numbers taken from user.


(i) Sum = 0
(ii) I=1
(iii) Read N
(iv) Until I <= N
Ø Read currentNumber
Ø Sum = Sum + currentNumber
(v) End while
(vi) Print Sum
(vii) Exit

3. Check if a given number N is prime.


(i) Read N
(ii) div = 2
(iii) While div < N do
Ø If N is divisible by div then
q Print “NOT PRIME”
q Exit
Ø End if
Ø div = div + 1
(iv) End while
(v) Print “IS PRIME”
(vi) Exit
4. Give grade to a student based on his/her marks. Above or at 90, give ‘A’, above or at 80 give ‘B’, above
or at 60 give ‘C’ and above or at 40 give ‘D’. Below 40 give ‘F’.

Read N
If N >=90 then
Print “A”

3
Telegram - https://t.me/campusdrive
LaunchPad

Else If N>=80 then


Print “B”
Else If N>=60 then
Print “C”
Else If N>=40 then
Print “D”
Else then
Print “F”
End If
Exit
In the above pseudocode, step number has been omitted. We can skip that in case indentation makes
things clear.
1. Write a pseudocode to generate this pattern given N.
For N=5
1
232
34543
4567654
567898765
Read nLines
curRow = 1
While curRow <= nLines do
nSpace = nLines – curRow
curSpace = 1
While curSpace <= nSpace
Print ‘ ‘
Val = curRow
While val < 2*curRow do
Print val
Val = val + 1
End while
val = (2*curRow) – 2
While val >= curRow
Print val
val = val – 1
End while
Print “\n”
End While
Exit

4
Telegram - https://t.me/campusdrive
LaunchPad

Now, when we are sure about our approach and we have expressed it unambiguously using
flowchart/pseudocode, it is time to tell it to the machine. We code it using a programming language.
This code is then converted into an executable and could be run on machine without the help of source
code.

What is a programming language and why we need it :


A language using which, we can instruct the computer to carry out real life tasks and computations is called
a programming language. It acts as a language in which we could easily express our thoughts to the machine.
Like natural languages, programming language has a fixed set of rules (called grammar or semantics)
according to which programs (source code) could be written in it. These programs are then converted into
a language which machines can understand (we call it binary language). This task is carried out by a special
software (called compiler). Every language has its own complier/interpreter. So for C++ we are going to use
g++ (included in GCC Compiler Suite), for java we’ll use OpenJDK. Once a program is compiled and linked,
its executable is created and the computer can run our program.

C++ compliation process

5
Telegram - https://t.me/campusdrive
LaunchPad

2 Getting Starting with


C++ Programming
In the last chapter, we saw how to write pseudocodes that are more general and express our
thoughts (solution to the problem) using a strict English-like language. In this chapter, we are
going to learn, how to convert a pseudocode into C++ source code that can be compiled and run.
The Classical Hello World :
Now let us start our C++ programming by writing a simple hello world program and understand its
syntax
1: # include <iostream>
2: using namespace std;
3:
4: int main(){
5: //print Hello World
6: cout << “Hello World!”;
7: //print a new line
8: cout << endl;
9: }

We’ll start reading the C++ code in the following sequence.


Line 4 : int main() : This is the line that is executed first whenever an executable is run. The ‘{‘ after
main() defines a block which will be read (translated) sequentially.
Line 5 : It is called a comment. It starts with ‘//’. Any code (as of now) written after ‘//’ is not meant to
be translated by compiler. It serves as a reference for humans making code more readable.
Line 6 : It says that hey computer, just print Hello world on to the console (or screen).
Line 7 : It’s again a comment
Line 8 : It is used to print an empty line (see it as hitting an enter on your keyboard after printing
Hello World)
Line 9 : The ‘}‘ marks the end of the region started in line 4.

This nearly completes the code. However, C++ Compiler doesn’t understand printing operation. So we
tell it to import everything that there in ‘iostream’ (think it as a dictionary) where the operation
printing is define. To achieve this operation Line 1 is written. After doing that, C++ knows that printing
can be done using ‘cout’. So if you use anything, that’s not understandable by C++ compiler, you need
to first define its meaning and then use it.

6
Telegram - https://t.me/campusdrive
LaunchPad

Line 2 : As of now, just write it after all #includes line.


We have learnt how to print on the console using cout << Things to print.
Now we’ll learn how to take input from user.

Taking input from User :


1: # include <iostream>
2: using namespace std;
3:
4: int main(){
5: int x;
6: cout << “Enter value for x “;
7: cin >> x; //Read into x
8: }

Line 5 : It tells compiler that x is a variable that will contain an integer. So we’ll say the data type
for x is int.
Line 7 : Read from keyboard(cin) to x.

Data Types in C++:


There are two types of data types:

Ø Primitive data types: These are basic data types which are used to represent single values.
Primitive data types are predefined.

Ø Non Primitive data types: These are made up of Primitive data types. These are not defined by the C++
programming language, but are created by the user like arrays and strings. We will learn about them
later!

Ø Data type Modifiers: These modifiers are used to increase or decrease the effective range
of certain data types. They have lesser or greater memory usage depending on the
increase/decrease in effective range. The 4 data type modifiers are - long, short, signed, unsigned.

Data Type

Primitive Non-Primitive

String
Void Boolean Numeric
Array
Character Integral etc.

void bool char float double

7
Telegram - https://t.me/campusdrive
LaunchPad

Primitive Data Types : There are 6 types of primitive data types.


1. int: The int data type is a 32-bit (4 bytes) integer having values from –231 to + (231 – 1).
5. char: The char data type is a single 8-bit ASCII character having values from 0 to 28 – 1.
6. float: The float data type is a single-precision 32-bit (4 bytes) floating point.
7. double: The double data type is a double-precision 64-bit (8 bytes) floating point.
8. bool: The bool data type can only contain two values true and false. Its size is 1 byte.
6. void: The void data type shows that it cannot contain anything. A variable cannot be defined for
void type. However, a function which is declared of the void type doesn’t return anything.

The following code, gives an insight to most commonly used data types for this course.
# include <iostream>
using namespace std;

int main(){
int num = 10; //num stores an integer
char alpha = ‘a’; //alpha stores a character. Note the quotes.
float pi = 3.14;
//pi is a floating point variable that stores value 3.14
double h = 6.626e-34;
//floating point numbers can also be written in scientific notation
//6.626e-34 = 6.626 * 10^-34
bool isPrime = true;
//isPrime is a var CAN store just 2 values either 0 or 1
}

Decision Making : Decision making is the integral part of most of the algorithms. This is achieved by
the use of the ‘If-else’ statements!

We will understand it using an example. Consider writing a program to find largest of 3 numbers.
//Largest of 3 Numbers
# include <iostream>
using namespace std;

int main() {
int no1;
int no2;
int no3;

//Read 3 numbers
cin >> no1 >> no2 >> no3; //chaining or cascading
/*
Same as doing

8
Telegram - https://t.me/campusdrive
LaunchPad

cin >> no1;


cin >> no2;
cin >> no3;
*/

if (no1 > no2 and no1 > no3) {


cout << no1 << “ is largest” << endl;
}
else if (no2 > no1 && no2 > no1) {
//same as if (no2 > no1 and no2 > no1)
cout << no2 << “ is largest” << endl;
}
else {
cout << no3 << “ is largest” << endl;
}

Here the condition in the parenthesis of ‘if’ is evaluated. If the condition comes out to be true then
the code in the ‘if’ block is executed but if it comes out as false then the ‘else’ block code is executed.
If more than two conditions are there which needs to be checked then the else if ladder is used and
hence multiple condition can be evaluated.
These conditions can also be nested which means that we can have an ‘if-else’ statements inside an
‘if’ block.
// Largest of 3 Numbers using nested if...else
# include <iostream>
using namespace std;

int main(){
int no1, no2, no3; //3 variables in one row
if (no1 > no2){
if (no1 > no3){
cout << no1 << “ is Largest” << endl;
}
else {
cout << no3 << “ is Largest” << endl;
}
}
else {
//this means no2 is larger than no1
if (no2 > no3){
cout << no2 << “ is Largest” << endl;
}

9
Telegram - https://t.me/campusdrive
LaunchPad

else {
cout << no3 << “ is Largest” << endl;
}
}
}

Looping : Most often while writing code, we need to perform a task over and over again till our work is
done! This can be achieved by the concept of loops. In loops, we check a condition of ‘Is the work done?’
every time before doing the work. Thus, eventually when the work gets finished we break out of that loop.
Note that it is very important to have a breaking condition in the loop else the loop would continue forever
and it will become an infinite loop!
Let us try to convert 2nd pseudocode into C++ code. The task is to add N numbers where N will be given by
user.
//Add N numbers input by user
# include <iostream>
using namespace std;
int main(){
int N;
cin >> N;

int sum = 0;
int x;
int i = 1; //number to be read

while (i <= N){ //Loop begins here.


//equivalent to UNTIL (i <= N)
cin >> x; //read x
sum = sum + x; //add x to sum
i = i + 1; //one number has been read. So read the next number
} //Loop ends here.
cout << sum << endl;
}

10
Telegram - https://t.me/campusdrive
LaunchPad

3 Arrays
Array Declaration and Usage
Suppose that you want to find the average of the marks for a class of 30 students, you certainly do not
want to create 30 variables: mark1, mark2, ..., mark30. Instead, You could use a single variable, called
an array, with 30 elements.
An array is a list of elements of the same type, identified by a pair of square brackets [ ]. To use an
array, you need to declare the array with 3 things: a name, a type and a dimension (or size, or length).
The syntax is:
type arrayName[arraylength]; // arraylength can only be a literal/constant

// I recommend using a plural name for array, e.g., marks, rows, numbers.
int marks[5]; // Declare an int array called marks with 5 elements
double numbers[10]; // Declare an double array of 10 elements
const int SIZE = 9;
float temps[SIZE]; // Use const int as array length
// Some compilers support an variable as array length, e.g.,
int size;
cout << “Enter the length of the array: “;
cin >> size;
float values[size];

Take note that, in C++, the value of the elements are undefined after declaration.
You can also initialize the array during declaration with a comma-separated list of values, as follows:
// Declare and initialize an int array of 3 elements
int numbers[3] = {11, 33, 44};
// If length is omitted, the compiler counts the elements
int numbers[] = {11, 33, 44};
// Number of elements in the initialization shall be equal to or less than
length
int numbers[5] = {11, 33, 44};
// Remaining elements are zero. Confusing! Don’t do this

11
Telegram - https://t.me/campusdrive
LaunchPad

int numbers[2] = {11, 33, 44}; // ERROR: too many initializers


// Use {0} or {} to initialize all elements to 0
int numbers[5] = {0}; // First element to 0, the rest also to zero
int numbers[5] = {}; // All element to 0 too
/* Test local array initialization (TestArrayInit.cpp) */
#include <iostream>
using namespace std;
int main() {
int const SIZE = 5;
int a1[SIZE]; // Uninitialized
for (int i = 0; i < SIZE; ++i) cout << a1[i] << " ";
cout << endl; // ? ? ? ? ?

int a2[SIZE] = {21, 22, 23, 24, 25}; // All elements initialized
for (int i = 0; i < SIZE; ++i) cout << a2[i] << " ";
cout << endl; // 21 22 23 24 25

int a3[] = {31, 32, 33, 34, 35}; // Size deduced from init values
int a3Size = sizeof(a3)/sizeof(int);
cout << "Size is " << a3Size << endl; // 5
for (int i = 0; i < a3Size; ++i) cout << a3[i] << " ";
cout << endl; // 31 32 33 34 35

int a4[SIZE] = {41, 42}; // Leading elements initialized, the rests to 0


for (int i = 0; i < SIZE; ++i) cout << a4[i] << " ";
cout << endl; // 41 42 0 0 0

int a5[SIZE] = {0}; // First elements to 0, the rests to 0 too


for (int i = 0; i < SIZE; ++i) cout << a5[i] << " ";
cout << endl; // 0 0 0 0 0

int a6[SIZE] = {}; // All elements to 0 too


for (int i = 0; i < SIZE; ++i) cout << a6[i] << " ";
cout << endl; // 0 0 0 0 0
}

12
Telegram - https://t.me/campusdrive
LaunchPad

You can refer to an element of an array via an index (or subscript) enclosed within the square bracket
[ ]. C++’s array index begins with zero. For example, suppose that marks is an int array of 5 elements,
then the 5 elements are: marks[0], marks[1], marks[2], marks[3], and marks[4].
// Declare & allocate a 5-element int array
int marks[5];
// Assign values to the elements
marks[0] = 95;
marks[1] = 85;
marks[2] = 77;
marks[3] = 69;
marks[4] = 66;
cout << marks[0] << endl;
cout << marks[3] + marks[4] << endl;

Array Name: a
Array Length: n

Index : 0 1 2 3 n−1

Elements : a[0] a[1] a[2] a[3] ... a[n − 1]

First Element Last Element

To create an array, you need to known the length (or size) of the array in advance, and allocate
accordingly. Once an array is created, its length is fixed and cannot be changed. At times, it is hard to
ascertain the length of an array (e.g., how many students in a class?). Nonetheless, you need to
estimate the length and allocate an upper bound. This is probably the major drawback of using an
array. C++ has a vector template class (and C++11 added an arraytemplate class), which supports
dynamic resizable array.
You can find the array length using expression sizeof(arrayName)/sizeof(arrayName[0]),
where sizeof(arrayName) returns the total bytes of the array and sizeof(arrayName[0]) returns the
bytes of first element.
C/C++ does not perform array index-bound check. In other words, if the index is beyond the array’s
bounds, it does not issue a warning/error. For example,
const int size = 5;
int numbers[size]; // array index from 0 to 4
// Index out of bound!
// Can compiled and run, but could pose very serious side effect!
numbers[88] = 999;
cout << numbers[77] << endl;

13
Telegram - https://t.me/campusdrive
LaunchPad

This is another pitfall of C/C++. Checking the index bound consumes computation power and depicts
the performance. However, it is better to be safe than fast. Newer programming languages such as
Java/C# performs array index bound check.

Array and Loop


Arrays works hand-in-hand with loops. You can process all the elements of an array via a loop,
for example,
/*
* Find the mean and standard deviation of numbers kept in an array
(MeanStdArray.cpp).
*/
#include <iostream>
#include <iomanip>
#include <cmath>
#define SIZE 7
using namespace std;

int main() {
int marks[] = {74, 43, 58, 60, 90, 64, 70};
int sum = 0;
int sumSq = 0;
double mean, stdDev;
for (int i = 0; i < SIZE; ++i) {
sum += marks[i];
sumSq += marks[i]*marks[i];
}
mean = (double)sum/SIZE;
cout << fixed << "Mean is " << setprecision(2) << mean << endl;

stdDev = sqrt((double)sumSq/SIZE - mean*mean);


cout << fixed << "Std dev is " << setprecision(2) << stdDev << endl;

return 0;
}

14
Telegram - https://t.me/campusdrive
LaunchPad

Range-based for loop (C++11) :


C++11 introduces a range-based for loop (or for-each loop) to iterate through an array, as illustrated in
the following example:
/* Testing For-each loop (TestForEach.cpp) */
#include <iostream>
using namespace std;
int main() {
int numbers[] = {11, 22, 33, 44, 55};
// For each member called number of array numbers - read only
for (int number : numbers) {
cout << number << endl;
}
// To modify members, need to use reference (&)
for (int &number : numbers) {
number = 99;
}
for (int number : numbers) {
cout << number << endl;
}
return 0;
}

To compile the program under GNU GCC (g++), you may need to specify option -std=c++0x or -std=c++11:
g++ -std=c++0x -o TestForEach.exe TestForEach.cpp
// or
g++ -std=c++11 -o TestForEach.exe TestForEach.cpp

Multi-Dimensional Array
For example
int mat[2][3] = { {11, 22, 33}, {44, 55, 66} };
0 1 2 3

Row 0 a[0] [0] a[0] [1] a[0] [2] a[0] [3] ...

Row 1 a[1] [0] a[1] [1] a[1] [2] a[1] [3] ...

Row index Column index

15
Telegram - https://t.me/campusdrive
LaunchPad

For 2D array (table), the first index is the row number, second index is the column number.
The elements are stored in a so-called row-major manner, where the column index runs out first.
Example :
/* Test Multi-dimensional Array (Test2DArray.cpp) */
#include <iostream>
using namespace std;
void printArray(const int[][3], int);

int main() {
int myArray[][3] = {{8, 2, 4}, {7, 5, 2}}; // 2x3 initialized
// Only the first index can be omitted and implied
printArray(myArray, 2);
return 0;
}
// Print the contents of rows-by-3 array (columns is fixed)
// Functions coming soon!
void printArray(const int array[][3], int rows) {
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 3; ++j) {
cout << array[i][j] << " ";
}
cout << endl;
}
}

Array of Characters - C-String


In C, a string is a char array terminated by a NULL character ‘\0’ (ASCII code of Hex 0). C++ provides a
new string class under header <string>. The original string in C is known as C-String (or C-style String or
Character String). You could allocate a C-string via:
char message[256]; // Declare a char array
// Can hold a C-String of up to 255 characters terminated by ‘\0’
char str1[] = “Hello”; // Declare and initialize with a “string literal”.
// The length of array is number of characters + 1 (for ‘\0’).
char str1char[] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’}; // Same as above
char str2[256] = “Hello”;
// Length of array is 256, keeping a smaller string of length 5+1=6.

16
Telegram - https://t.me/campusdrive
LaunchPad

Example :
You can use cin and cout to handle C-strings.
cin << reads a string delimited by whitespace;
cin.getline(var, size) reads a string of into var till newline of length up to size-1, discarding the newline
(replaced by ‘\0’). The size typically corresponds to the length of the C-string array.
cin.get(var, size) reads a string till newline, but leaves the newline in the input buffer.
cin.get(), without argument, reads the next character.
/* Test C-string (TestCString.cpp) */
#include <iostream>
using namespace std;

int main() {
char msg[256]; // Hold a string of up to 255 characters (terminated by '\0')

cout << "Enter a message (with space)" << endl;


cin.getline(msg, 256); // Read up to 255 characters into msg
cout << msg << endl;

// Access via null-terminated character array


for (int i = 0; msg[i] != '\0'; ++i) {
cout << msg[i];
}
cout << endl;

cout << "Enter a word (without space)" << endl;


cin >> msg;
cout << msg << endl;

// Access via null-terminated character array


for (int i = 0; msg[i] != '\0'; ++i) {
cout << msg[i];
}
cout << endl;
return 0;
}

17
Telegram - https://t.me/campusdrive
LaunchPad

4 Functions
Say, we want to compute factorial of two different numbers. For that the program would be :
01: //factorial of a number
02:
03: # include <iostream>
04: using namespace std;
05:
06: int main(){
07: int n1, n2;
08: cin >> n1 >> n2;
09:
10: int fact1 = 1;
11: int fact2 = 1;
12:
13: //computing factorial of n1
14: for(int i = 2; i <= n1; ++i) fact1 = fact1 * i;
15:
16: //computing factorial of n2
17: for(int i = 2; i <= n2; ++i) fact2 = fact2 * i;
18:
19: cout << “Factorial of “ << n1 << “ “ << fact1 << endl;
20: cout << “Factorial of “ << n2 << “ “ << fact2 << endl;
21: }

The above program has the following problems :


(a) Repetition of Code : Code in line 14 and 17 performs a task that is logically same i.e. to compute
factorial of a number.
(b) Non-maintainable : If there is some error while developing the solution for factorial and later it needs
to be corrected, then all the lines where factorial is computed need to be updated.
Let us look at the following program that is written using functions.
01: //factorial using function
02:
03: # include <iostream>

18
Telegram - https://t.me/campusdrive
LaunchPad

04: using namespace std;


05:
06: int factorial(int n); //function declaration
07:
08: int main(){
09: int n1, n2;
10: cin >> n1 >> n2;
11:
12: int fact1 = factorial(n1); //function invocation
13: int fact2 = factorial(n2); //or simply function calling
14:
15: cout << “Factorial of “ << n1 << “ “ << fact1 << endl;
16: cout << “Factorial of “ << n2 << “ “ << fact2 << endl;
17: }
18:
19: //function definition
20: int factorial(int n){
21: int fact = 1;
22:
23: //computing factorial of n
24: for(int i = 2; i <= n; ++i) fact = fact * i;
25:
26: return fact;
27: }

Here the code for computing factorial has been written only once [20, 27]1 but used twice [12], [13].
[6] is a function declaration that provides the following 4 information about the function.
(a) Function name : factorial
(b) Return type : int
(c) Number of arguments : 1
(d) Function arguments : a variable of int type
Note that it does NOT tell us anything about what the function does. It just tell us how it looks.

Function definition starts from [20]. It tells the compiler what the function does and how it does so.
Had the function definition done before function calling [12], the function declaration becomes optional.
1
[i] represents ith line number

19
Telegram - https://t.me/campusdrive
LaunchPad

Now we can define function formally. A function is a set of code which is referred to by a name and can be
called (invoked) at any point in the program by using the function’s name. It is like a subprogram where we
can pass parameters to it and it can even return a value.

By the return type we mean that we specify the data type of the return value. It may also happen sometimes
that the function does not return anything. In that case the return type of the function should be ‘void’.
Ø If no return type is mentioned, by default int is assumed.
Ø If no return statement exists in a function which is expected to return some value, some garbage value
is returned.
Ø You cannot return a value from a function with void return type.

Function Calling :
Now just making the function would not execute it even when the program is run. To execute any
function we need to invoke it. This is called calling the function. Function can be called from the main
function and also from other functions. The called function must either be declared or be defined
before the calling function. Otherwise, the calling function cannot find the called function, and it will
lead to a compile time error. If there are any parameters to be given to the function, they are also
passed within the parenthesis. If the function returns any value it can be stored in a variable. Now let
us write two functions and see how they can be called!
When the function is called, we can pass the parameters of the same data type as specified in its
definition. Note that while calling the function we only pass in the names of the variables or values
and don’t specify its data type!
01: //sum function
02: # include <iostream>
03: using namespace std;
04:
05: int add(int num1, int b){
06: return num1 + b;
07: }
08:
09: int main(){
10: int num1 = 3;
11: int num2 = 5;
12: int sum = add(num1, num2);
13: cout << sum << endl;
14: return 0;
15: }

20
Telegram - https://t.me/campusdrive
LaunchPad

In the above code, we have written a function for adding two numbers. An important point to note
here is that the variables present in the main function are different from those that are present in the
add function, though they might have same names. To understand this better let us learn about the
concept of variables and their scope.
Variables and their Scope :
During runtime a memory pool is created by the program to store all the variables, codes for functions, etc.
In this memory pool there are majorly two types of memory – the stack memory and the heap memory.
Any variables that are statically declared (their size if known at compile time) are stored in the stack
memory, whereas any variables/objects that are created dynamically at runtime (their size is not known at
compile time), are created in the heap memory. All the functions that are called occupy their area on the
stack where all their variables are stored. The functions keep stacking one above the other in the order of
their calls. When the function returns, it is erased from the stack and all its local variables are destroyed.

The variables that are present in one function are local to only that function. It cannot be accessed outside
that function. For instance a variable made in the main function is not available to the add function unless
we pass it as a parameter to it. However, using pass by reference, a variable defined in one function can be
accessed in other function.
Consider the following example of swapping to numbers using function.
01: //call by reference
02: # include <iostream>
03: using namespace std;
04:
05: void swp1(int x, int y){
06: int tmp = x;
07: x = y;
08: y = tmp;
09: }
10:
11: void swp2(int& x, int& y){
12: //x and y are references...call by reference
13: int tmp = x;
14: x = y;
15: y = tmp;
16: }
17:
18: int main() {
19: int a, b;

21
Telegram - https://t.me/campusdrive
LaunchPad

20: cin >> a >> b;


21:
22: swp1(a, b);
23: cout << a << “ “ << b << endl; //No swapping
24:
25: swp2(a, b);
26: cout << a << “ “ << b << endl; //values actually swapped
27: }

Note the difference in line 5 and line 11. In line 11, & is used to refer to an already existing VARAIBLE.
Also, there is no change in the calling of function.
Difference between Pass by Value v/s Pass by Reference:

22
Telegram - https://t.me/campusdrive
LaunchPad

5 Recursion
Recursion is a technique (just like loops) where the solution to a problem depends on the solution to
smaller instances of that problem. In cases where we need to solve a bigger problem, we try to reduce that
bigger problem in a number of smaller problems and then solve them. For instance we need to calculate 28.
We can do this by first calculating 24 and then multiplying 24 with 24. Now to calculate 24, we can calculate 22
which in turn depend on just 21. So for calculating 28, we just need to calculate 21 and then keep on
multiplying the obtained numbers until we reach the result.
So by dividing the problem in smaller problems we can solve the original program easily. But we can’t go on
making our problem smaller infinitely. We need to stop somewhere and terminate. This point is called a
base case. The base case tells us that the problem doesn’t have to be divided further. For instance, in the
above case when power of 2 equals to 1 we can return 2. So power being equal to 1 becomes our base case.
int power(int x,int n)
{
if(n==1)
{
return x;
}
int smallPow=power(x,n/2);
if(n%2==0) {
return smallPow*smallPow;
}
else {
return smallPow*smallPow*x;
}
}

In the next program we are calculating the factorial of a number.


For calculating the factorial of n, all we need is the factorial for n-1 and then multiply it with the number
itself. As we can see that factorial of a bigger number depends on the factorial of a smaller number, thus
this problem can be solved by recursion.
So for calculation of factorial of a bigger number, we first calculate factorial of n-1 and then multiply it by n.
If the number is 1, we don’t need to calculate anything but just return 1. Thus this is our base case.
Notice that in the above code the factorial function calls itself on a smaller input and then use that result
to calculate the final answer. Thus in recursion, a function calls itself.

23
Telegram - https://t.me/campusdrive
LaunchPad

An important point to note here is that in the base case we need to have a return statement compulsorily
even if the return type of the function is void. This is because even in the base case if we don’t have the
return statement, the code after the base case would begin to execute and the recursion would execute
infinitely. Thus to terminate recursion, we need to have a return statement in the base case.
For factorial of 5:

Let us understand the recursive function calls through memory maps:

Now we know that every recursive function includes a base case and a call to itself. So every time the
function calls itself we have two opportunities to do our work. First is before calling itself and second is
after the call. First opportunity helps to do work on the input whereas second opportunity lets us to do
work on the result for the smaller problem. To understand this better let us write code for printing a series
of increasing and decreasing numbers.
void printID(int n)
{
if(n==0)
return;

cout<<n<<endl;
printID(n-1);
cout<<n<<endl;
}
Every time a recursive function is called, the called function is pushed into the running program stack over
the calling function. This uses up memory equal to the data members of a function. So, using recursion is
recommended only upto certain depth. Otherwise too much memory would be wasted in stack space for
program.

24
Telegram - https://t.me/campusdrive
LaunchPad

Limitation of Recursive Call :


There is an upper limit to the number of recursive calls that can be made. To prevent this make
sure that your base case is reached before stack size limit exceeds.
So, if we want to solve a problem using recursion, then we need to make sure that:
Ø The problem can broken down into smaller problems of same type.
Ø Problem has some base case(s).
Ø Base case is reached before the stack size limit exceeds.

Backtracking :
When we solve a problem using recursion, we break the given problem into smaller ones.
Let’s say we have a problem PROB and we divided it into three smaller problems P1, P2 and P3. Now
it may be the case that the solution to PROB does not depend on all the three subproblems, in fact we
don’t even know on which one it depends.
Let’s take a situation. Suppose you are standing in front of three tunnels, one of which is having a bag
of gold at its end, but you don’t know which one. So you’ll try all three. First go in tunnel 1, if that is
not the one, then come out of it, and go into tunnel 2, and again if that is not the one, come out of it
and go into tunnel 3. So basically in backtracking we attempt solving a subproblem, and if we don’t
reach the desired solution, then undo whatever we did for solving that subproblem, and again try
solving another subproblem.
Basically Backtracking is an algorithmic paradigm that tries different solutions until finds a solution
that “works”. Problems which are typically solved using backtracking technique have following property
in common. These problems can only be solved by trying every possible configuration and each
configuration is tried only once.

Note : We can solve this by using recursion method.


dead end

?
dead end
dead end

?
Start ? ?
dead end
dead end
?

success!

25
Telegram - https://t.me/campusdrive
LaunchPad

Problems based on Backtracking :


1. N-Queen Problem : Given a chess board having N×N cells, we need to place N queens in such a
way that no queen is attacked by any other queen. A queen can attack horizontally, vertically and
diagonally.

Approach towards the Solution:


We are having N × N unattacked cells where we need to place N- queens. Let’s place the first
queen at a cell (i, j), so now the number of unattacked cells is reduced, and number of queens
to be placed is N – 1. Place the next queen at some unattacked cell. This again reduces the number
of unattacked cells and number of queens to be placed becomes N – 2. Continue doing this, as long
as following conditions hold.
Ø The number of unattacked cells is not equal to 0.
Ø The number of queens to be placed is not equal to 0.
If the number of queens to be placed becomes 0, then it’s over, we found a solution. But if the
number of unattacked cells become 0, then we need to backtrack, i.e. remove the last placed
queen from its current cell, and place it at some other cell.
Let’s N = 4, We have to place 4 queen in 4×4 cells.

26
Telegram - https://t.me/campusdrive
LaunchPad

Place Queen 1
at (1, 1)

Place Queen 2 Place Queen 3


at (2, 3) at (4, 2)

No more valid cells


Backtrack

Place Queen 2 No more valid


at (2, 4) cells backtrack

Place Queen 3
at (3, 2)

No more valid Place Queen 3


cells backtrack at (4, 3)

No more valid cells


Backtrack

Place Queen 2 No more valid


at (3, 2) cells backtrack
And so on ...

So, final solution of this problem is

So, clearly, we tries solving a subproblem, if that does not result in the solution, it undo whatever
changes were made and solve the next subproblem. If the solution does not exists ( like N=2),
then it returns false.

27
Telegram - https://t.me/campusdrive
LaunchPad

4. Rat in a Maze :
Given a maze, NxN matrix. A rat has to find a path from source to des-ti-na-tion. maze[0][0]
(left top corner)is the source and maze[N-1][N-1](right bot-tom cor-ner) is des-ti-na-tion. There
are few cells which are blocked, means rat can-not enter into those cells. The rat can move
only in two directions: forward and down.

Approach Towards the Solution :


Here we have to generate all paths from source to destination and one by one check if the
generated path satisfies the constraints or not.
Let’s the given maze is in the form of matrix whose entries are either 0 or 1. Entry 0 represent
the block path from where the rat can’t move. We have to print another matrix whose entries
are either 0 or 1. In output matrix, entry 1 represents the the path of Rat from starting point to
destination point.

Algorithm to generate all the Paths :


While there are untried paths
{
Generate the next paths
If this path has all blocks as 1
{
Print this path;
}
}

Backtracking Algorithm :
If destination is reached
Print the solution
Else
{
a) Mark current cell in solution matrix as 1.
b) Move forward in horizontal direction and recursively check if this move leads to a solution.
c) If the move chosen in the above step doesn’t lead to a solution then move down and check of this
move leads to a solution.
d) If none of the above solutions work then unmark this cell as 0 (Backtrack) and return false.
}

28
Telegram - https://t.me/campusdrive
LaunchPad

6 Pointers
Pointers, References and Dynamic Memory Allocation are the most powerful features in C/C++
language, which allows programmers to directly manipulate memory to efficiently manage the
memory - the most critical and scarce resource in computer - for best performance. However, “pointer”
is also the most complex and difficult feature in C/C++ language.

Pointers are extremely powerful because they allows you to access addresses and manipulate
their contents. But they are also extremely complex to handle. Using them correctly, they could
greatly improve the efficiency and performance. On the other hand, using them incorrectly could
lead to many problems, from un-readable and un-maintainable codes, to infamous bugs such as
memory leaks and buffer overflow, which may expose your system to hacking. Many new
languages (such as Java and C#) remove pointer from their syntax to avoid the pitfalls of pointers,
by providing automatic memory management.

Although you can write C/C++ programs without using pointers, however, it is difficult not to
mention pointer in teaching C/C++ language. Pointer is probably not meant for novices and dummies.

Pointer Variables :
A computer memory location has an address and holds a content. The address is a numerical number
(often expressed in hexadecimal), which is hard for programmers to use directly. Typically, each address
location holds 8-bit (i.e., 1-byte) of data. It is entirely up to the programmer to interpret the meaning
of the data, such as integer, real number, characters or strings.
To ease the burden of programming using numerical address and programmer-interpreted data,
early programming languages (such as C) introduce the concept of variables. A variable is
a named location that can store a value of a particular type. Instead of numerical addresses, names (or
identifiers) are attached to certain addresses. Also, types (such as int, double, char) are associated
with the contents for ease of interpretation of data.
Each address location typically hold 8-bit (i.e., 1-byte) of data. A 4-byte int value occupies 4 memory
locations. A 32-bit system typically uses 32-bit addresses. To store a 32-bit address, 4 memory locations
are required.
The following diagram illustrate the relationship between computers’ memory address and content;
and variable’s name, type and value used by the programmers.

29
Telegram - https://t.me/campusdrive
LaunchPad

Pointer Variables (or Pointers) :


A pointer variable (or pointer in short) is basically the same as the other variables, which can store a
piece of data. Unlike normal variable which stores a value (such as an int, a double, a char), a pointer
stores a memory address.

Declaring Pointers :
Pointers must be declared before they can be used, just like a normal variable. The syntax of declaring
a pointer is to place a * in front of the name. A pointer is associated with a type (such as int and double)
too.
type *ptr; // Declare a pointer variable called ptr as a pointer of type
// or
type * ptr;
// or
type *ptr ; // I shall adopt this convention

For Example,
int * iPtr;
// Declare a pointer variable called iPtr pointing to an int (an int pointer)
// It contains an address. That address holds an int value.
double * dPtr; // Declare a double pointer

Take note that you need to place a * in front of each pointer variable, in other words, * applies only to
the name that followed. The * in the declaration statement is not an operator, but indicates that the
name followed is a pointer variable. For example,
int *p1, *p2, i ; // p1 and p2 are int pointers. i is an int
int *p1, p2, i ; // p1 is a int pointer, p2 and i are int
int *p1, *p2, i ; // p1 and p2 are int pointers, i is an int

Naming Convention of Pointers: Include a “p” or “ptr” as prefix or suffix,


e.g., iPtr, numberPtr, pNumber, pStudent.

Initializing Pointers via the Address-Of Operator (&) :


When you declare a pointer variable, its content is not initialized. In other words, it contains an
address of “somewhere”, which is of course not a valid location. This is dangerous! You need to initialize
a pointer by assigning it a valid address. This is normally done via the address-of operator (&).
The address-of operator (&) operates on a variable, and returns the address of the variable.
For example, if number is an int variable, &number returns the address of the variable number.

30
Telegram - https://t.me/campusdrive
LaunchPad

You can use the address-of operator to get the address of a variable, and assign the address to a
pointer variable. For example,
int number = 88 ; // An int variable with a value
int * pNumber ;
// Declare a pointer variable called pNumber pointing to an int (or int
pointer)
pNumber = &number;
// Assign the address of the variable number to pointer pNumber.
int * pAnther = &number;
// Declare another int pointer and init to address of the variable number

As illustrated, the int variable number, starting at address 0x22ccec, contains an int value 88.
The expression &number returns the address of the variable number, which is 0x22ccec. This address
is then assigned to the pointer variable pNumber, as its initial value.
The address-of operator (&) can only be used on the RHS.

Indirection or Dereferencing Operator (*) :


The indirection operator (or dereferencing operator) (*) operates on a pointer, and returns
the value stored in the address kept in the pointer variable. For example, if pNumber is
an int pointer, *pNumberreturns the int value “pointed to” by pNumber.

For Example,
int number 88;
int * pNumber = &number;
// Declare and assign the address of variable number to pointer pNumber (0x22ccec)
cout << pNumber << endl ;
// Print the content of the pointer variable, which contain an address (0x22ccec)
cout << *pNumber << endl;
// Print the value “pointed to” by the pointer, which is an int (88)
*pNumber = 99;
// Assign a value to where the pointer is pointed to, NOT to the pointer
variable

31
Telegram - https://t.me/campusdrive
LaunchPad

cout << *pNumber << endl;


// Print the new value “pointed to” by the pointer (99)
cout << number << endl;
// The value of variable number changes as well (99)

Take note that pNumber stores a memory address location, whereas *pNumber refers to the value
stored in the address kept in the pointer variable, or the value pointed to by the pointer.
As illustrated, a variable (such as number) directly references a value, whereas a pointer indirectly
references a value through the memory address it stores. Referencing a value indirectly via a pointer
is called indirection or dereferencing.
The indirection operator (*) can be used in both the RHS (temp = *pNumber) and the LHS (*pNumber
= 99) of an assignment statement.
Take note that the symbol * has different meaning in a declaration statement and in an expression.
When it is used in a declaration (e.g., int * pNumber), it denotes that the name followed is a pointer
variable. Whereas when it is used in a expression (e.g., *pNumber = 99; temp << *pNumber;),
it refers to the value pointed to by the pointer variable.

Pointer has a Type Too :


A pointer is associated with a type (of the value it points to), which is specified during declaration.
A pointer can only hold an address of the declared type; it cannot hold an address of a different type.
int i = 88;
double d = 55.66;
int *iPtr = &i; // int pointer pointing to an int value
double * dPtr = &d ; // double pointer pointing to a double value
iPtr = &d ; // ERROR, cannot hold address of different type
dPtr = &i ; // ERROR
iPtr = i ; // ERROR, pointer holds address of an int, NOT int value
int j = 99 ;
iPtr = &j ; // You can change the address stored in a pointer

Uninitialized Pointers :
The following code fragment has a serious logical error!
int * iPtr;
*iPtr = 55;
cout << *iPtr << endl;

Null Pointers :
You can initialize a pointer to 0 or NULL, i.e., it points to nothing. It is called a null pointer. Dereferencing
a null pointer (*p) causes an STATUS_ACCESS_VIOLATION exception.

32
Telegram - https://t.me/campusdrive
LaunchPad

int * iPtr = 0 ;
// Declare an int pointer, and initialize the pointer to point to nothing
cout << *iPtr << endl;
// ERROR! STATUS_ACCESS_VIOLATION exception
int * p = NULL ; // Also declare a NULL pointer points to nothing

Initialize a pointer to null during declaration is a good software engineering practice.


C++11 introduces a new keyword called nullptr to represent null pointer.

Reference Variables
C++ added the so-called reference variables (or references in short). A reference is an alias,
or an alternate name to an existing variable. For example, suppose you make peter a reference (alias)
to paul, you can refer to the person as either peter or paul.
The main use of references is acting as function formal parameters to support pass-by-reference. In
an reference variable is passed into a function, the function works on the original copy (instead of a
clone copy in pass-by-value). Changes inside the function are reflected outside the function.
A reference is similar to a pointer. In many cases, a reference can be used as an alternative to pointer,
in particular, for the function parameter.

References (or Aliases) (&) :


Recall that C/C++ use & to denote the address-of operator in an expression. C++ assigns an additional
meaning to & in declaration to declare a reference variable.
The meaning of symbol & is different in an expression and in a declaration. When it is used in an
expression, & denotes the address-of operator, which returns the address of a variable,
e.g., if number is an intvariable, &number returns the address of the variable number (this has been
described in the above section).
Howeve, when & is used in a declaration (including function formal parameters), it is part of the type
identifier and is used to declare a reference variable (or reference or alias or alternate name). It is used
to provide another name, or another reference, or alias to an existing variable.
The syntax is as follow:
type &newName = existingName ;
// or
type&newName = existingName ;
// or
type&newName = existingName ; // I shall adopt this convention

It shall be read as “newName is a reference to exisitngName”, or “newNew is an alias of existingName”.


You can now refer to the variable as newName or existingName.

33
Telegram - https://t.me/campusdrive
LaunchPad

For example,
/* Test reference declaration and initialization (TestReferenceDeclaration.cpp) */
#include <iostream>
using namespace std;

int main() {
int number = 88; // Declare an int variable called number
int & refNumber = number;
// Declare a reference (alias) to the variable number
// Both refNumber and number refer to the same value

cout << number << endl; // Print value of variable number (88)
cout <<refNumber<< endl; // Print value of reference (88)

refNumber = 99; // Re-assign a new value to refNumber


cout <<refNumber<< endl;
cout << number << endl; // Value of number also changes (99)

number = 55; // Re-assign a new value to number


cout << number << endl;
cout <<refNumber<< endl; // Value of refNumber also changes (55)
}

How References Work?


A reference works as a pointer. A reference is declared as an alias of a variable. It stores the address
of the variable, as illustrated:

34
Telegram - https://t.me/campusdrive
LaunchPad

References vs. Pointers :


Pointers and references are equivalent, except:
1. A reference is a name constant for an address. You need to initialize the reference during
declaration.
int & iRef; // Error : ‘iRef’ declared as reference but not initialized
Once a reference is established to a variable, you cannot change the reference to reference another
variable.
2. To get the value pointed to by a pointer, you need to use the dereferencing operator
* (e.g., if pNumber is a int pointer, *pNumber returns the value pointed to by pNumber. It is
called dereferencing or indirection). To assign an address of a variable into a pointer, you need to
use the address-of operator & (e.g., pNumber = &number).
On the other hand, referencing and dereferencing are done on the references implicitly.
For example, if refNumber is a reference (alias) to another int variable, refNumber returns the
value of the variable. No explicit dereferencing operator * should be used. Furthermore, to assign
an address of a variable to a reference variable, no address-of operator & is needed.

For Example,
/* References vs. Pointers (TestReferenceVsPointer.cpp) */
#include <iostream>
using namespace std;

int main() {
int number1 = 88, number2 = 22;

// Create a pointer pointing to number1


int * pNumber1 = &number1; // Explicit referencing
*pNumber1 = 99; // Explicit dereferencing
cout << *pNumber1 << endl; // 99
cout <<&number1 << endl; // 0x22ff18
cout << pNumber1 << endl;
// 0x22ff18 (content of the pointer variable - same as above)
cout <<&pNumber1 << endl; // 0x22ff10 (address of the pointer variable)
pNumber1 = &number2; // Pointer can be reassigned to store another address

// Create a reference (alias) to number1


int & refNumber1 = number1; // Implicit referencing (NOT &number1)
refNumber1 = 11; // Implicit dereferencing (NOT *refNumber1)
cout << refNumber1 << endl; // 11

35
Telegram - https://t.me/campusdrive
LaunchPad

cout <<&refNumber1 << endl; // 0x22ff18


//refNumber1 = &number2; // Error! Reference cannot be re-assigned
// error: invalid conversion from ‘int*’ to ‘int’
refNumber1 = number2; // refNumber1 is still an alias to number1.
// Assign value of number2 (22) to refNumber1 (and number1).
number2++;
cout << refNumber1 << endl; // 22
cout << number1 << endl; // 22
cout << number2 << endl; // 23
}

A reference variable provides a new name to an existing variable. It is dereferenced implicitly and
does not need the dereferencing operator * to retrieve the value referenced. On the other hand,
a pointer variable stores an address. You can change the address value stored in a pointer. To retrieve
the value pointed to by a pointer, you need to use the indirection operator *, which is known as explicit
dereferencing. Reference can be treated as a const pointer. It has to be initialized during declaration,
and its content cannot be changed.
Reference is closely related to pointer. In many cases, it can be used as an alternative to pointer.
A reference allows you to manipulate an object using pointer, but without the pointer syntax of
referencing and dereferencing.
The above example illustrates how reference works, but does not show its typical usage, which is
used as the function formal parameter for pass-by-reference.

Pass-By-Reference into Functions with Reference Arguments vs. Pointer Arguments


Pass-by-Value
In C/C++, by default, arguments are passed into functions by value (except arrays which is treated
as pointers). That is, a clone copy of the argument is made and passed into the function. Changes to
the clone copy inside the function has no effect to the original argument in the caller. In other words,
the called function has no access to the variables in the caller. For example,
/* Pass-by-value into function (TestPassByValue.cpp) */
#include <iostream>
using namespace std;
int square(int);
int main() {
int number = 8;
cout << “In main(): “ <<&number << endl; // 0x22ff1c
cout << number << endl; // 8

36
Telegram - https://t.me/campusdrive
LaunchPad

cout << square(number) << endl; // 64


cout << number << endl; // 8 - no change
}

int square(int n) { // non-const


cout << “In square(): “ <<&n << endl; // 0x22ff00
n *= n; // clone modified inside the function
return n;
}

The output clearly shows that there are two different addresses.
Pass-by-Reference with Pointer Arguments
In many situations, we may wish to modify the original copy directly (especially in passing huge object
or array) to avoid the overhead of cloning. This can be done by passing a pointer of the object into the
function, known as pass-by-reference. For example,
/* Pass-by-reference using pointer (TestPassByPointer.cpp) */
#include <iostream>
using namespace std;
void square(int *);
int main() {
int number = 8;
cout << “In main(): “ <<&number << endl; // 0x22ff1c
cout << number << endl; // 8
square(&number); // Explicit referencing to pass an address
cout << number << endl; // 64
}
void square(int * pNumber) { // Function takes an int pointer (non-const)
cout << “In square(): “ << pNumber << endl; // 0x22ff1c
*pNumber *= *pNumber; // Explicit de-referencing to get the value pointed-to
}

The called function operates on the same address, and can thus modify the variable in the caller.

37
Telegram - https://t.me/campusdrive
LaunchPad

Pass-by-Reference with Reference Arguments


Instead of passing pointers into function, you could also pass references into function, to avoid
the clumsy syntax of referencing and dereferencing. For example,
/* Pass-by-reference using reference (TestPassByReference.cpp) */
#include <iostream>
using namespace std;

void square(int &);

int main() {
int number = 8;
cout << “In main(): “ <<&number << endl; // 0x22ff1c
cout << number << endl; // 8
square(number); // Implicit referencing (without ‘&’)
cout << number << endl; // 64
}

void square(int & rNumber) { // Function takes an int reference (non-const)


cout << “In square(): “ <<&rNumber << endl; // 0x22ff1c
rNumber *= rNumber; // Implicit de-referencing (without ‘*’)
}

Again, the output shows that the called function operates on the same address, and can thus modify
the caller’s variable.
Take note referencing (in the caller) and dereferencing (in the function) are done implicitly. The only
coding difference with pass-by-value is in the function’s parameter declaration.
Recall that references are to be initialized during declaration. In the case of function formal parameter,
the references are initialized when the function is invoked, to the caller’s arguments.
References are primarily used in passing reference in/out of functions to allow the called function
accesses variables in the caller directly.

“const” Function Reference/Pointer Parameters


A const function formal parameter cannot be modified inside the function. Use const whenever
possible as it protects you from inadvertently modifying the parameter and protects you against many
programming errors.
A const function parameter can receive both const and non-const argument. On the other hand,
a non-const function reference/pointer parameter can only receive non-const argument. For example,

38
Telegram - https://t.me/campusdrive
LaunchPad

/* Test Function const and non-const parameter (FuncationConstParameter.cpp) */


#include <iostream>
using namespace std;

int squareConst(const int);


int squareNonConst(int);
int squareConstRef(const int &);
int squareNonConstRef(int &);

int main() {
int number = 8;
const int constNumber = 9;
cout << squareConst(number) << endl;
cout << squareConst(constNumber) << endl;
cout << squareNonConst(number) << endl;
cout << squareNonConst(constNumber) << endl;

cout << squareConstRef(number) << endl;


cout << squareConstRef(constNumber) << endl;
cout << squareNonConstRef(number) << endl;
// cout << squareNonConstRef(constNumber) << endl;
// error: invalid initialization of reference of
// type ‘int&’ from expression of type ‘const int’
}
int squareConst(const int number) {
//number *= number;// error: assignment of read-only parameter
return number * number;
}

int squareNonConst(int number) { // non-const parameter


number *= number;
return number;
}

int squareConstRef(const int & number) { // const reference


return number * number;
}
int squareNonConstRef(int & number) { // non-const reference
return number * number;
}

39
Telegram - https://t.me/campusdrive
LaunchPad

Passing the Function’s Return Value :


Passing the Return-value as Reference
You can also pass the return-value as reference or pointer. For example,
/* Passing back return value using reference (TestPassByReferenceReturn.cpp) */
#include <iostream>
using namespace std;

int & squareRef(int &);


int * squarePtr(int *);

int main( ) {
int number1 = 8
cout << “In main ( ) & number1: “ <<&number1 << endl; // 0x22ff14
int & result = squareRef (number1) ;
cout << “In main ( ) &result : “ <<&result << endl; // 0x22ff14
cout << result << endl; // 64
cout << number1<< endl; // 64

int number2 = 9;
cout << “In main ( ) &number2: “ <<&number2 << endl; // 0x22ff10
int * pResult = squarePtr(&number2);
cout << *pResult << endl; // 81
cout << number2 << endl; // 81
}

int & squareRef(int & rNumber) {


cout << “In squareRef( ): “ <<&rNumber << endl; // 0x22ff14
cout << “In squareRef( ) : “ <<&rNumber << endl; // 0x22ff14
return rNumber;
}

int * squarePtr(int * pNumber) {


cout << “In squarePtr( ) : “ << pNumber << endl ; // 0x22ff10
*pNumber *= *pNumber ;
return pNumber;

40
Telegram - https://t.me/campusdrive
LaunchPad

You should not pass Function’s local variable as return value by reference
/* Test passing the result (TestPassResultLocal.cpp) */
#include <iostream>
using namespace std;

int * squarePtr(int) ;
int & squareRef(int) ;

int main( ) {
int number = 8;
cout << number << endl; //8
cout << *squarePtr(number) << endl; // ??
cout << squareRef(number) << endl; // ??
}

int * squaePtr(int number) {


int localResult = number * number;
return &localResult ;
// warning: address of local variable ‘localResult’ returned
}

int & squareRef(int number) {


int localResult = number * number;
return localResult;
// warning: reference of local variable ‘localResult’ returned
}

This program has a serious logical error, as local variable of function is passed back as return value by
reference. Local variable has local scope within the function, and its value is destroyed after the
function exits. The GCC compiler is kind enough to issue a warning (but not error).
It is safe to return a reference that is passed into the function as an argument. See earlier examples.

41
Telegram - https://t.me/campusdrive
LaunchPad

Passing Dynamically Allocated Memory as Return Value by Reference


Instead, you need to dynamically allocate a variable for the return value, and return it by reference.
/* Test passing the result (TestPassResultNew.cpp) */
#include <iostream>
using namespace std;

int * squarePtr(int);
int & squareRef(int);
int main( ) {
int number = 8;
cout << number << endl; // 8
cout << *squarePtr(number) << endl; // 64
cout << squareRef(number) << endl; // 64
}

int * squarePtr(int number) {


int * dynamicAllocatedResult = new int(number * number);
return dynamicAllocatedResult;
}

int & squareRef(int number) {


int * dynamicAllocatedResult = new int(number * number);
return *dynamicAllocatedResult;
}

Summary :
Pointers and references are highly complex and difficult to master. But they can greatly improve the
efficiency of the programs.
For novices, avoid using pointers in your program. Improper usage can lead to serious logical bugs.
However, you need to understand the syntaxes of pass-by-reference with pointers and references,
because they are used in many library functions.
q In pass-by-value, a clone is made and passed into the function. The caller’s copy cannot be modified.
q In pass-by-reference, a pointer is passed into the function. The caller’s copy could be modified
inside the function.
q In pass-by-reference with reference arguments, you use the variable name as the argument.
q In pass-by-reference with pointer arguments, you need to use &varName (an address) as the
argument.

42
Telegram - https://t.me/campusdrive
LaunchPad

7 Dynamic Memory
Allocation
New and Delete Operators :
Instead of define an int variable (int number), and assign the address of the variable to the int pointer
(int *pNumber = &number), the storage can be dynamically allocated at runtime, via a new operator.
In C++, whenever you allocate a piece of memory dynamically via new, you need to use delete to
remove the storage (i.e., to return the storage to the heap).
The new operation returns a pointer to the memory allocated. The delete operator takes a pointer
(pointing to the memory allocated via new) as its sole argument.
For example,
// Static allocation
int number = 88;
int * p1 = &number; // Assign a “valid” address into pointer

// Dynamic Allocation
int * p2; // Not initialize, points to somewhere which is invalid
cout << p2 << endl; // Print address before allocation
p2 = new int;
// Dynamically allocate an int and assign its address to pointer
// The pointer gets a valid address with memory allocated
*p2 = 99;
// The pointer gets a valid address with memory allocated
*p2 = 99;
cout << p2 << endl; // Print address after allocation
cout << *p2 << endl; // Print value point-to
delete p2; // Remove the dynamically allocated storage

Observe that new and delete operators work on pointer.

43
Telegram - https://t.me/campusdrive
LaunchPad

To initialize the allocated memory, you can use an initializer for fundamental types, or invoke a
constructor for an object. For example,
// use an initializer to initialize a fundamental type (such as int, double)
int * p1 = new int(88);
double * p2 = new double(1.23);

// c++11 brace initialization syntax


int * p1 = new int {88};
double * p2 = new double {1.23};

// invoke a constructor to initialize an object (such as Date, Time)


Date * date1 = new Date(1999, 1, 1);
Time * time1 = new Time(12, 34, 56);

You can dynamically allocate storage for global pointers inside a function. Dynamically allocated
storage inside the function remains even after the function exits. For example,
// Dynamically allocate global pointers (TestDynamicAllocation.cpp)
#include <iostream>

int * p1, * p2; // Global int pointers

// This function allocates storage for the int*


// which is available outside the function
void allocate( ) {
p1 = new int; // Allocate memory, initially content unknown
*p1 = 88; // Assign value into location pointed to by pointer
p2 = new int(99); // Allocate and initialize
}

int main( ) {
allocate( );
cout << *p1 << endl; // 88
cout << *p2 << endl; // 99
delete p1; // Deallocate
delete p2;
return 0;
}

44
Telegram - https://t.me/campusdrive
LaunchPad

The main differences between static allocation and dynamic allocations are:
1. In static allocation, the compiler allocates and deallocates the storage automatically, and handle
memory management. Whereas in dynamic allocation, you, as the programmer, handle the memory
allocation and deallocation yourself (via new and delete operators). You have full control on the
pointer addresses and their contents, as well as memory management.
2. Static allocated entities are manipulated through named variables. Dynamic allocated entities are
handled through pointers.

new[] and delete[] Operators :


Dynamic array is allocated at runtime rather than compile-time, via the new[] operator. To remove
the storage, you need to use the delete[] operator (instead of simply delete). For example,
/* Test dynamic allocation of array (TestDynamicArray.cpp) */
#include <iostream>
#include <cstdlib>
using namespace std;
int main( ) {
const int SIZE = 5;
int * pArray;
pArray = new int[SIZE]; // Allocate array via new[ ] operator
// Assign random numbers between 0 and 99
for (int i = 0; i < SIZE; ++i) {
*(pArray + 1) = rand( ) % 100;
}
// Print array
for (int i = 0; i < SIZE; ++i) {
cout << *(pArray + 1) << “ “;
}
cout << endl;
delete[ ] pArray; // Deallocate array via delete[ ] operator
return 0;
}

C++03 does not allow your to initialize the dynamically-allocated array. C++11 does with the brace
initialization, as follows:
// c++11
int * p = new int[5] {1, 2, 3, 4, 5};

45
Telegram - https://t.me/campusdrive
LaunchPad

8 Time & Space


Complexiety
In the world of computing, we want our programs to be fast and efficient. But fast and efficient are
loose terms.
A software might be fast on a supercomputer but its insanely slow on a personal computer. So to
define this fastness and efficientness, we take the help of time and space complexity analysis.
Complexity analysis is a way to quantify or measure time and space required by an algorithm with
respect to the input fetched to it.
In time complexity analysis, we are concerned with this growth which remains same on all machines.
This analysis doesn’t require algorithms to be run. It can be found just by doing little maths.
int print(int n){
for(int i = 0; i < 10; ++i){
cout << “Coding Blocks”;
}
}

Irrespective of n, print function runs exactly 10 times. So, we ll say, time complexity is T(N) = O(1).
This is Big-O notation
T(N) here denotes the time complexity when represented in terms of N, the input size.
int print(int n){
for(int i = 0; i < n; ++i){
cout << “Coding Blocks”;
}
}

As N increases linearly, printing also happens with the same order. So time complexity is proportional
to N.
T(N) = O(N)
int print(int n){
for(int i = 0; i < n; ++i){
for(int j = i; j < n; ++j)
cout << “Coding Blocks”;
}
}

46
Telegram - https://t.me/campusdrive
LaunchPad

Lets us assume n = 4

When i is 0 1 2 3

Then inner loop runs 4 3 2 1

Total printing done = Total time inner loop runs = Sum of first 4 natural numbers
T(N) = Sum of first N natural Numbers = N ( N + 1) / 2 = N 2 / 2 + N / 2
To get the Big-O notation, just keep the term with the highest power of N, N2/2 in our case
Remove all constants associated with this term, in our case 1 / 2 will be removed
Remaining term is the proportionality factor. Hence, we will say time complexity is Big-O of N square.
T(N) = O(N2)

Constant Complexity: O(1)


A constant task’s run time won’t change no matter what the input value is. Consider a function that
prints a value in an array.
void print_element(int arr[], int idx){
cout << “arr[“ << idx << “] “ << idx << “ “ << arr[i];
}

No matter which element’s value you’re asking the function to print, only one step is required. So we
can say the function runs in O(1) time; its run-time does not increase. Its order of magnitude is
always 1.

Linear Complexity: O(n)


A linear task’s run time will vary depending on it’s input value. If you ask a function to print all the
items in a 10-element array, it will require less steps to complete than it would a 10,000 element array.
This is said to run at O(n); it’s run time increases at an order of magnitude proportional to n.
void print_array(int arr[]){
int i = 0;
while(arr[i]){
cout << arr[i] << “ “;
++i;
}
}

47
Telegram - https://t.me/campusdrive
LaunchPad

Quadratic Complexity: O(N²)


A quadratic task requires a number of steps equal to the square of it’s input value. Lets look at a
function that takes an array and N as it’s input values where N is the number of values in the array.
If I use a nested loop both of which use N as it’s limit condition, and I ask the function to print the
array’s contents, the function will perform N rounds, each round printing N lines for a total of N² print
steps.

Let’s look at that practically. Assume the index length N of an array is 10. If the function prints the
contents of it’s array in a nested-loop, it will perform 10 rounds, each round printing 10 lines for a total
of 100 print steps. This is said to run in O(N²) time; it’s total run time increases at an order of
magnitude proportional to N².
void print_array(int arr[], int n){
int x = 1;
for(int i = 0; i < size; ++i){
for(int j = 0; j < size; ++j){
cout << x << “ “ << arr[j] << “ “;
}
++x;
cout << endl;
}
}

Exponential: O(2N)
O(2N) is just one example of exponential growth (among O(3 N), O(4 N), etc.). Time complexity at an
exponential rate means that with each step the function performs, it’s subsequent step will take
longer by an order of magnitude equivalent to a factor of N. For instance, with a function whose
step-time doubles with each subsequent step, it is said to have a complexity of O(2 N). A function
whose step-time triples with each iteration is said to have a complexity of O(3N) and so on.

Logarithmic Complexity: O(log n)


This is the type of algorithm that makes computation blazingly fast. Instead of increasing the time it
takes to perform each subsequent step, the time is decreased at magnitude inversely proportional
to N.

Let’s say we want to search a database for a particular number. In the data set below, we want to
search 20 numbers for the number 100. In this example, searching through 20 numbers is a non-issue.
But imagine we’re dealing with data sets that store millions of users’ profile information. Searching
through each index value from beginning to end would be ridiculously inefficient. Especially if it
had to be done multiple times.

48
Telegram - https://t.me/campusdrive
LaunchPad

A logarithmic algorithm that performs a binary search looks through only half of an increasingly smaller
data set per step.
Assume we have an ascending ordered set of numbers. The algorithim starts by searching half of the
entire data set. If it doesn’t find the number, it discards the set just checked and then searches half of
the remaining set of numbers.

Index 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Value 8 10 14 20 27 32 33 34 37 44 51 52 55 56 86 87 94 95 97 98
9

Index 0 1 2 3 4 5 6 7 8 9
Value 8 10 14 20 27 32 33 34 37 44
5

Index 0 1 2 3 4
Value 8 10 14 20 27
3

Index 0 1
Value 8 10
1

Index 0
Value 8
1
As illustrated above, each round of searching consists of a smaller data set than the previous,
decreasing the time each subsequent round is performed. This makes log n algorithms very scalable.
void binary_search(int arr[], int size, int key){
int startIndex = 0;
int endIndex = size - 1;
while(startIndex <= endIndex){
int mid = (startIndex + endIndex) / 2;
if (arr[mid] == key) {
return key;
} else if(arr[mid] > key){
// search in the left
endIndex = mid - 1;
}
else {
// search in the right

49
Telegram - https://t.me/campusdrive
LaunchPad

startIndex = mid + 1;
}
}
return -1;
}

EXPONENTIAL TIME – O(2n)


Fibonacci Series

CUBIC TIME – O(n3)


Multiplication of Matrices

EFFICIENCY DECREASES
SQUARED TIME – O(n2)
TIME TO SOLVE THE PROBLEM

Bubble, Selection, Insertion Sort

O(nlog(n))
INCREASES

Merge Sort

LINEAR TIME – O(N)


Linear search

LOG TIME – O(log(n))


Binary Search

CONSTANT TIME – O(1)


Printing, Algebraic operations

Space Complexity :
Total amount of computer memory required for the execution of an algorithm is when expressed in
terms of input size is called space complexity. Auxiliary space is the extra space or temporary space
used by an algorithm. Space complexity only includes auxiliary space and NOT the space required by
the input.

50
Telegram - https://t.me/campusdrive
LaunchPad

9 Object Oriented
Programming
Programming languages help a programmer to translate his ideas into a form that the machine
can understand. The method of designing and implementing the programs using the features of a
language is a programming paradigm. One such method is the Procedural programming which focuses
more on procedure than on data. It separates the functions and the data that uses those functions
which is not a very useful thing as it makes our code less maintainable. This leads us to another
technique called the object oriented programming.

What is OOPs ?
Object means any real world entity like pen, car, chair, etc. Object oriented programming is the
programming model that focuses more on the objects than on the procedure for doing it. Thus we can
say that object oriented programing focuses more on data than on logic or action. In this method the
program is made in a way as real world works. In short it is a method to design a program using objects
and classes.

Advantages of OOPs :
1. It is more appropriate for real world problems.
2. OOP provides better modularity
Data hiding
Abstraction
3. OOPs provides better reusability
Inheritance
4. OOP makes our code easy to maintain and modify.
Polymorphism

Concepts in OOPs : One of the major shortcomings of the procedural method is that the data and
the functions that use those data are separated. This is not preferred as if a data member I changed
then all the functions relating to it have to be changed again and again. This leads to mistakes and
maintaining the code becomes tough.

How to solve?????
The best way to solve this problem would be to wrap the data and all the functions related to them in
one unit. Each of this unit is an object.

51
Telegram - https://t.me/campusdrive
LaunchPad

Objects are the basic units of Object-oriented programming. Objects are the real world entities
about which we code. Objects have properties and behavior. For instance take an example of a car
which has properties like its model and has four tyres and behavior of changing speed and applying
brakes.

State of the objects is represented by data members and their behavior is represented by methods.

Now many objects share common properties and behavior. So a group of such properties and behavior
is made that can generate many instances of itself that have similar characteristics. This group is
called a class.

Classes are the blueprints from which objects are created. Like there are many cars which share
common properties and behavior. The class provides these properties and behavior to its objects
which are the instances of that class.
For example a vehicle class might look like this:
class Vehicle {
double price;
int numWheels;
int yearofManufacture;
public:
char brand[100];
char model[100];
double getPrice(){
return price;
}
void printDescription(){
cout << brand << “ “ << model << “ “ << price << “ “ << “ “ <<
numWheels << endl;
}
};

Now each vehicle will be a specific copy of this template.


A class in C++ contains:
Ø Data members Ø Methods Ø Constructor
Now let us talk about each of these in detail:
Data Members : Data members are the properties that are present in a class. The type of these
properties can be modified by the use of special keywords called modifiers. Let us build our own
student class and learn about them.

52
Telegram - https://t.me/campusdrive
LaunchPad

Static and Non Static Properties : Static properties are those that are common to all objects and
belong to the class rather each specific object. So each object that we create doesn’t have their copy.
They are shared by all the objects of the class. We need to write the static keyword before it in order
to make it static.
For e.g.: static int numStudents;
Here the number of students in a batch is a property that isn’t specific to each student and hence is
static.
But the properties like name, Roll Number etc can have different values for each student and are
object specific and thus are non static.
An important point to note is that whenever we create a new object only the non static data member
copies are created and the static properties are stored within the class only! This could be considered
a very memory efficient practice as static members of a class are made only once.
A general student class might look like this:
class Student{
static int numStudents;
char name[10];
int rollNo;
};

Access Modifiers :
Private: If we make any data member as private it is visible only within the class i.e. it can be accessed
by and through the methods of the same class. So we can provide setters and getters function through
which they can be accessed outside the class.
For instance in our student class we would like to keep the roll numbers for each student as private as
we may not want any other person to modify those roll numbers by making them public. So we will
make these private and provide the getter method to access the roll numbers of the students outside
the student class.
class Student {
private: int rollNo;
public: int qetRollNo( ) {
return this.rollNo;
}
}

Public: It is accessible everywhere.

53
Telegram - https://t.me/campusdrive
LaunchPad

An important point to note here is that it is better to make a variable private and then provide getters
and setters in case we wish allow others to view and change it than making the variable public.
Because by providing setter we can actually add constraints to the function and update value only if
they are satisfied (say if we make the marks of a student public someone can even set them to
incorrect values like negative numbers. So it would be wise to provide a setter method for marks of
the student so that these conditions are checked and correct marks are updated).

Methods : After having discussed about the data members of a class let us move onto the methods
contained in the class relating to the data members of the class. We made many members of the
class as private or protected. Now to set, modify or get the values of those data members,
public getter and setter methods can be made and called on the objects of that class. The methods are
called on the object name by using the dot operator.

Now let us look at various modifiers that can be used to modify the types of methods and the
differences between them.

Static v/s Non Static Methods : Like data members, methods of a class can also be static which means
those methods belong to the class rather than the objects for the class. These methods are directly
called by the class name.
As the static methods belong to a class we don’t need any instance of a class to access them.
An important implication of this point is that the non static properties thus can’t be accessed by the
static methods as there is no specific instance of the class associated with them (the non static
properties are specific to each object). So, non static members and the ‘this’ keyword can’t be used
with the static functions. Thus these methods are generally used for the static properties of the class
only!
The non static methods on the other hand are called on an instance of a class or an object and can thus
access both static and non static properties present in the object.

The access modifiers work the same with the methods as they do with the data members. The public
methods can be accessed anywhere whereas the private methods are available only within the same
class. Thus private methods can be used to work with the data members that we don’t wish to expose
to the clients.
Now let us add some methods to our student class.
class Student{
static int numStudents;
char name[10];
int rollNo;
public:
char * getName(){
return name;

54
Telegram - https://t.me/campusdrive
LaunchPad

}
int getRollNo(){
return this.rollNo;
}
int getNumStudents(){
return numStudents;
}
}

Constructor : As we have our student class ready, we can now create its objects. Each student will be
a specific copy of this template. The syntax to create an object is:
int main(){
Student s; //s is created in the memory

The method called constructor, is a special method used to initialize a new object and its name is
same as that of the class name.

Even though in our student class we haven’t created an explicit constructor there is a default
constructor implicitly there. Every class has a constructor. If we do not explicitly write a constructor for
a class, the C++ compiler builds a default constructor for that class.

We can also create our own constructors. One important point to note here is that as soon as we
create our constructor the default constructor goes off. We can also make multiple constructors each
varying in the number of arguments being passed (i.e. constructor overloading). The constructor that
will be called will be decided on runtime depending on the type and number of arguments
specified while creating the object.
Below is our own custom constructor for our student class.
class Student{
char name[10];
int rollNo;

Student(char n[]){
strcpy(name, n);
}
Student(char name[], int rollNo){
strcpy(this->name, n);
this->rollNo = r;
}
}

55
Telegram - https://t.me/campusdrive
LaunchPad

The ‘this’ keyword : Here this is a keyword that refers to current object. So, this.name refers to the
data member (i.e. name) of this object and not the argument variable name.
In general, there can be many uses of the ‘this’ keyword.
1. The ‘this’ keyword can be used to refer current class instance variable.
2. The this() can be used to invoke current class constructor.
3. The ‘this’ keyword can be used to invoke current class method (implicitly)
4. The ‘this’ can be passed as an argument in the method call.
5. The ‘this’ can be passed as argument in the constructor call.
6. The ‘this’ keyword can also be used to return the current class instance.
There is also a special type of constructor called the Copy Constructor. C++ doesn’t have a default copy
constructor but we can create one of our own. Given below is an example of a copy constructor.
class Student{
char name[20];
int rollNo;

Student(char n[], int r){


strcpy(name, n);
rollNo = r;
}
Student(Student& s){
rollNo = s.rollNo;
strcpy(name, s.name);
//private members of object s are accessed by class Student
}
};

After leaning so much about the objects and constructors let us summarize the initialization of an
object in simple steps.
As any new object is created, following steps take place:
1. Memory is allocated on heap and its reference is stored in stack.
2. The data members are initialized to their default values. (Only the non static properties are
copied!)
3. The ‘this’ keyword is set to the current object.
4. The instance initialize is run and the fields are initialized to their respected values.
The constructor code is executed.

56
Telegram - https://t.me/campusdrive
LaunchPad

The final memory map of the instance of our student class is given below:

Encapsulation-The First Pillar of OOPs : We learnt that one of the major disadvantages of the procedural
paradigm was that the functions and the data were separated which made the maintainability of the code
poor! To overcome this we made classes which contained both the data members and their methods.
This wrapping up of data and its functions into a single unit (called class) is known as Encapsulation.
A class classifies its members into three types: private, protected and public. Thus Data Hiding is implemented
through private and protected members. These private and protected members can be accessed or set
using public functions.
Also, the outside world needs to know only the essential details through public members. The rest of the
implementation details remain hidden from the outside world which is nothing but Abstraction. Like for
instance if we wish to know the aggregate marks of a student, we need not know how the aggregate is
calculated. We are just interested in the marks and for that we only need to know that we need to call a
public function called ‘totalMarks’ on any student object.
We have an additional benefit that comes bounded with encapsulation which is Modularity!
Modularity is nothing but partitioning our code into small individual components called modules. It makes
our code less complex and easy to understand. Like for example, our code represents a school. A school has
many individual components like students, teachers, other helping staff, etc. now each of them are complete
units in themselves yet they are part of the school. This is called modularity.
After having the overview of benefits of encapsulation let us dive into the details of data hiding and
abstraction.
Let us suppose that we make all the data members of our student class as public. Now accidently if any of
our client changes the roll numbers of the student objects, all are data would be ruined. Thus public data
is not safe. So, to make or data safe, we need to hide our data by making it either private or protected! This
technique is called information hiding. The private data can be accessed only via a proper channel that is
through their access methods which are made public.
To use a class, we only need to know the public API of the class. We only need to know the signature of the
public methods that is their input and output forms to access a class. Thus, only the essential details are
sown to the end user and all the complex implementation details are kept hidden. This is Abstraction.
After all sometimes ignorance is bliss!

57
Telegram - https://t.me/campusdrive
LaunchPad

10 Linked Lists
We have used arrays to store multiple values under a single name. But using arrays has its disadvantages
that :
Ø They are static and hence their size cannot be changed
Ø It needs contiguous memory to store its values
Ø It is difficult(costly) to insert and delete elements from an array
To overcome these problems, we use another data structure called linked lists.
Linked list is a linear data structure that contains sequence of elements such that each element has a
reference to its next element in the sequence. Each element in a linked list is called “Node”.
Each Node contains a data element and a Node next which points to the next node in the linked list.
In a linked list we can create as many nodes as we want according to our requirement.
This can even be done at the runtime. The memory allocation is done at the run time and is known as
DYNAMIC MEMORY ALLOCATION. Thus, linked list uses dynamic memory allocation to allocate
memory at the run time.

General Implementation : The linked list class contains a structure Node and a Node head.
The Node head points to the starting of the linked list. Every Node contains a data element and a Node
next which points to the next node in the linked list. The last node points to null.
struct Node{
int data; //Data of the Node
Node* next; // pointer to t
}

58
Telegram - https://t.me/campusdrive
LaunchPad

Different Types Of Linked Lists :


Singly Linked List : These are normal linked lists in which every node points to the next node and
head node points to the starting node.

Doubly Linked List : These are linked lists in which every node points to the next node as well as the
previous node.

Circular Linked List : This is a type of singly linked list in which the last node points to the starting
node.

Advantages of Linked Lists :


They are dynamic in nature as their size is not fixed and can be changed during the runtime.
Insertion and deletion operations can be easily implemented at any point in the linked list.

Disadvantages of Linked Lists :


Some amount of memory is wasted in storing the references for the next nodes.
Unlike array, no element can be accessed randomly in a linked list.

Applications :
1. Process Queue’s in operating system are actually doubly linked list of processes in ready state
where the process at the front of linked list denotes the one to be operated next.
2. Any situation where insertion and deletion operations are more as compared to retrievals.

59
Telegram - https://t.me/campusdrive
LaunchPad

Big O efficiency :
Insertion O(n)
Deletion O(n)
Searching O(n)

3. Reverse a linked list


class Node{
public:
int data;
Node* next;
}

Node* rev(Node* root){


if(root is NULL || root is last node of linked list)
return root;
Node* temp=rev(root->next);
root->next->next=root;
root->next=NULL;
return temp;
}

60
Telegram - https://t.me/campusdrive
LaunchPad

11 Stack & Queues


Stacks & Queues :
Your friend sent you 100 messages. Which message you intent to read first. If the answer is the 100th
message, then you are probably using the stack without knowing it.
Our daily life make use of plenty of such examples where the last item in the series is accessed first.
Other example include, pile of cash notes and plates arranges in a party.

Stack :
This is LIFO (Last In First Out), as the last element that is added, is the first to be removed off.
Removal from in between is not allowed, i.e. Peaking is not allowed in ideal stack.
Stack operations:
q Push : Addition of an element to stack.
q Pop : Removal of element from stack.

In addition to this there is a top() function in the


STL stack, that is used to get the topmost element
in the stack.

Three ways to implement stack


1. Use Arrays 2. Use Linked list

The structure of a class Stack


class Stack {
private:
int array[20];
int top;
public:
Stack(); //constructor
int size();
void pop();
void push(int x);
bool empty();
};

61
Telegram - https://t.me/campusdrive
LaunchPad

Queue :
Suppose your friend just sent you 100 messages. If you wish to read the 1st message first, then the 2nd
message and so on, then you are basically reading it in a queue fashion.

This follow FIFO(First In First Out) method. Another example of queue is getting a token on a railway
platform. The person who came first will receive the token first. And not to foreget the queue outside
ATMs after demonetization.

Functions on queue :
q Enqueue/push : enter an element to the queue.
q Dequeue/pop : remove an element from the queue.
q IsFull : check whether the queue is full or not.
q IsEmpty : check whether the queue is empty or not.
q Size : return the size of the array.

Queue like stack can be implemented using Linked List , arrays, or vectors. etc

62
Telegram - https://t.me/campusdrive
LaunchPad

Generic Programming in C++ :


12 A Conceptual Overview
A computer deals with 3 things:
(a) Data (b) Algorithm (c) Container
Let us consider a simple function to search an element in an array.

Observation :
The search function above works only for an integer array. However the functionality search, is logically
separate from array and applicable to all data types, i.e. searching a char in a char array or searching is
applicable in searching in a linked list.
Hence, data, algorithms and containers are logically separate but very strongly connected to each
other.
Generic Programming enables programmer to achieve this logical separation by writing general
algorithms that work on all containers with all data types.

Separating Data
A function can be made general to all data types with the help of templates.
Templates are a blueprint based on which compiler generates a function as per the
requirements.
To create a template of search function, we replace int with a type T and tells compiler that T is
a type using the statement template <class T>
template <class T>
int search(T arr[], int n, T elementToSearch) {
for (int pos = 0; pos < n; ++pos) {
if (arr[pos] == elementToSearch) {
return pos;
}
}
return -1;
}

Now the search function runs for all types of arrays for which statement 4 is defined.
Now the search function runs for all types of arrays for which statement 4 is defined.

63
Telegram - https://t.me/campusdrive
LaunchPad

int arrInt[100];
char arrChar[100];
float arrFloat[100];
Book arrBook[100], X; //X is a book
search(arrInt, 100, 5); //T is replaced by int
search(arrChar, 100, ‘A’); //T is replaced by char
search(arrFloat, 100, 1.24); //T is replaced by float
search(arrBook, 100, X); //T is replaced by Book.

So, one function can be run on different data types. This makes our function general for all types of
data.

Note
In the actual code that is produced after compilation, 4 different functions will be produced based on
the template with T replaced accordingly.
You could use <typename T> or <class T> in the template statement 1. The keywords typename and
class serve the same purpose.

Separating Algorithm
The search function will not work for Book objects if computer doesn’t know how to compare
2 books. So our function is limited in some sense. To work it for all data types, we use a concept
of comparator (also see predicate).
Let’s rewrite the templated search function again
template <class T, class Compare>
int search(T arr[], int n, T elementToSearch, Compare obj) {
//compare is a class that has () operator overloaded
for (int pos = 0; pos < n; ++pos) {
if (obj(arr[pos], elementToSearch) == true) {
//obj compares elements of type T
return pos;
}
}
return -1;
}

64
Telegram - https://t.me/campusdrive
LaunchPad

To use the search function for integers, you shall now write:
//defining a class compare
class compareInt {
public:
bool operator()(int a, int b) {
return a == b ? true : false;
}
};
//calling search templated function
compareInt obj;
//obj is the object of class compareInt
search(arrInt, 100, 20, obj); //T replaced with int
//Compare replaced with compareInt

Line 4 works since obj(xInt, yInt) is defined by the class compareInt.


To use search function for a book class, we should write a compareBook class.
class compareBook{
public:
bool operator()(const Book& B1, const Book& B2){
return B1.getIsbn () == B2.getIsbn();
}

search(arrBook, 100, X, compareBook);


//calling search function

The same search function now operators for Books just by writing a small compare class.
However, search function still works only for arrays. However the functionality of searching extends
to list equally. To make it general for all containers(here array) we introduce a concept of iterators
in our discussion.

Separating Containers
Iterators
Visualise iterators as an entity using which you can access the data within a container with
certain restrictions.

65
Telegram - https://t.me/campusdrive
LaunchPad

These are classified into 5 categories


Input Iterator : An entity through which you can read from container and move ahead.
What sort of container will posses an input iterator???
A keyboard.

Output Iterator An entity through which you can write into the container and move ahead.
Container like printer or monitor will have such an iterator.

Forward Iterator Iterator with functionalities of both Input and Output iterator in single direction.
Singly linked list will posses a forward entity since we can read/write only in the forward direction.

Bidirectional Iterator Forward iterator that can move in both the directions.
Doubly linked list will posses a bidirectional iterator.

Random Access Iterator Iterator that can read/write in both directions plus can also take jumps.
An array will have random access iterator. Since, you can jump by writing arr[5], which means jump to
the 5th element.
Entity that does this, behaves like a pointer in some sense.
To write search function that is truly independent of data and the underlying container, we use iterator.
template<class ForwardIterator, class T, class Compare>
ForwardIterator search(ForwardIter beginOfContainer, ForwardIter endOfContainer,
T elementToSearch, Compare Obj) {
while (beginOfContainer != endOfContainer) {
if (obj((*beginOfContainer), elementToSearch) == true) break; //iterators are like
pointers!
++beginOfContainter;
}
return beginOfContainer;
}
Here, beginOfContainer is a ForwardIterator, i.e., beginOfContainer must know how to read/write
and move in forward direction.
So, if a container has at least ForwardIterator, the algorithm works. Hence, it works for list, doubly
linked list and array as well thus achieving generality over container.

66
Telegram - https://t.me/campusdrive
LaunchPad

//search for book in an array


search(arr, arr + n, X, compareBook);
//search for book in a list
list<Book> lb; // see list
search(lb.begin(), lb.end(), X, compareBook);
//begin and end are member function of the class list.

Summary
1. Using templates, we achieve freedom from data types
2. Using comparators, we achieve freedom from operation(s) acting on data
3. Using iterators, we achieve freedom from underlying data structure (container).

67
Telegram - https://t.me/campusdrive

13 Trees
It is a non-linear data structure in which each node may be associated with more than one node.Each
node in the tree points to its children. A tree that is not empty consists of a root node and potentially
many levels of additional nodes that form a hierarchy.
Ø The root of a tree is the node with no parent. There can be only one root node.
Ø A node with no children is known as a leaf.
Ø A node p is ancestor of node q if the node p lies on the path from q to the root of the tree.
[Two type of ancestor drawing]
Ø If each node of a tree has only one child then it is called skew.

The most basic examples of a Tree are :


q file system inside the computer,
q HTML DOM, where there is a hierarchy of HTML tags,
q organisational structure of employees inside an organization.
The Tree data structure looks reverse of the actual physical tree, and has root on top.

Basic Terminology in Trees


q Node : It is the structure that contains the data about each element in the tree. Each element in a
tree constitutes a node.
q Root : The top node in a tree.
q Children : A node directly connected to another node when moving away from the Root.
q Parent : The immediate node to which a node is connected on the path to root.
q Siblings : A group of nodes with same parent.
q Ancestor : A node reachable by repeated proceeding from child to parent.
q Descendant : A node reachable by repeated proceeding from parent to child.

68
Telegram - https://t.me/campusdrive

q Leaves : The nodes that don’t have any children.


q Path : The sequence of nodes along the edges of the tree.
q Height of Tree : The number of edges on the longest downward path from root to a leaf.
q Depth : The number of edges from the node to the root node.
q Degree : The number of edges that are being connected to the node, can be further subdivided
into in-degree and out-degree.

Binary Tree
A tree is called Binary tree if it has each node has two or fewer children.
In C++ each node of a binary tree can be represented as a structure or class.
This structure contains left and right pointers, pointing to the children of the node.

Eg.
struct node {
int data; //Data of the Node
node* left; //Pointing to the left child
node* right; //Pointing to the right child
}

Since input of a tree is not known, we will consider -1 as the NULL node for the tree.
So for input,
1 2 3 -1 -1 -1 4 -1 -1
1→left(2)→right(3)→left(-1)→backtrack→right(-1)→backtrackright of 2(-1)→backtrack→right of
1(4) →left(-1)→backtrack→right(-1)→backtrack
The tree would look like,

2 4

Code for the above tree construction can be written like :


class TreeNode {
public:
int data;
TreeNode* left;
TreeNode* right;
TreeNode(int d) {

69
Telegram - https://t.me/campusdrive

data = d;
next = NULL;
left = NULL;
right = NULL;
}
};

TreeNode* createTree() {
int x;
cin >> x;
if (x == -1) {
return NULL;
}

TreeNode* root = new TreeNode(x);


// cout << “Enter left child of “ << x << “ “;
root->left = createTree();
// cout << “Enter right child of “ << x << “ “;
root->right = createTree();
return root;
}

Applications
Ø File hierarchy structure in the computer is tree based. Assuming folder as a node can have
subdirectories and files as a leaf node
Ø For Android Developers out there, findViewById searches the view using search in tree type
layout structure
Ø For Web Developers out there, Document Object Model is tree type of structure and all traversal
in it are tree search.

Traversals in a Tree

70
Telegram - https://t.me/campusdrive

q Pre-order Traversal : Each node is processed before(pre) any nodes in its subtrees.
root left right
A (B D E) (C F)
void preOrderPrint(TreeNode* root) {
if (root == NULL) {
return;
}

cout << root->data << “ “;


preOrderPrint(root->left);
preOrderPrint(root->right);
}

q Post-order Traversal : Each node is processed after(post) all nodes in its subtrees.
Left right root
(D E B) (F C) A
void postOrderPrint(TreeNode* root) {
if (root == NULL) {
return;
}

preOrderPrint(root->left);
preOrderPrint(root->right);
cout << root->data << “ “;
}

q Inorder Traversal : Each node is processed after all nodes in between its left subtree, and its
right subtree.
Left root right
(D B E) A (C F)
void postOrderPrint(TreeNode* root) {
if (root == NULL) {
return;
}
preOrderPrint(root->left);
cout << root->data << “ “;
preOrderPrint(root->right);
}

71
Telegram - https://t.me/campusdrive

q Level-order Traversal: Each node is processed level by level from left to right before any nodes in
their subtrees.
A (B C) (D E F)
void printLevelOrder2(TreeNode* root) {
queue<TreeNode*> q;
q.push(root);

while (q.empty() == false) {


TreeNode* cur = q.front();
q.pop();
cout << cur->data << “ “;
if (cur->left) {
q.push(cur->left);
}

if (cur->right) {
q.push(cur->right);
}
}
}

72
Telegram - https://t.me/campusdrive

14 Binary Search Trees


It’s a binary tree with special property that is satisfied is by each node, which is:
q Each node in the left subtree is less than the value of the root node,
q Each node in the right subtree is greater than the root node’s value.

3 10

1 6 14

4 7 13

Inorder traversal (left root right) of above tree : 1 , 3 , 4 , 6 , 7 , 8 , 10 , 13 , 14


The inorder traversal of any Binary Search Tree will give elements in sorted order.

Creating a BST
class Node {
public:
int data;
Node * left;
Node * right;
Node(int d) {
data = d;
left = NULL;
right = NULL;
}
};
Node * buildBST() {
int x;
Node * root = NULL;
Node * insertBST(Node *, int);
while (true) {
cin >> x;
if (x == -1) break;
root = insertBST(root, x);

73
Telegram - https://t.me/campusdrive

}
return root;
}

Node * insertBST(Node * root, int d) {


if (root == NULL) {
Node * newNode = new Node(d);
return newNode;
}

if (d < root->data) {
root->left = insertBST(root->left, d);
}
else {
root->right = insertBST(root->right, d);
}
return root;
}

Searching in a Binary Search Tree :


The main application of a Binary search tree is efficient searching.
Due to the Binary Search Tree property, one Subtree of the BST can be discarded, as the value to be searched
can be either greater or smaller or equal to the root of the tree, but can’t be multiple of these.

Node* search(Node* root,int val){


if(root is empty)
// Not found
else{
if(current node’s data is same as val)
// return this node
else if(current node’s data is > val)
// return answer found in left subtree
else if(current node’s data is < val)
// return answer found in right subtree
}
}

This type of searching algorithm is similar to that of the binary search, where we reduce the search space and
thereby optimise the search process.

74
Telegram - https://t.me/campusdrive

Time complexity for searching in a Binary Search Tree


Though the search algorithm is similar to that of a binary search, the time complexity is not Log(n) every time.
Instead it depends on the height of the tree.

Consider this binary search tree:

7
16
20
37

38
43

This is an example of Skew tree.

In this case the Time Complexity to search an element would be O(n).

The Time Complexity for searching an element in a Binary Search Tree is O( Height of the BST ).

The Height of the tree is Log(N) only in the case of complete/balanced binary tree, only for these cases the time
complexity would be O(log(n)).

Balanced BST
A height balanced BST is a BST where the absolute difference between heights of left and right subtrees is 1 for
all nodes in the tree.

Deletion in a BST
Given a BST, write an efficient function to delete a given key in it.
To delete a node from BST, there are three possible cases to consider:

Case 1: Deleting a node with no children: simply remove the node from the tree.

15 15

Delete (18)
10 20 10 20
Case 1

8 12 18 25 8 12 25

75
Telegram - https://t.me/campusdrive

Case 2: Deleting a node with two children: call the node to be deleted N . Do not delete N . Instead, choose
either its in-order successor node or its in-order predecessor node R . Swap R with N, then recursively call delete
on until reaching one of the first two cases. If you choose in-order successor of a node, as right sub tree is not
NULL (Our present case is node has 2 children), then its in-order successor is node with least value in its right sub
tree, which will have at a maximum of 1 sub tree, so deleting it would fall in one of the first 2 cases.

15 15

Delete (20)
10 20 10 19
Case 2 & Case 1

8 12 18 25 8 12 18 30

16 19 in-order 16
predecessor
node

Case 3: Deleting a node with one child: remove the node and replace it with its child.

15 15

Delete (25)
10 20 10 20
Case 3

8 12 18 25 8 12 18 30

16 19 30 16 19

TreeNode* successor(TreeNode* root) {


// detaches the inorder successor it is exists
if (root == NULL) return NULL;
TreeNode* parent = root;
TreeNode* cur = root->right;
while (cur && cur->left) {
parent = cur;
cur = cur->left;
}
if (cur) parent->left = cur->right;
return cur;
}
TreeNode* deleteFromBST(TreeNode* root, int x) {

76
Telegram - https://t.me/campusdrive

if (root == NULL) {
return NULL;
}
if (root->data == x) {
// if root is a leaf
if (!root->left && !root->right) {
delete root;
return NULL;
}
// if root has one child
if (!root->left) {
TreeNode* tmp = root->right;
delete root;
return tmp;
}
if (!root->right) {
TreeNode* tmp = root->left;
delete root;
return tmp;
}
// if root has 2 children
TreeNode* inOrderSuccessor = successor(root);
inOrderSuccessor->left = root->left;
inOrderSuccessor->right = root->right;
delete root;
return inOrderSuccessor;
}
if (root->data > x) {
root->left = deleteFromBST(root->left, x);
return root;
} else {
root->right = deleteFromBST(root->right, x);
return root;
}
}

77
Telegram - https://t.me/campusdrive

15 Heaps
Consider an array in which we repeatedly have to find maximum or minimum element.
Naive method would be searching for maximum/minimum in each query, that would take O(qn) time
if q queries were there. Another way could to be to keep a sorted array and give min/max element in
constant time but in that case, insertion and deletion would cost us linear time.
To solve this problem we needed a data structure called heap. In heap, time complexity of operations
are:
(a) Insertion : log(n) (b) Deletion : log(n) (c) Minimum Element : O(1)
Binary Heap is a tree based data structure, each node having at most 2 children. The constraint on a
binary heap are:
Ø the value of the node is greater than both children in case max heap and smaller than both children
in case of min heap.
Ø it is a complete binary tree that is new level will begin only when all the previous levels are
completely full.

Priority queues are implemented using heaps, as the top element of heap is prioritized as per
the value.

What is the use of heap being complete Binary tree?


Being a complete binary tree, heap can be stored in form of an array, making access to each value
easier. Also as we can see in the image below, children of each node are available at location 2i or
2i + 1, so complete can be stored in random access linear DS in effective manner.

78
Telegram - https://t.me/campusdrive

Insertion in a Heap :
For this we will talk about max heap only, min heap would be just opposite. So for insertion we create
a new node and place it at the end. Now this node may or may not be at its correct position.
To take it to correct position we compare this node/value with parent, if parent is smaller then we
swap and we keep repeating it until the parent is greater or it reaches the root.

79
Telegram - https://t.me/campusdrive

//Constructing a maxheap
class Heap {
vector<int> v;
int sze;

int parent(int i) { return i / 2;}


int left(int i) { return 2 * i; }
int right(int i) { return 2 * i + 1; }

void heapify(int i){

// left child comparison


int posOfLargest = i;
if (left(i) <= sze && v[left(i)] > v[i]){
posOfLargest = left(i);
}

if (right(i) <= sze && v[right(i)] > v[posOfLargest]){


posOfLargest = right(i);
}

if (i != posOfLargest){
// I HAVE to swap
swap(v[i], v[posOfLargest]);
heapify(posOfLargest);
}
}

public:
Heap() {
sze = 0;
v.push_back(-1); // empty position
}

int top() {
if (empty()) return -1;
return v[1];

80
Telegram - https://t.me/campusdrive

bool empty() {
return sze == 0;
}

void pop() {
if (empty()) return;

swap(v[1], v[sze]);
v.pop_back();
—sze;

heapify(1);
}

void push(int d) {
// push and compare with the parent
v.push_back(d);
++sze;

int i = v.size() - 1;
while(i > 1 && v[i] > v[parent(i)]){
swap(v[i], v[parent(i)]);
i = parent(i);
}
}
};

81
Telegram - https://t.me/campusdrive

16 Hashing
In Hashing the idea is to use hash function that converts a given key to a smaller number and uses
the small number as index in a table called hash table.
We generate a hash for the input using the hash function, and then store the element using the
generated hash as the key in the hash table.

Hash Table : Hash table is a collection of Key-Value pairs. It Used when the searching, insertion of an
element is required to be fast.

Operations in hash function:


q Insert – T [ h( key ) ] = value ;
Calculate the hash, use it as the key and store the value in hash table.
q Delete – T [ h ( key ) ] = NULL ;
Calculate the hash, reset the value stored in the hash table for that key.
q Search – return T [ h ( key ) ] ;
Calculate the hash, find and return the value stored in the hash table for that key.

Hash Collision: when two or more inputs are mapped to same keys as used in the hash table.
Example : h( “ john “ ) == h( “ joe “ )

Collision can not be completely avoided but can be minimised using “good” hash function and a
bigger table size.
The chances of hash collision are less, if the table size if a prime number.

82
Telegram - https://t.me/campusdrive

Characteristics of a good Hash Function :


Ø Uniform Distribution : For distribution throughout the constructed table.
Ø Fast : The generation of Hash should be very fast, and should not produce any considerable
overhead.

Collision Handling Techniques :


1. Open Hashing (Separate Chaining) : In this using Linked List , new nodes are added to the list, the
key acts as the head pointer, pointing to a number of nodes having different values.

2. Closed Hashing ( Open Addressing ) : In this we find the “next” vacant bucket in Hash Table
and store the value in that bucket.
(a) Linear Probing : We linearly go to every next bucket and see if it is vacant or not.
(b) Quadratic Probing : We go to the 1st, 4th , 9th … bucket and check if they are vacant or not.

3. Double Hashing : Here we subject the generated key from the hash function to a second hash
function.

Load Factor : It is a measure of how full the hash table is allowed to get before its capacity is
increased.
Load factor ë of a hash table T is defined as follows:
N = number of elements in T (“current size”)
M = size of T (“table size”)
ë = N/M (“ load factor”)

Generally if load factor is greater than 0.5, we increase the size of bucket array and rehash all the
key value pairs again.

Rehashing :
Process of increasing the size of the hash table when load factor becomes “too high” (defined by a
threshold value) , anticipating that collisions would become higher now.
q Typically expand the table to twice its size (but still prime).
q Need to reinsert all existing elements into the new hash table.

// Code to construct a hashmap


struct Node {
string key; // object
int val;
Node* next;

83
Telegram - https://t.me/campusdrive

};
class Hashmap {
Node * *table;
int sze;
int capacity;
void insertIntoList(int idx, Node* tmp) {
Node*& head = table[idx]; // reference...
if (head == NULL) {
head = tmp;
}
else {
tmp->next = head;
head = tmp;
}
}
double loadFactor() {
return (double)sze / capacity;
}
int hashFunction(const string& s) {
int hashCode = 0;
int mul = 31; // he told me to take prime...research
const int MOD = capacity;
for (int i = 0; i < s.size(); ++i) {
hashCode = (mul * hashCode) % MOD;
hashCode = (hashCode + s[i] % MOD) % MOD;
mul = (mul * 31) % MOD;
}
return hashCode;
}
void rehash() {
Node** oldTable = table;
int oldCapacity = capacity;
capacity = 2 * oldCapacity;
table = new Node*[capacity](); // WARNING! MUST be initialised
sze = 0; // insert function increases size
for (int i = 0; i < oldCapacity; ++i) {
Node* head = oldTable[i];

84
Telegram - https://t.me/campusdrive

while (head != NULL) {


// insert into newtable
Node* nextItem = head->next;
head->next = NULL;
insert(head);
head = nextItem;
}
}
delete [] oldTable;
}
void insert(Node* tmp) {
int hashCode = hashFunction(tmp->key);
int idx = hashCode % capacity;
insertIntoList(idx, tmp);
++sze;
if (loadFactor() > 0.7) {
rehash();
}
}
public:
Hashmap() {
cout << “Welcome, Creating hashmap...\n”;
capacity = 7;
sze = 0;
table = new Node*[capacity]();
// initialise all ptrs to NULL. WARNING!!!
}
void insert(const string& s, const int& val) {
Node* tmp = new Node();
tmp->key = s;
tmp->val = val;
tmp->next = NULL;
insert(tmp);
}
int at(const string& s) {
// returns the val
int idx = hashFunction(s) % capacity;

85
Telegram - https://t.me/campusdrive

Node* head = table[idx];


// search s in the list
while (head) {
if (head->key == s) {
return head->val;
}
head = head->next;
}
return -1;
}

bool empty() {
return sze == 0;
}
void remove(const string& s) {
// delete from list
int idx = hashFunction(s) % capacity;
Node* head = table[idx];

// delete element with s from the list


Node* cur = head;
Node* pre = NULL;
while (cur) {
if (cur->key == s) {
if (cur == head) {
table[idx] = cur->next;
delete cur;
} else {
pre->next = cur->next;
delete cur;
}
—sze;
}
pre = cur;
cur = cur->next;
}
}

86
Telegram - https://t.me/campusdrive

~Hashmap() {
for (int i = 0; i < capacity; ++i) {
Node* head = table[i];
while (head) {
Node* tmp = head->next;
delete head;
head = tmp;
}
}
delete [] table;
cout << “Bye! Dtor called\n”;
}
void printHashMap(){
for(int i = 0; i < capacity; ++i){
Node* head = table[i];
cout << i << “\t:\t”;
while(head){
cout << head->key << “—>”;
head = head->next;
}
cout << endl;
}
}
};

87
Telegram - https://t.me/campusdrive

11 Graphs
Introduction :
Graphs are mathematical structures that represent pairwise relationships between objects. A graph is
a flow structure that represents the relationship between various objects. It can be visualized by
using the following two basic components :
q Nodes : These are the most important components in any graph. Nodes are entities whose
relationships are expressed using edges. If a graph comprises 2 nodes A and B and an undirected
edge between them, then it expresses a bi-directional relationship between the nodes and edge.
q Edges : Edges are the components that are used to represent the relationships between various
nodes in a graph. An edge between two nodes expresses a one-way or two-way relationship
between the nodes.

Some Real Life Applications:


q Google Maps : To find a route based on shortest route/Time.
q Social Networks : Connecting with friends on social media, where each user is a vertex, and
when users connect they create an edge.
q Web Search : Google, to search for webpages, where pages on the internet are linked to each
other by hyperlinks, each page is a vertex and the link between two pages is an edge.
q Recommendation System : On eCommerce websites relationship graphs are used to show
recommendations.

Types of Graphs :
q Undirected : An undirected graph is a graph in which all the edges are bi-directional i.e. the
edges do not point in any specific direction.

88
Telegram - https://t.me/campusdrive

q Directed : A directed graph is a graph in which all the edges are uni-directional i.e. the edges
point in a single direction.

q Weighted : In a weighted graph, each edge is assigned a weight or cost. Consider a graph of
4 nodes as in the diagram below. As you can see each edge has a weight/cost assigned to it. If you
want to go from vertex 1 to vertex 3, you can take one of the following 3 paths:
Ø 1 -> 2 -> 3 Ø 1 -> 3 Ø 1 -> 4 -> 3
Therefore the total cost of each path will be as follows: The total cost of 1 -> 2 -> 3 will be (1 + 2) i.e.
3 units - The total cost of 1 -> 3 will be 1 unit - The total cost of 1 -> 4 -> 3 will be (3 + 2) i.e. 5 units

q Cyclic: A graph is cyclic if the graph comprises a path that starts from a vertex and ends at the
same vertex. That path is called a cycle. An acyclic graph is a graph that has no cycle.

Cycle presents in the above graph (0->1->2)


A tree is an undirected graph in which any two vertices are connected by only one path. A tree is
an acyclic graph and has N - 1 edges where N is the number of vertices. Each node in a graph
may have one or multiple parent nodes. However, in a tree, each node (except the root node)
comprises exactly one parent node.
Note: A root node has no parent.
A tree cannot contain any cycles or self loops, however, the same does not apply to graphs.

89
Telegram - https://t.me/campusdrive

(Tree : There is no any cycle or self loop in this graph)

Graph Representation
You can represent a graph in many ways. The two most common ways of representing a graph is as
follows:
1. Adjacency Matrix
An adjacency matrix is a V ∗ V binary matrix A. Element A i, j is 1 if there is an edge from vertex i to
vertex j else Ai, j is 0.
Note: A binary matrix is a matrix in which the cells can have only one of two possible values -
either a 0 or 1.
The adjacency matrix can also be modified for the weighted graph in which instead of storing
0 or 1 in Ai, j the weight or cost of the edge will be stored.
In an undirected graph, if Ai, j = 1, then Aj, i = 1.
In a directed graph, if Ai, j = 1, then may or may not be 1.
Adjacency matrix provides constant time access (O(1)) to determine if there is an edge between
two nodes. Space complexity of the adjacency matrix is O(V2).

2. Adjacency List
The other way to represent a graph is by using an adjacency list. An adjacency list is an array A of
separate lists. Each element of the array Ai is a list, which contains all the vertices that are adjacent
to vertex i.
For a weighted graph, the weight or cost of the edge is stored along with the vertex in the list
using pairs. In an undirected graph, if vertex j is in list Ai then vertex i will be in list Aj.
The space complexity of adjacency list is O(V + E)because in an adjacency list information is stored
only for those edges that actually exist in the graph. In a lot of cases, where a matrix is sparse using
an adjacency matrix may not be very useful. This is because using an adjacency matrix will take up
a lot of space where most of the elements will be 0, anyway. In such cases, using an adjacency list
is better.

90
Telegram - https://t.me/campusdrive

Two representations of an undirected graph.


(a) An undirected graph G having five vertices and seven edges
(b) An adjacency-list representation of G
(c) The adjacency-matrix representation of G

Code for Adjacency list representation of a graph :


#include<iostream>
#include<list>
using namespace std;
class Graph{
int V;
list<int> *l;
public:
Graph(int v){
V = v;
//Array of Linked Lists
l = new list<int>[V];
}
void addEdge(int u,int v,bool bidir=true){

91
Telegram - https://t.me/campusdrive

l[u].push_back(v);
if(bidir){
l[v].push_back(u);
}
}
void printAdjList(){
for(int i=0;i<V;i++){
cout<<i<<“->”;
//l[i] is a linked list
for(int vertex: l[i]){
cout<<vertex<<“,”;
}
cout<<endl;
}
}
};
int main(){
// Graph has 5 vertices number from 0 to 4
Graph g(5);
g.addEdge(0,1);
g.addEdge(0,4);
g.addEdge(4,3);
g.addEdge(1,4);
g.addEdge(1,2);
g.addEdge(2,3);
g.addEdge(1,3);
g.printAdjList();
return 0;
}

Graph Traversal :
Graph traversal means visiting every vertex and edge exactly once in a well-defined order. While using
certain graph algorithms, you must ensure that each vertex of the graph is visited exactly once. The order
in which the vertices are visited are important and may depend upon the algorithm or question that
you are solving.
During a traversal, it is important that you track which vertices have been visited. The most common way
of tracking vertices is to mark them.

92
Telegram - https://t.me/campusdrive

Breadth First Search (BFS) :


There are many ways to traverse graphs. BFS is the most commonly used approach.
BFS is a traversing algorithm where you should start traversing from a selected node (source or starting
node) and traverse the graph layerwise thus exploring the neighbour nodes (nodes which are directly
connected to source node). You must then move towards the next-level neighbour nodes.
As the name BFS suggests, you are required to traverse the graph breadthwise as follows:
1. First move horizontally and visit all the nodes of the current layer
2. Move to the next layer
Breadth-first search colors each vertex white, gray, or black. All vertices start out white and may later
become gray and then black. A vertex is discovered the first time it is encountered during the search,
at which time it becomes nonwhite. Gray and black vertices, therefore, have been discovered,
but breadth-first search distinguishes between them to ensure that the search proceeds in a breadth-first
manner. If (u, v) “ E and vertex u is black, then vertex v is either gray or black; that is, all vertices adjacent to
black vertices have been discovered. Gray vertices may have some adjacent white vertices; they represent
the frontier between discovered and undiscovered vertices.
Breadth-first search constructs a breadth-first tree, initially containing only its root, which is the source
vertex s. Whenever a white vertex v is discovered in the course of scanning the adjacency list of an
already discovered vertex u, the vertex v and the edge (u, v) are added to the tree. We say that u is the
predecessor or parent of v in the breadth-first tree.

Algorithm :
BFS(G, s)
for each vertex u ∈ V [G] – {s}
do color[u] ← WHITE
d[u] ← ∞
π[u] ← NIL
color[s] ← GRAY
d[s] ← 0
π[s] ← NIL
Q ← 0
ENQUEUE(Q, s)
while Q ≠ 0
do u ← DEQUEUE(Q)
for each ∈ Adj[u]
do if color[v] = WHITE
then color[v] ← GRAY

93
Telegram - https://t.me/campusdrive

d[v] ← d[u] + 1
π[v] ← u
ENQUEUE(Q, v)

color[u] ← BLACK

The operation of BFS on an undirected graph. Tree edges are shown shaded as they are prduced by BFS. Within each
vertex vertex u is shown d[u]. The queue Q is shown at the beginning of each iteration of the while loop of lines 10-18.
Vertex distances are shown next to vertices in the queue.

94
Telegram - https://t.me/campusdrive

Code :
#include<iostream>
#include<map>
#include<list>
#include<queue>
using namespace std;

template<typename T>
class Graph{

map<T,list<T> > adjList;

public:
Graph(){

}
void addEdge(T u, T v,bool bidir=true){

adjList[u].push_back(v);
if(bidir){
adjList[v].push_back(u);
}
}

void print(){

Time Complexity :
Time complexity and space complexity of above algorithm is O(V + E) and O(V).

Application of BFS
1. Shortest Path and Minimum Spanning Tree for unweighted graph In unweighted graph, the
shortest path is the path with least number of edges. With Breadth First, we always reach a vertex
from given source using minimum number of edges. Also, in case of unweighted graphs, any
spanning tree is Minimum Spanning Tree and we can use either Depth or Breadth first traversal
for finding a spanning tree.
Code for finding shortest path using BFS :
#include<iostream>
#include<map>
#include<list>

95
Telegram - https://t.me/campusdrive

#include<queue>
using namespace std;
template<typename T>
class Graph{
map<T,list<T> > adjList;
public:
Graph(){
}
void addEdge(T u, T v,bool bidir=true){
adjList[u].push_back(v);
if(bidir){
adjList[v].push_back(u);
}
}

void print(){
//Iterate over the map
for(auto i:adjList){
cout<<i.first<<“->”;
//i.second is LL
for(T entry:i.second){
cout<<entry<<“,”;
}
cout<<endl;
}
}
voidbfs(T src){
queue<T> q;
map<T,int>dist;
map<T,T> parent;
for(auto i:adjList){
dist[i.first] = INT_MAX;
}
q.push(src);

96
Telegram - https://t.me/campusdrive

dist[src] = 0;
parent[src] = src;
while(!q.empty()){
T node = q.front();
cout<<node<<“ “;
q.pop();
// For the neigbours of the current node, find out the nodes which
are not visited
for(intneigbour :adjList[node]){
if(dist[neigbour]==INT_MAX){
q.push(neigbour);
dist[neigbour] = dist[node] + 1;
parent[neigbour] = node;
}
}
}
//Print the distance to all the nodes
for(auto i:adjList){
T node = i.first;
cout<<“Dist of “<<node<<“ from “<<src<<“ is “<<dist[node]<<endl;
}
}
};
int main(){
Graph<int> g;
g.addEdge(0,1);
g.addEdge(1,2);
g.addEdge(0,4);
g.addEdge(2,4);
g.addEdge(2,3);
g.addEdge(3,5);
g.addEdge(3,4);
g.bfs(0);
}

97
Telegram - https://t.me/campusdrive

2. Peer to Peer Networks : In Peer to Peer Networks like BitTorrent, Breadth First Search is used to
find all neighbor nodes.
3. Crawlers in Search Engines : Crawlers build index using Breadth First. The idea is to start from
source page and follow all links from source and keep doing same. Depth First Traversal can also
be used for crawlers, but the advantage with Breadth First Traversal is, depth or levels of built tree
can be limited.
4. Social Networking Websites : In social networks, we can find people within a given distance ‘k’
from a person using Breadth First Search till ‘k’ levels.
5. GPS Navigation systems : Breadth First Search is used to find all neighboring locations.
6. Broadcasting in Network : In networks, a broadcasted packet follows Breadth First Search to reach
all nodes.
7. In Garbage Collection : Breadth First Search is used in copying garbage collection using Cheney’s
algorithm. Refer this and for details. Breadth First Search is preferred over Depth First Search
because of better locality of reference:
8. Cycle detection in undirected graph : In undirected graphs, either Breadth First Search or Depth
First Search can be used to detect cycle. In directed graph, only depth first search can be used.
9. Ford–Fulkerson algorithm : In Ford-Fulkerson algorithm, we can either use Breadth First or Depth
First Traversal to find the maximum flow. Breadth First Traversal is preferred as it reduces worst
case time complexity to O(VE2).
10. To test if a graph is Bipartite : We can either use Breadth First or Depth First Traversal.
11. Path Finding : We can either use Breadth First or Depth First Traversal to find if there is a path
between two vertices.
12. Finding all nodes within one connected component : We can either use Breadth First or Depth First
Traversal to find all nodes reachable from a given node.

Depth First Search (DFS)


The DFS algorithm is a recursive algorithm that uses the idea of backtracking. It involves exhaustive
searches of all the nodes by going ahead, if possible, else by backtracking.
Here, the word backtrack means that when you are moving forward and there are no more nodes
along the current path, you move backwards on the same path to find nodes to traverse. All the nodes
will be visited on the current path till all the unvisited nodes have been traversed after which the
next path will be selected.
This recursive nature of DFS can be implemented using stacks. The basic idea is as follows:
q Pick a starting node and push all its adjacent nodes into a stack.
q Pop a node from stack to select the next node to visit and push all its adjacent nodes into a stack.

98
Telegram - https://t.me/campusdrive

q Repeat this process until the stack is empty. However, ensure that the nodes that are visited are
marked. This will prevent you from visiting the same node more than once. If you do not mark
the nodes that are visited and you visit the same node more than once, you may end up in an
infinite loop.
As in breadth-first search, vertices are colored during the search to indicate their state.
Each vertex is initially white, is grayed when it is discovered in the search, and is blackened when
it is finished, that is, when its adjacency list has been examined completely. This technique
guarantees that each vertex ends up in exactly one depth-first tree, so that these trees are disjoint.
Algorithm :
DFS(G)
for each vertex u ∈ V[G]
do color [u] ← WHITE
p[u] ← NIL
time ← 0
for each vertex u ∈ V[G]
do if color[u] = WHITE
then DFS-VISIT(u)
DFS-VISIT(u)
color[u] ← GRAY White vertex u has just been discovered
time ← time + 1
d[u] ← time
for each v ∈ Adj[u] Explore edge (u, v)
do if color[v] = WHITE
then π[v] ← u
DFS-VISIT(v)
color[u] BLACK Blacken u; it is finished
f[u] ← time ← time + 1

99
Telegram - https://t.me/campusdrive

The progress of the depth-first-search algorithm DFS on a directed path. As edges are explored by the
algorithm, they are shown as either shaded (if they are tree edges) or dashed (otherwise). Nontree edges
are labeled B, C or F according to whether they are back, cross, or forward edges. Vertices are timestamped
by discovery time/finishing time.

Code :
#include<iostream>
#include<map>
#include<list>
using namespace std;

template<typename T>
class Graph{

map<T,list<T> > adjList;

public:
Graph(){

}
void addEdge(T u, T v,bool bidir=true){
adjList[u].push_back(v);

100
Telegram - https://t.me/campusdrive

if(bidir){
adjList[v].push_back(u);
}
}

void print(){

//Iterate over the map


for(auto i:adjList){
cout<<i.first<<“->”;

//i.second is LL
for(T entry:i.second){
cout<<entry<<“,”;
}
cout<<endl;
}
}
void dfsHelper(T node,map<T,bool> &visited){
//Whenever to come to a node, mark it visited
visited[node] = true;
cout<<node<<“ “;
//Try to find out a node which is neigbour of current node and not
yet visited
for(T neighbour: adjList[node]){
if(!visited[neighbour]){
dfsHelper(neighbour,visited);
}
}
}
void dfs(T src){
map<T,bool> visited;
dfsHelper(src,visited);
}
};
int main(){

Graph<int> g;
g.addEdge(0,1);
g.addEdge(1,2);
g.addEdge(0,4);

101
Telegram - https://t.me/campusdrive

g.addEdge(2,4);
g.addEdge(2,3);
g.addEdge(3,4);
g.addEdge(3,5);
g.dfs(0);

return 0;
}

Time Complexity :
Time complexity of above algorithm is O(V + E).
Application of DFS :
1. For an unweighted graph, DFS traversal of the graph produces the minimum spanning tree
and all pair shortest path tree.
2. Detecting cycle in a graph : A graph has cycle if and only if we see a back edge during DFS. So we can
run DFS for the graph and check for back edge.

Code for detect a cycle in a graph :


#include<iostream>
#include<map>
#include<list>
using namespace std;
template<typename T>
class Graph{
map<T,list<T> > adjList;
public:
Graph(){
}
void addEdge(T u, T v,bool bidir=true){
adjList[u].push_back(v);
if(bidir){
adjList[v].push_back(u);
}
}
bool isCyclicHelper(T node,map<T,bool> &visited,map<T,bool> &inStack){

102
Telegram - https://t.me/campusdrive

//Processing the current node - Visited, InStack


visited[node] = true;
inStack[node] = true;

3. Path Finding : We can specialize the DFS algorithm to find a path between two given vertices u
and z.
(i) Call DFS(G, u) with u as the start vertex.
(ii) Use a stack S to keep track of the path between the start vertex and the current vertex.
(iii)As soon as destination vertex z is encountered, return the path as the contents of the stack.

4. Topological Sorting : In the field of computer science, a topological sort or topological


ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv
from vertex u to vertex v, u comes before v in the ordering. For instance, the vertices of the graph
may represent tasks to be performed, and the edges may represent constraints that one task must
be performed before another; in this application, a topological ordering is just a valid sequence
for the tasks. A topological ordering is possible if and only if the graph has no directed cycles,
that is, if it is a directed acyclic graph (DAG). Any DAG has at least one topological ordering.

Code for printing Topological sorting of DAG :


#include<iostream>
#include<map>
#include<list>
#include<queue>
using namespace std;
template<typename T>
class Graph{
map<T,list<T> > adjList;
public:
Graph(){
}
void addEdge(T u, T v,bool bidir=true){
adjList[u].push_back(v);
if(bidir){
adjList[v].push_back(u);
}
}
void topologicalSort(){

103
Telegram - https://t.me/campusdrive

queue<T> q;
map<T,bool> visited;
map<T,int> indegree;
for(auto i:adjList){
//i is pair of node and its list
T node = i.first;
visited[node] = false;
indegree[node] = 0;
}
//Init the indegrees of all nodes
for(auto i:adjList){
T u = i.first;
for(T v: adjList[u]){
indegree[v]++;
}
}
//Find out all the nodes with 0 indegree
for(auto i:adjList){
T node = i.first;
if(indegree[node]==0){
q.push(node);
}
}
//Start with algorithm
while(!q.empty()){
T node = q.front();
q.pop();
cout<<node<<“—>”;
for(T neighbour:adjList[node]){
indegree[neighbour]—;
if(indegree[neighbour]==0){
q.push(neighbour);
}
}

104
Telegram - https://t.me/campusdrive

}
}
};

int main(){
Graph<string> g;
g.addEdge(“English”,”ProgrammingLogic”,false);
g.addEdge(“Maths”,”Programming Logic”,false);
g.addEdge(“Programming Logic”,”HTML”,false);
g.addEdge(“Programming Logic”,”Python”,false);
g.addEdge(“Programming Logic”,”Java”,false);
g.addEdge(“Programming Logic”,”JS”,false);
g.addEdge(“Python”,”WebDev”,false);
g.addEdge(“HTML”,”CSS”,false);
g.addEdge(“CSS”,”JS”,false);
g.addEdge(“JS”,”WebDev”,false);
g.addEdge(“Java”,”WebDev”,false);
g.addEdge(“Python”,”WebDev”,false);
g.topologicalSort();

return0;
}

5. To test if a graph is bipartite : We can augment either BFS or DFS when we first discover a new
vertex, color it opposited its parents, and for each other edge, check it doesn’t link two vertices of
the same color. The first vertex in any connected component can be red or black.

6. Finding Strongly Connected Components of a graph : A directed graph is called strongly connected
if there is a path from each vertex in the graph to every other vertex.
7. Solving puzzles with only one solution, such as mazes. (DFS can be adapted to find all solutions to
a maze by only including nodes on the current path in the visited set.)
8. For finding Connected Components : In graph theory, a connected component (or just component)
of an undirected graph is a subgraph in which any two vertices are connected to each other by
paths, and which is connected to no additional vertices in the supergraph.

105
Telegram - https://t.me/campusdrive

Example :

In the above picture, there are three connected components.


Code for finding connected components :
#include<iostream>
#include<map>
#include<list>
using namespace std;

template<typename T>
class Graph{
map<T,list<T> > adjList;
public:
Graph(){
}
void addEdge(T u, T v,bool bidir=true){
adjList[u].push_back(v);
if(bidir){
adjList[v].push_back(u);
}
}
void print(){
//Iterate over the map
for(auto i:adjList){
cout<<i.first<<“->”;
//i.second is LL

106
Telegram - https://t.me/campusdrive

for(T entry:i.second){
cout<<entry<<“,”;
}
cout<<endl;
}
}
void dfsHelper(T node,map<T,bool> &visited){
//Whenever to come to a node, mark it visited
visited[node] = true;
cout<<node<<“ “;
//Try to find out a node which is neigbour of current node and not yet visited
for(T neighbour: adjList[node]){
if(!visited[neighbour]){
dfsHelper(neighbour,visited);
}
}
}
void dfs(T src){
map<T,bool> visited;
int component = 1;
dfsHelper(src,visited);
cout<<endl;
for(auto i:adjList){
T city = i.first;
if(!visited[city]){
dfsHelper(city,visited);
component++;
}
}
cout<<endl;
cout<<“The current graph had “<<component<<“ components”;
}
};
int main(){
Graph<string> g;

107
Telegram - https://t.me/campusdrive

g.addEdge(“Amritsar”,”Jaipur”);
g.addEdge(“Amritsar”,”Delhi”);
g.addEdge(“Delhi”,”Jaipur”);
g.addEdge(“Mumbai”,”Jaipur”);
g.addEdge(“Mumbai”,”Bhopal”);
g.addEdge(“Delhi”,”Bhopal”);
g.addEdge(“Mumbai”,”Bangalore”);
g.addEdge(“Agra”,”Delhi”);
g.addEdge(“Andaman”,”Nicobar”);
g.dfs(“Amritsar”);
return 0;
}

Minimum Spanning Tree


What is a Spanning Tree?
Given an undirected and connected graph G=(V,E) , a spanning tree of the graph G is a tree that
spans G (that is, it includes every vertex of G) and is a subgraph of G (every edge in the tree
belongs to G)

What is a Minimum Spanning Tree?


The cost of the spanning tree is the sum of the weights of all the edges in the tree. There can be
many spanning trees. Minimum spanning tree is the spanning tree where the cost is minimum
among all the spanning trees. There also can be many minimum spanning trees.
Minimum spanning tree has direct application in the design of networks. It is used in algorithms
approximating the travelling salesman problem, multi-terminal minimum cut problem and minimum-
cost weighted perfect matching. Other practical applications are:
1. Cluster Analysis 2. Handwriting recognition 3. Image segmentation

There are two famous algorithms for finding the Minimum Spanning Tree:

108
Telegram - https://t.me/campusdrive

Kruskal’s Algorithm
Kruskal’s Algorithm builds the spanning tree by adding edges one by one into a growing spanning
tree. Kruskal’s algorithm follows greedy approach as in each iteration it finds an edge which has least
weight and add it to the growing spanning tree.

Algorithm Steps
q Sort the graph edges with respect to their weights.
q Start adding edges to the MST from the edge with the smallest weight until the edge of the largest
weight.
q Only add edges which doesn’t form a cycle , edges which connect only disconnected components.
So now the question is how to check if 2 vertices are connected or not ?
This could be done using DFS which starts from the first vertex, then check if the second vertex is
visited or not. But DFS will make time complexity large as it has an order of O(V+E) where V is the
number of vertices, E is the number of edges. So the best solution is “Disjoint Sets”:
Disjoint sets are sets whose intersection is the empty set so it means that they don’t have any element
in common.
In Kruskal’s algorithm, at each iteration we will select the edge with the lowest weight. So, we will
start with the lowest weighted edge first.
Note : Kruskal’s algorithm is a greedy algorithm, because at each step it adds to the forest an edge of
least possible weight.

Algorithm :
MST-KRUSKAL(G, w)
A ← 0
For each vertex v ∈ V[G]
Do MAKE-SET(v)
sort the edges of E into nondecreasing order by weight w
for each edge (u, v) ∈ E, taken in nondecreasing order by weight
do if FIND-SET(u)≠ FIND-SET(v)
then A ← A ∪ {(u, v)}
UNION(u, v)
return A

Here A is the set which contains all the edges of minimum spanning tree.

109
Telegram - https://t.me/campusdrive

The execution of Kruskal’s algorithm on the graph from figure. Shaded edges belong to the forest A being
grown. The edges are considered by the algorithm in sorted order by weight. An arrow points to the edge
consideration at each step of the algorithm. If the edge joins two distinct trees in the forest, it is added to
the forest, thereby merging the two trees.

Code :
#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>

using namespace std;


const int MAX = 1e4 + 5;
int id[MAX], nodes, edges;
pair <long long, pair<int, int> > p[MAX];

void initialize()
{
for(int i = 0;i < MAX;++i)

110
Telegram - https://t.me/campusdrive

id[i] = i;
}

int root(int x)
{
while(id[x] != x)
{
id[x] = id[id[x]];
x = id[x];
}
return x;
}

void union1(int x, int y)


{
int p = root(x);
int q = root(y);
id[p] = id[q];
}

long long kruskal(pair<long long, pair<int, int> > p[])


{
int x, y;
long long cost, minimumCost = 0;
for(int i = 0;i < edges;++i)
{
// Selecting edges one by one in increasing order from the beginning
x = p[i].second.first;
y = p[i].second.second;
cost = p[i].first;
// Check if the selected edge is creating a cycle or not
if(root(x) != root(y))
{
minimumCost += cost;
union1(x, y);
}

111
Telegram - https://t.me/campusdrive

}
return minimumCost;
}

int main()
{
int x, y;
long long weight, cost, minimumCost;
initialize();
cin >> nodes >> edges;
for(int i = 0;i < edges;++i)
{
cin >> x >> y >> weight;
p[i] = make_pair(weight, make_pair(x, y));
}
// Sort the edges in the ascending order
sort(p, p + edges);
minimumCost = kruskal(p);
cout << minimumCost << endl;
return 0;
}

Time Complexity :
The total running time complexity of kruskal algorithm is O(V log E).

Prim’s Algorithm :
Prim’s Algorithm also use Greedy approach to find the minimum spanning tree. In Prim’s Algorithm we
grow the spanning tree from a starting position. Unlike an edge in Kruskal’s, we add vertex to the
growing spanning tree in Prim’s.

Algorithm Steps :
q Maintain two disjoint sets of vertices. One containing vertices that are in the growing spanning
tree and other that are not in the growing spanning tree.
q Select the cheapest vertex that is connected to the growing spanning tree and is not in the growing
spanning tree and add it into the growing spanning tree. This can be done using Priority Queues.
Insert the vertices, that are connected to growing spanning tree, into the Priority Queue.

112
Telegram - https://t.me/campusdrive

q Check for cycles. To do that, mark the nodes which have been already selected and insert only
those nodes in the Priority Queue that are not marked.
In Prim’s Algorithm, we will start with an arbitrary node (it doesn’t matter which one) and mark it.
In each iteration we will mark a new vertex that is adjacent to the one that we have already
marked. As a greedy algorithm, Prim’s algorithm will select the cheapest edge and mark the vertex.
MST-PRISM(G, w, r)
for each u ∈ V[G]
do key[u] ← ∞
p[u] ← NIL
key[r] ← 0
Q ← V[G]
while Q ≠ 0
do u ← EXTRACT-MIN(Q)
for each v ∈ Adj[u]
do if v ∈ Q and w(u, v) < key[v]
then π[v] ← u
key[v] ← w(u, v)

The prims algorithm works as shown in below graph.

113
Telegram - https://t.me/campusdrive

Code :
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include <utility>
using namespace std;
const int MAX = 1e4 + 5;
typedef pair<long long, int> PII;
bool marked[MAX];
vector <PII> adj[MAX];

long long prim(int x)


{
priority_queue<PII, vector<PII>, greater<PII> > Q;
int y;
long long minimumCost = 0;
PII p;
Q.push(make_pair(0, x));
while(!Q.empty())
{
// Select the edge with minimum weight
p = Q.top();
Q.pop();
x = p.second;
// Checking for cycle

114
Telegram - https://t.me/campusdrive

if(marked[x] == true)
continue;
minimumCost += p.first;
marked[x] = true;
for(int i = 0;i < adj[x].size();++i)
{
y = adj[x][i].second;
if(marked[y] == false)
Q.push(adj[x][i]);
}
}
return minimumCost;
}
int main()
{
int nodes, edges, x, y;
long long weight, minimumCost;
cin >> nodes >> edges;
for(int i = 0;i < edges;++i)
{
cin >> x >> y >> weight;
adj[x].push_back(make_pair(weight, y));
adj[y].push_back(make_pair(weight, x));
}
// Selecting 1 as the starting node
minimumCost = prim(1);
cout << minimumCost << endl;
return 0;
}

Shortest Path Algorithms


The shortest path problem is about finding a path between 2 vertices in a graph such that the total
sum of the edges weights is minimum.
This problem could be solved easily using (BFS) if all edge weights were (1), but here weights can take
any value. There are some algorithm discussed below which work to find Shortest path between two
vertices.

115
Telegram - https://t.me/campusdrive

1. Single Source Shortest Path Algorithm :


In this kind of problem, we need to find the shortest path of single vertices to all other vertices.
Example :

There are two algorithm works to find Single source shortest path from a given graph.

A. Dijkstra’s Algorithm
Dijkstra’s algorithm solves the single-source shortest-paths problem on a weighted, directed graph
G = (V, E) for the case in which all edge weights are nonnegative.
Algorithm Steps:
q Set all vertices distances = infinity except for the source vertex, set the source distance = 0.
q Push the source vertex in a min-priority queue in the form (distance , vertex), as the comparison
in the min-priority queue will be according to vertices distances.
q Pop the vertex with the minimum distance from the priority queue (at first the popped
vertex = source).
q Update the distances of the connected vertices to the popped vertex in case of “current vertex
distance + edge weight < next vertex distance”, then push the vertex with the new distance to
the priority queue.
q If the popped vertex is visited before, just continue without using it.
q Apply the same algorithm again until the priority queue is empty.

Pseudo Code :
DIJKSTRA(G, w, s)
INITIALIZE-SINGLE-SOURCE(G, s)
S ← 0
Q ← V[G]
while Q ≠ 0

116
Telegram - https://t.me/campusdrive

do u ← EXTRACT-MIN(Q)
S ← S ∪ {u}
for each vertex v ∈ Adj[u]
do RELAX(u, v, w)
For relaxing an edge of given graph, Algorithm works likes this :
RELAX(u, v, w)
if d[v] > d[u] + w(u, v)
then d[v] ← d[u] + w(u, v)
π[v] ← u

Example :

The execution of Dijkstra’s algorithm. The source s is the leftmost vertex. The shortest-path estimates are
shown within the vertices, and shaded edges indicate predecessor values. Black vertices are in the set S,
and white vertices are in the min-priority queue Q = V – S. (a) The situation just before the first iteration of
the while loop. The shaded vertex has the minimum d value and is chosen as vertex u . (b)–(f) The situation
after each successive iteration of the while loop. The shaded vertex in each part is chosen as vertex u in
line 5 of the next iteration. The d and À values shown in part (f) are the final values.
Code :
#include<bits/stdc++.h>
using namespace std;
template<typename T>
class Graph{
unordered_map<T, list<pair<T,int> > > m;
public:

117
Telegram - https://t.me/campusdrive

void addEdge(T u,T v,int dist,bool bidir=true){


m[u].push_back(make_pair(v,dist));
if(bidir){
m[v].push_back(make_pair(u,dist));
}
}
void printAdj(){
//Let try to print the adj list
//Iterate over all the key value pairs in the map
for(auto j:m){
cout<<j.first<<“->”;
//Iterater over the list of cities
for(auto l: j.second){
cout<<“(“<<l.first<<“,”<<l.second<<“)”;
}
cout<<endl;
}
}
void dijsktraSSSP(T src){
unordered_map<T,int> dist;
//Set all distance to infinity
for(auto j:m){
dist[j.first] = INT_MAX;
}
//Make a set to find a out node with the minimum distance
set<pair<int, T> > s;
dist[src] = 0;
s.insert(make_pair(0,src));
while(!s.empty()){
//Find the pair at the front.
auto p = *(s.begin());
T node = p.second;
int nodeDist = p.first;
s.erase(s.begin());
//Iterate over neighbours/children of the current node
for(auto childPair: m[node]){

118
Telegram - https://t.me/campusdrive

if(nodeDist + childPair.second < dist[childPair.first]){


//In the set updation of a particular is not possible
// we have to remove the old pair, and insert the new pair
to simulation updation
T dest = childPair.first;
auto f = s.find( make_pair(dist[dest],dest));
if(f!=s.end()){
s.erase(f);
}
//Insert the new pair
dist[dest] = nodeDist + childPair.second;
s.insert(make_pair(dist[dest],dest));
}
}
}
//Lets print distance to all other node from src
for(auto d:dist){
cout<<d.first<<“ is located at distance of “<<d.second<<endl;
}
}
};
int main(){
Graph<int> g;
g.addEdge(1,2,1);
g.addEdge(1,3,4);
g.addEdge(2,3,1);
g.addEdge(3,4,2);
g.addEdge(1,4,7);
//g.printAdj();
// g.dijsktraSSSP(1);
Graph<string> india;
india.addEdge(“Amritsar”,”Delhi”,1);
india.addEdge(“Amritsar”,”Jaipur”,4);
india.addEdge(“Jaipur”,”Delhi”,2);
india.addEdge(“Jaipur”,”Mumbai”,8);
india.addEdge(“Bhopal”,”Agra”,2);

119
Telegram - https://t.me/campusdrive

india.addEdge(“Mumbai”,”Bhopal”,3);
india.addEdge(“Agra”,”Delhi”,1);
//india.printAdj();
india.dijsktraSSSP(“Amritsar”);

return 0;
}

Time Complexity : Time Complexity of Dijkstra’s Algorithm is O(V2) but with min-priority queue it
drops down to O(V + E logV).
B. Bellman Ford’s Algorithm
Bellman Ford’s algorithm is used to find the shortest paths from the source vertex to all other
vertices in a weighted graph. It depends on the following concept: Shortest path contains at most
n – 1 edges, because the shortest path couldn’t have a cycle.
So why shortest path shouldn’t have a cycle ?
There is no need to pass a vertex again, because the shortest path to all other vertices could be
found without the need for a second visit for any vertices.
Algorithm Steps:
q The outer loop traverses from 0 to n – 1.
q Loop over all edges, check if the next node distance > current node distance + edge weight, in
this case update the next node distance to “current node distance + edge weight”.
This algorithm depends on the relaxation principle where the shortest distance for all vertices is
gradually replaced by more accurate values until eventually reaching the optimum solution. In the
beginning all vertices have a distance of “Infinity”, but only the distance of the source vertex = 0,
then update all the connected vertices with the new distances (source vertex distance + edge
weights), then apply the same concept for the new vertices with new distances and so on.
Pseudo Code :
BELLMAN-FORD(G, w, s)
INITIALIZE-SINGLE-SOURCE(G, s)
for i ← 1 to |V[G]| – 1
do for each edge (u, v) ∈ E[G]
do RELAX(u, v, w)
for each edge (u, v) ∈ E[G]
do if d[v] > d[u] + w(u, v)
then return FALSE
return TRUE

120
Telegram - https://t.me/campusdrive

Example :

All-Pairs Shortest Paths :


The all-pairs shortest path problem is the determination of the shortest graph distances between every
pair of vertices in a given graph. The problem can be solved using n applications of Dijkstra’s algorithm at
each vertex if graph doesn’t contains negative weight. We use two algorithms for finding All-pair shortest
path of a graph.
1. Floyd-Warshall’s Algorithm 2. Johnson’s Algorithm

Floyd-Warshall’s Algorithm :
Here we use a different dynamic-programming formulation to solve the all-pairs shortest-paths problem
on a directed graph G = (V, E). The resulting algorithm, known as the Floyd-Warshall algorithm.
Floyd–Warshall’s Algorithm is used to find the shortest paths between between all pairs of vertices in a
graph, where each edge in the graph has a weight which is positive or negative. The biggest advantage of
using this algorithm is that all the shortest distances between any 2 vertices could be calculated in O(V3),
where V is the number of vertices in a graph.
The Floyd-Warshall algorithm is based on the following observation. Under our assumption that the vertices
of G are V = {1, 2, . . . , n}, let us consider a subset {1,2,...,k} of vertices for some k. For any pair of vertices i,
j – V, consider all paths from i to j whose intermediate vertices are all drawn from {1, 2, . . . , k}, and let p be
a minimum-weight path from among them. The Floyd- Warshall algorithm exploits a relationship between
path p and shortest paths from i to j with all intermediate vertices in the set {1, 2, . . . , k – 1}. The relationship
depends on whether or not k is an intermediate vertex of path p. There would two possibilities :
1. If k is not an intermediate vertex of path p, then all intermediate vertices of path p are in the
set {1,2,...,k – 1}. Thus, a shortest path from vertex i to vertex j with all intermediate vertices in the set
{1,2,...,k –1} is also a shortest path from i to j with all intermediate vertices in the set {1, 2, . . . , k}.

121
Telegram - https://t.me/campusdrive

2. If k is an intermediate vertex of path p, then we break p down into i -> k -> j as p1 is a shortest path from
i to k and p2 is a shortest path from k to j. Since p1 is a shortest path from i to k with all intermediate
vertices in the set {1, 2, . . . , k}. Because vertex k is not an intermediate vertex of path p1, we see that
p1 is a shortest path from i to k with all intermediate vertices in the set {1, 2, . . . , k – 1}. Similarly, p2 is
a shortest path from vertex k to vertex j with all intermediate vertices in the set {1,2,...,k – 1}.

Path P is a shortest path from vertices i to vertex j, and k is the highest=numbered intermediate vertex
of p. Path p1, the portion of path p from vertex i to vertex k, has all intermediate vertices in the set
{1, 2, . . . . . , k – 1}. The same holds for path p2 from vertex k to vertex j.

Let dij(k) be the weight of shortest path from vertex i to vertex j for which all intermediate vertices are
in the set {1, 2, . . . , k}. A recursive definition following the above discussion is given by

 wij if k=0
(k ) 
dij =
(
( k − 1 ) ( k −1 )
min dij , dik + dkj
( k −1 )
) if k ≥1

The Algorithm Steps:


For a graph with V vertices:
q Initialize the shortest paths between any 2 vertices with Infinity.
q Find all pair shortest paths that use 0 intermediate vertices, then find the shortest paths that use
1 intermediate vertex and so on.. until using all V vertices as intermediate nodes.
q Minimize the shortest paths between any 2 pairs in the previous operation.
q For any 2 vertices (i , j) , one should actually minimize the distances between this pair using the first K
nodes, so the shortest path will be:
Min (dist[i][k] + dist[k][j] , dist[i][j] )
dist[i][k] represents the shortest path that only uses the first K vertices, dist[k][j] represents the
shortest path between the pair k , j. As the shortest path will be a concatenation of the shortest path
from i to k, then from k to j.

122
Telegram - https://t.me/campusdrive

Constructing a shortest Path


For construction of the shortest path, we can use the concept of predecessor matrix π to construct the path.
We can compute the predecessor matrix π “on-line” just as the Floyd-Warshall algorithm computes the
matrices D(k). Specifically, we compute a sequence of matrices π(k) where π(k) is defined to be the
predecessor of vertex j on a shortest path from vertex i with all intermediate vertices in the set {1,2,...,k}.
We can give a recursive formulation of π(k) as
For k = 0 :
NILL if i = j or wij = ∞
πij( ) = 
0

 i if i ≠ j and w ij < ∞
And For 1<=k<=V
−1) −1) −1)
(k )
π(k
ij if d(k
ij ≤ d(k
ik + dkj(k −1)
πij = −1) (k −1)
 i if d(k ij > dik + dkj(k −1)

Consider the below graph and find distance (D) and parents (π) matrix for this graph

Computed distance (D) and parents (π) matrix for the above graph :

0 3 8 ∞ −4   NIL 1 1 NIL 1 
∞ 0 ∞ 1 7   NIL NIL NIL 2 2 
 
D(0) =  ∞ 4 0 ∞ ∞ Π(3) =  NIL 3 NIL NIL NIL 
   
 2 ∞ −5 0 ∞  4 NIL 4 NIL NIL 
∞ ∞ ∞ 6 0   NIL NIL NIL 5 NIL 
  

0 3 8 ∞ −4   NIL 1 1 NIL 1 
∞ 0 ∞ 1 7   NIL NIL NIL 2 2 
 
D(1) = ∞ 4 0 ∞ ∞ Π(1) =  NIL 3 NIL NIL NIL 
   
 2 5 −5 0 −2   4 1 4 NIL 1 
∞ ∞ ∞ 6 0   NIL NIL NIL 5 NIL 
  

0 3 8 4 −4   NIL 1 1 2 1 
∞ 0 ∞ 1 7   NIL NIL NIL 2 2 
 
D(2) = ∞ 4 0 5 11  Π(2) =  NIL 3 NIL 2 2 
   
 2 5 −5 0 −2   4 1 4 NIL 1 
∞ ∞ ∞ 6 0   
  NIL NIL NIL 5 NIL 

123
Telegram - https://t.me/campusdrive

0 3 8 4 −4   NIL 1 1 2 1 
∞ 0 ∞ 1 7   NIL NIL NIL 2 2 
 
D(3) = ∞ 4 0 5 11  Π(3) =  NIL 3 NIL 2 2 
   
 2 −1 −5 0 −2   4 3 4 NIL 1 
∞ ∞ ∞ 6 0   NIL NIL NIL 5 NIL 
  

 0 3 −1 4 −4   NIL 1 4 2 1 
 3 0 −4 1 −1   4 NIL 4 2 1 
 
D(4) = 7 4 0 5 3 Π(4) = 4 3 NIL 2 1 
   
 2 −1 −5 0 −2   4 3 4 NIL 1 
8 5 1 6 0   5 NIL 
  4 3 4

 0 1 −3 2 −4   NIL 3 4 5 1 
 3 0 −4 1 −1   4 NIL 4 2 1 
 
D(5) = 7 4 0 5 3 Π(5) = 4 3 NIL 2 1 
   
 2 −1 −5 0 −2   4 3 4 NIL 1 
8 5 1 6 0   5 NIL 
  4 3 4

Implemented Code :
#include<bits/stdc++.h>
#include<iostream>
#include<unordered_map>
#define INF 99999
#define V 5
using namespace std;
template<typename T>
class Graph
{
unordered_map<T, list<pair<T, int> > > m;
int graph[V][V];
int parent[V][V];

public:
//Adjacency list representation of the graph
void addEdge(T u, T v, int dist,bool bidir = false)
{
m[u].push_back(make_pair(v,dist));
/*if(bidir){

124
Telegram - https://t.me/campusdrive

m[v].push_back(make_pair(u,dist));
}*/

}
//Print Adjacency list
void printAdj()
{
for(auto j:m)
{
cout<<j.first<<“->”;
for(auto l: j.second)
{
cout<<“(“<<l.first<<“,”<<l.second<<“)”;
}
cout<<endl;
}

}
void matrix_form(int u, int v , int w)
{
graph[u-1][v-1] = w;
parent[u-1][v-1] = u;
return;
}

//Adjacency matrix representation of the graph


void matrix_form2()
{
for(int i=0;i<V;i++)
{
for(int j=0;j<V;j++)
{
if(i==j)
{
graph[i][j]=0;
parent[i][j]=0;
}

125
Telegram - https://t.me/campusdrive

else
{
graph[i][j]=INF;
parent[i][j]=0;
}
}
}
return;
}
//Print Adjacency matrix
void print_matrix()
{
for(int i=0;i<V;i++)
{
for(int j=0;j<V;j++)
{
if(graph[i][j]==INF)
cout<<“INF”<<“ “;
else
cout<<graph[i][j]<<“ “;
}
cout<<endl;
}
}

//Print predecessor matrix


void printParents(int p[][V])
{

cout<<“Parents Matrix”<<“\n”;
for (int i = 0; i < V; i++)
{
for (int j = 0; j < V; j++)
{
if(p[i][j]!=0)
cout<<p[i][j]<<“ “;
else

126
Telegram - https://t.me/campusdrive

{
cout<<“NIL”<<“ “;
//cout<<p[i][j]<<“ “;
}
}
cout<<endl;
}
}
//All pair shortest path matrix i.e D
void printSolution(int dist[][V])
{

cout<<“Following matrix shows the shortest distances”


“ between every pair of vertices”<<“\n”;
for (int i = 0; i < V; i++)
{
for (int j = 0; j < V; j++)
{
if (dist[i][j] == INF)
cout<<“INF “;
else
cout<<dist[i][j]<<“ “;
}
cout<<endl;
}
}
//Print the shortest path , distance and all intermediate vertex
void print_path(int p[][V], int d[][V])
{
// cout<<“Hello”<<“\n”;
for(int i=0;i<V;i++)
{
for(int j=0;j<V;j++)
{
// cout<<“Hello1”<<“\n”;
if(i!=j)

127
Telegram - https://t.me/campusdrive

{
cout<<“Shortest path from “<<i+1<<“ to “<< j+1<<“ => “;
cout<<“[Total Distance : “<<d[i][j]<<“ ( Path : “;
//cout<<“Hello1”<<“\n”;
int k=j;
int l=0;
int a[V];
a[l++] = j+1;
while(p[i][k]!=i+1)
{
//cout<<“Hello1”<<“\n”;
a[l++]=p[i][k];
k=p[i][k]-1;
}
a[l]=i+1;
//cout<<“Hello1”<<“\n”;
for(int r =l;r>0;r—)
{
//cout<<“Hello1”<<“\n”;
cout<<a[r]<<“ ——> “;
}
cout<<a[0]<<“ )”;
cout<<endl;

}
}
}
}
//Floyd Warshall Algorithm
void floydWarshall ()
{
int dist[V][V], i, j, k;
int parent2[V][V];
for (i = 0; i < V; i++)
for (j = 0; j < V; j++)
dist[i][j] = graph[i][j];

128
Telegram - https://t.me/campusdrive

for(int i=0;i<V;i++)
{
for(int j=0;j<V;j++)
{
parent2[i][j] = parent[i][j];
}
}

for (k = 0; k < V; k++)


{
for (i = 0; i < V; i++)
{
for (j = 0; j < V; j++)
{
if (dist[i][k] + dist[k][j] < dist[i][j])
{
dist[i][j] = dist[i][k] + dist[k][j];
parent2[i][j] = parent2[k][j];
}
}
}
}

printSolution(dist);
cout<<“\n\n”;
printParents(parent2);
cout<<“\n\n”;
cout<<“All pair shortest path is given below :”<<“\n”;
print_path(parent2, dist);
}
};
//Main Function
int main()
{
Graph<int> g;
g.matrix_form2();

129
Telegram - https://t.me/campusdrive

// g.make_parent();
//Intializing the graph given in above picture
g.addEdge(1,2,3);
g.matrix_form(1,2,3);
g.addEdge(1,3,8);
g.matrix_form(1,3,8);
g.addEdge(1,5,-4);
g.matrix_form(1,5,-4);
g.addEdge(2,4,1);
g.matrix_form(2,4,1);
g.addEdge(2,5,7);
g.matrix_form(2,5,7);
g.addEdge(3,2,4);
g.matrix_form(3,2,4);
g.addEdge(4,3,-5);
g.matrix_form(4,3,-5);
g.addEdge(4,1,2);
g.matrix_form(4,1,2);
g.addEdge(5,4,6);
g.matrix_form(5,4,6);
cout<<“Graph in the form of adjecency list representation : “<<“\n”;
g.printAdj();
cout<<“Graph in the form of matrix representation : “<<“\n”;
g.print_matrix();
cout<<“\n\n”;
g.floydWarshall();

130
Telegram - https://t.me/campusdrive

18 Dynamic Programming
Dynamic Programming :
It is a technique of solving a complex problem by breaking it down into a collection of simpler
subproblems, solving each of those sub-problems just once, and storing their solutions.
Dynamic Programming avoids repeated computations and thus is an optimization over standard
solutions.
Dynamic Programming can be applied to any problem if that satisfies these two criteria.
1. Overlapping Subproblem :
One or more task is computed multiple times
Consider recursion tree of calls for fibonnaci(6)

f(4) → 2 times
f(3) → 1 time independently, and 2times inside f(4)

2. Optimal Substructure :
The optimal solution of the smaller problem helps building the optimal solution to the larger
problem.
That means, when building your solution for a problem of size n, you split the problem to smaller
problems, one of them of size n’. Now, you need only to consider the optimal solution to n’,
and not all possible solutions to it, based on the optimal substructure property.

131
Telegram - https://t.me/campusdrive

There exists multiple paths from one city to another. But to choose the optimal (smallest) route
from Delhi to Brasilia, you consider only the optimal (smallest) paths from Delhi to intermediate
cities and from intermediate cities to Brasilia.
Presence of alternate routes is not taken into consideration to find the optimal path.
To avoid recomputation of overlapping subproblems, either of two approachs can be used.

1. Memoization (Top-Down) : Calculate the solution for a problem and all its subproblem as and
when it is required. The next time they are required just use the value. Here all the subproblems
are not computed, only those that are required are computed.
This is the top-down approach which is used in recursive problems.
2. Tabulation (Bottom-up): The solution of all smaller sub-problems is computed before hand and
then the solution of actual problem is solved.
The intuition behind dynamic programming is that we trade space for time, i.e. to say that instead of
calculating all the states taking a lot of time but no space, we take up space to store the results of all
the sub-problems to save time later.
Let’s try to understand this by taking an example of Fibonacci numbers.
Fibonacci (n) = 1; if n = 0
Fibonacci (n) = 1; if n = 1
Fibonacci (n) = Fibonacci(n-1) + Fibonacci(n-2)
So, the first few numbers in this series will be: 1, 1, 2, 3, 5, 8, 13, 21, 35...
A code for it using pure recursion:
int fib (int n) {
if (n < 2)
return 1;
return fib(n-1) + fib(n-2);
}

Using Dynamic Programming approach with memoization:

132
Telegram - https://t.me/campusdrive

void fib () {
fib[0] = 1;
fib[1] = 1;
for (int i = 2; i<n; i++)
fib[i] = fib[i-1] + fib[i-2];
}

Q. Are we using a different recurrence relation in the two codes? No


Q. Are we doing anything different in the two codes? Yes

Let’s Visualize :

Here we are running fibonacci function multiple times for the same value of n, which can be prevented
using memoization.
Optimization Problems :
Dynamic Programming is typically applied to optimization problems. In such problems there can be
any possible solutions. Each solution has a value, and we wish to find a solution with the optimal
(minimum or maximum) value. We call such a solution an optimal solution to the problem.

q MINIMUM COIN CHANGE


Given a value N, if we want to make change for N cents, and we have infinite supply of each of
C = { C1, C2, ... , CM} valued coins, what is the minimum number of coins to make the change?

Example :
Input: coins[] = {25, 10, 5}, N = 30
Output: Minimum 2 coins required
We can use one coin of 25 cents and one of 5 cents
Input: coins[] = {9, 6, 5, 1}, N = 13
Output: Minimum 3 coins required
We can use one coin of 6 + 6 + 1 cents coins.

133
Telegram - https://t.me/campusdrive

Recursive Solution :
Start the solution with initially sum = N cents and at each iteration find the minimum coins required
by dividing the problem in subproblems where we take {C1, C2, ..., CM} coin and decrease the sum N
by C[i] (depending on the coin we took). Whenever N becomes 0, this means we have a possible
solution. To find the optimal answer, we return the minimum of all answer for which N became 0.
If N == 0, then 0 coins required.
If N > 0
minCoins(N , coins[0..m-1]) = min {1 + minCoins(N-coin[i] ,coins[0....m-1])}
where i varies from 0 to m-1 and coin[i] <= N

Code :
int minCoins(int coins[], int m, int N)
{
// base case
if (N == 0)
return 0;
// Initialize result
int res = INT_MAX;
// Try every coin that has smaller value than V
for (int i=0; i<m; i++)
{
if (coins[i] <= N)
{
int sub_res = 1 + minCoins(coins, m, N-coins[i]);
// see if result can minimized
if (sub_res < res)
res = sub_res;
}
}
return res;
}

Dynamic Programming Solution :


Since same suproblems are called again and again, this problem has Overlapping Subprolems property.
Like other typical Dynamic Programming(DP) problems, re-computations of same subproblems can be
avoided by constructing a temporary array dp[] and memoizing the computed values in this array.

134
Telegram - https://t.me/campusdrive

1. Top Down DP
Code :
int minCoins(int N, int M)
{
// if we have already solved this subproblem
// return memoized result
if(dp[N] != -1)
return dp[N];
// base case
if (N == 0)
return 0;
// Initialize result
int res = INF;
// Try every coin that has smaller value than N
for (int i=0; i<M; i++)
{
if (coins[i] <= N)
{
int sub_res = 1 + minCoins(N-coins[i], M);
// see if result can minimized
if (sub_res < res)
res = sub_res;
}
}
return dp[N] = res;
}

135
Telegram - https://t.me/campusdrive

2. Bottom Up DP : ith state of dp : dp[i] : Minimum number of coins required to sum to i cents.
Code :
int minCoins(int N, int M)
{
//Initializing all values to infinity i.e. minimum coins to make any
//amount of sum is infinite
for(int i = 0;i<=N;i++)
dp[i] = INF;
//Base case i.e. minimum coins to make sum = 0 cents is 0
dp[0] = 0;
//Iterating in the outer loop for possible values of sum between 1 to N
//Since our final solution for sum = N might depend upon any of these
values
for(int i = 1;i<=N;i++)
{
//Inner loop denotes the index of coin array.
//For each value of sum, to obtain the optimal solution.
for(int j = 0;j<M;j++)
{
//i —> sum
//j —> next coin index
//If we can include this coin in our solution
if(coins[j] <= i)
{
//Solution might include the newly included coin.
dp[i] = min(dp[i], 1 + dp[i - coins[j]]);
}
}
}
//for(int i = 1;i<=N;i++)cout<<i<<“ “<<dp[i]<<endl;
return dp[N];
}
T = O(N*M)

136
Telegram - https://t.me/campusdrive

q LONGEST INCREASING SUBSEQUENCE


The Longest Increasing Subsequence (LIS) problem is to find the length of the longest subsequence
of a given sequence such that all elements of the subsequence are sorted in increasing order.
For example, the length of LIS for {10, 9, 3, 5, 4, 11, 7, 8} is 4 and LIS is {3, 4, 7, 8}.

Recurrence relation :
L(i) = 1 + max{ L(j) } where 0 < j < i and arr[j] < arr[i];
or

L(i) = 1, if no such j exists.


return max(L(i)) where 0 < i < n

Code :
int LIS(int n)
{
int i,j,res = 0;
/* Initialize LIS values for all indexes */
for (i = 0; i < n; i++ )
lis[i] = 1;
/* Compute optimized LIS values in bottom up manner */
for (i = 1; i < n; i++ )
for (j = 0; j < i; j++ )
if ( arr[i] > arr[j] && lis[i] < lis[j] + 1)
lis[i] = lis[j] + 1;
/* Pick maximum of all LIS values */
for (i = 0; i < n; i++ )
if (res < lis[i])
res = lis[i];
return res;
}

T = O(n^2)
We can also print the Longest Increasing Subsequence as:
void print_lis(int n)
{
//denotes the current LIS
//initially it is equal to the LIS of the whole sequence
int cur_lis = LIS(n);
//denotes the previous element printed

137
Telegram - https://t.me/campusdrive

//to print the LIS, previous element printed must always be larger than current
//element (if we are printing the LIS backwards)
//Initially set it to infinity
int prev = INF;
for(int i = n-1;i>=0;i—)
{
//find the element upto which the LIS equal to the cur_LIS
//and that element is less than the previous one printed
if(lis[i] == cur_lis && arr[i] <= prev)
{
cout<<arr[i]<<“ “;
cur_lis—;
prev = arr[i];
}
if(!cur_lis)
break;
}
return;
}

q LONGEST COMMON SUBSEQUENCE


LCS Problem Statement: Given two sequences, find the length of longest subsequence present in
both of them.
A subsequence is a sequence that appears in the same relative order, but not necessarily contiguous.
For example, “abc”, “abg”, “bdf”, “aeg”, ‘“acefg”, ... etc are subsequences of “abcdefg”. So a string of
length n has 2^n different possible subsequences.
Example :
LCS for input Sequences “ABCDGH” and “AEDFHR” is “ADH” of length 3.
LCS for input Sequences “AGGTAB” and “GXTXAYB” is “GTAB” of length 4

Recurrence Relation :
LCS(str1, str2, m, n) = 0, if m = 0 or n = 0 //Base Case
LCS(str1, str2, m, n) = 1 + LCS(str1, str2, m-1, n-1), if str1[m] = str2[n]
LCS(str1, str2, m, n) = max{LCS(str1, str2, m-1, n), LCS( str1, str2, m, n-1)}, otherwise
LCS can take value between 0 and min(m,n).

138
Telegram - https://t.me/campusdrive

Code :
int lcs( char *str1, char *str2, int m, int n )
{
//Following steps build L[m+1][n+1] in bottom up fashion. Note
//that L[i][j] contains length of LCS of str1[0..i-1] and str2[0..j-1]
for (int i=0; i<=m; i++)
{
for (int j=0; j<=n; j++)
{
if (i == 0 || j == 0)
LCS[i][j] = 0;
else if (str1[i-1] == str2[j-1])
LCS[i][j] = LCS[i-1][j-1] + 1;
else
LCS[i][j] = max(LCS[i-1][j], LCS[i][j-1]);
}
}
//Print LCS
int i = m, j = n, index = LCS[m][n];
//array containing the final LCS
char *lcs_arr = new char[index];
while (i > 0 && j > 0)
{
// If current character in str1[] and str2[] are same, then
// current character is part of LCS
if (str1[i-1] == str2[j-1])
{
// Put current character in result
lcs_arr[index-1] = str1[i-1];
// reduce values of i, j and index
i—;
j—;

139
Telegram - https://t.me/campusdrive

index—;
}
// If not same, then find the larger of two and
// go in the direction of larger value
else if (LCS[i-1][j] > LCS[i][j-1])
i—;
else
j—;
}
// Print the lcs
cout << “LCS of “ << str1 << “ and “ << str2 << “ is “<< lcs_arr << endl;
//L[m][n] contains length of LCS for str1[0..n-1] and
str2[0..m-1]
return LCS[m][n];
}

T = O(mn)

q EDIT DISTANCE
Problem Statement: Given two strings str1 and str2 and below operations can be performed on str1.
Find min number of edits(operations) required to convert str1 to str2.
q Insert q Remove q Replace
All the above operations are of equal cost.
http://www.spoj.com/problems/EDIST/

Example :
str1 = “cat”
str2 = “cut”
Replace ‘a’ with ‘u’, min number of edits = 1
str1 = “sunday”
str2 = “saturday”
Last 3 characters are same, we only need to replace “un” with “atur”.
Replace n—>r and insert ‘a’ and ‘t’ before ‘u’, min number of edits = 3

140
Telegram - https://t.me/campusdrive

Recurrence Relation :
if str1[m] = str2[n]
editDist(str1, str2, m, n) = editDist(str1, str2, m-1, n-1)
else
editDist(str1, str2, m, n) = 1 + min{editDist(str1, str2, m-1, n) //Remove
editDist(str1, str2, m, n-1) //Insert
editDist(str1, str2, m-1, n-1) //Replace
}

Transform “Sunday” to “Saturday” :


Last 3 are same, so ignore. We will transform Sun —> Satur
(Sun, Satur) —> (Su, Satu) //Replace ‘n’ with ‘r’, cost= 1
(Su, Satu) —> (S, Sat) //Ignore ‘u’, cost = 0
(S, Sat) —> (S,Sa) //Insert ‘t’, cost = 1
(S,Sa) —> (S,S) //Insert ‘a’, cost = 1
(S,S) —> (‘’,’’) //Ignore ‘S’, cost = 0
(‘’, ‘’) —> return 0

Dynamic Programming :
int editDist(string str1, string str2){
int m = str1.length();
int n = str2.length();
// dp[i][j] —> the minimum number of edits to transform str1[0...i-1] to
str2[0...j-1]
//Fill up the dp table in bottom up fashion
for(int i = 0;i<=m;i++)
{
for(int j = 0;j<=n;j++)
{
//If both strings are empty
if(i == 0 && j == 0)
dp[i][j] = 0;
//If first string is empty, only option is to
//insert all characters of second string

141
Telegram - https://t.me/campusdrive

//So number of edits is the length of secondstring


else if(i == 0)
dp[i][j] = j;
//If second string is empty, only option is to
//remove all characters of first string
//So number of edits is the length of first string
else if(j == 0)
dp[i][j] = i;
//If the last character of the two strings are
//same, ignore this character and recur for the
//remaining string
else if(str1[i-1] == str2[j-1])
dp[i][j] = dp[i-1][j-1];
//If last character is different, we need at least one
//edit to make them same. Consider all the possibilities
//and find minimum
else
dp[i][j] = 1 + min(min(
dp[i-1][j], //Remove
dp[i][j-1]), //Insert
dp[i-1][j-1] //Replace
);
}
}
//Return the most optimal solution
return dp[m][n];
}

q 0-1 KNAPSACK PROBLEM


For each item you are given its weight and its value. You want to maximize the total value of all the
items you are going to put in the knapsack such that the total weight of items is less than knapsack’s
capacity. What is this maximum total value?
http://www.spoj.com/problems/KNAPSACK/
To consider all subsets of items, there can be two cases for every item: (1) the item is included in the
optimal subset, (2) not included in the optimal set.

142
Telegram - https://t.me/campusdrive

Therefore, the maximum value that can be obtained from n items is max of following two values.
1. Maximum value obtained by n-1 items and W weight (excludingnth item).
2. Value of nth item plus maximum value obtained by n-1 items and W minus weight of the nth item
(including nth item).
If weight of nth item is greater than W, then the nth item cannot be included and case 1 is the only
possibility.

Recurrence Relation :
//Base case
//If we have explored all the items all we have reached the maximum capacity
of Knapsack
if (n=0 or W=0)
return 0
//If the weight of nth item is greater than the capacity of knapsack, we cannot
include //this item
if (wieght[n] > W)
return solve(n-1, W)
otherwise
return max{ solve(n-1, W), //We have not included the item
solve(n-1, W-weight[n]) //We have included the item in the knapsack
}

If we build the recursion tree for the above relation, we can clearly see that the property of
overlapping sub-problems is satisfied. So, we will try to solve it using dynamic programming.
Let us define the dp solution with states i and j as
dp[i,j] —> max value that can be obtained with objects u
pto index i and knapsack capacity of j.

The most optimal solution to the problem will be dp[N][W] i.e. max value that can be obtained upto
index N with max capacity of W.

Code :
int knapsack(int N, int W)
{
for(int i = 0;i<=N;i++)
{
for(int j = 0;j<=W;j++)
{
//Base case

143
Telegram - https://t.me/campusdrive

//When no object is to be explored or our knapsack’s capacity is 0


if(i == 0 || j == 0)
dp[i][j] = 0;
//When the wieght of the item to be considered is more than the
//knapsack’s capacity, we will not include this item
if(wt[i-1] > j)
dp[i][j] = dp[i-1][j];
else
dp[1][j] = max(
//If we include this item, we get a value of val[i-1] but the
//capacity of the knapsack gets reduced by the weight of that
//item.
val[i-1] + dp[i-1][j - wt[i-1]],
//If we do not include this item, max value will be the
//solution obtained by taking objects upto index i-1, capacity
//of knapsack will remain unchanged.
dp[i-1][j]);
}
}
return dp[N][W];
}

Time Complexity = O(NW)


Space Complexity = O(NW)

Can we do better?
If we observe carefully, we can see that the dp solution with states (i,j) will depend on state (i-1, j) or
(i-1, j-wt[i-1]). In either case the solution for state (i,j) will lie in the i-1th row of the memoization
table. So at every iteration of the index, we can copy the values of current row and use only this row
for building the solution in next iteration and no other row will be used. Hence, at any iteration we
will be using only a single row to build the solution for current row. Hence, we can reduce the space
complexity to just O(W).

Space-Optimized DP Code :
int knapsack(int N, int W)
{
for(int j = 0;j<=W;j++)
dp[0][j] = 0;

144
Telegram - https://t.me/campusdrive

for(int i = 0;i<=N;i++)
{
for(int j = 0;j<=W;j++)
{
//Base case
//When no object is to be explored or our knapsack’s capacity is 0
if(i == 0 || j == 0)
dp[1][j] = 0;
//When the wieght of the item to be considered is more than the
//knapsack’s capacity, we will not include this item
if(wt[i-1] > j)
dp[1][j] = dp[0][j];
else
dp[1][j] = max(
//If we include this item, we get a value of val[i-1] but the
//capacity of the knapsack gets reduced by the weight of that
//item.
val[i-1] + dp[0][j - wt[i-1]],
//If we do not include this item, max value will be the
//solution obtained by taking objects upto index i-1, capacity
//of knapsack will remain unchanged.
dp[0][j]);
}
//Here we are copying value of current row into the previous row,
//which will be used in building the solution for next iteration ofrow.
for(int j = 0;j<=W;j++)
dp[0][j] = dp[1][j];
}
return dp[1][W];
}

Time Complexity: O(N*W)


Space Complexity: O(W)

145
Telegram - https://t.me/campusdrive

19 Tries

Tries :
A Trie, (also known as a prefix tree) is a special type of tree used to store associative data structures.
A trie (pronounced try) gets its name from retrieval - its structure makes it a stellar matching algorithm.
If we need to search a word of length m, in a paragraph of n lines, if normal string searching is used
then it would take O(m*n) time, but as the number of lines increases, this complexity becomes
unbearably slow and gives a bad user experience.
Think of using Microsoft Word, and searching a word in it. Would you like the software if it takes
minutes to search?
This is where Trie comes in the picture, and is suitable data structure for applications like dictionary,
word processing, etc.

General Implementation :
We need to make a class Node to represent each node. The class Node contains a Character data,
a boolean is Terminal which determines if a word ends at this node or not and a HashMap of Character
and Node, children.
Initially we add ‘\n’ at the root node and all the
words are its children. The node marked with red
circle shows that the boolean isTerminal for them
is true. Some of the words in the trie are dog,
trap, drag, tap, etc.

Time Complexity for a Trie :


The formation of trie has a time complexity of
O(n) where n is length of the paragraph but once
the trie is created, the time for searching a word
is only O(m) where m is the length of the word.
So searching a word has been reduced from
O(n*m) to O(m). This makes finding a word very
fast because generally the size of word is small
and we get nearly constant searching time.

146
Telegram - https://t.me/campusdrive

Following is the code in C++ for creating a class Trie


class Node {
public:
bool isterminal;
Node* characters[26];
Node(){
isterminal = false;
fill(characters, characters + 26, (Node*)NULL);
}
};
class Trie{
Node * root;
public:
Trie(){
root = new Node();
}
int charToInt(char c){
return c - ‘a’;
}
void addWord(char word[]){
Node * cur = root;
for(int i = 0; word[i] != ‘\0’; ++i){
int idx = charToInt(word[i]);
if (cur->characters[idx] == NULL){
//furthur levels do NOT exist
cur->characters[idx] = new Node();
}
cur = cur->characters[idx];
}
cur->isterminal = true;
}
bool searchWord(char word[]){
Node * cur = root;
for(int i = 0; word[i] != ‘\0’; ++i){
int idx = charToInt(word[i]);
if (!cur->characters[idx]) return false;
cur = cur->characters[idx];
}
return cur->isterminal;
}
};

147

You might also like