Data Structure (AutoRecovered)
Data Structure (AutoRecovered)
You created an ArrayList containing three objects of the Student type. Then, you
added a new Student object to it. But what would happen if you added a new object
of any other type (for example, a String or Professor type) to the same
ArrayList? Let’s find out in this video.
So, you saw that you got an error when you tried to add a new object of
the String type to the ArrayList. But before you start fixing this error and making
your code type-safe, please answer the following questions. The exercise below will
give you more clarity on the reason behind the run-time error.
You now know that you can create an ArrayList containing elements of different
types, where the data type of all the objects can be ‘Object’.
In most cases, you should create an ArrayList of the same data type (like the
Student ArrayList in our example). Otherwise, if you create a generic ArrayList that
holds generic objects, you will need to cast the element back to its original type (i.e.
Object → Student). However, you may encounter run-time errors if you accidentally
store an element that cannot be cast (like the String → Student example you saw
above).
To deal with this, let’s learn about a special type of ArrayList in the next segment.
ArrayList Using Generics
In this segment, there are two major issues you will learn to deal with:
1. You need to avoid typecasting multiple times.
2. You should lower the chances of potential run-time errors while adding
incompatible elements to an ArrayList and typecasting them.
So, let’s look at the Java ArrayList, which has a wonderful solution to both these
issues.
This special ArrayList, where you cannot add elements of different data types, is
referred to as an ArrayList using Generics.
Earlier, when you could add elements of different types in the ArrayList, they were
non-generic in nature. Generics ensure that you can always add elements of the same
data type in an ArrayList. But if you try to add elements of any other data type, this
will give you a compiler error.
Advantages of using generics
1. The ArrayList of a specific data type and the elements of other data types are
restricted.
2. Typecasting is not required.
3. There is a conversion of potential run-time errors into compiler errors.
Format to Declare ArrayList Using Generics
The class ArrayList can be declared using generics in the method shown below:
ArrayList<datatype> listName =new ArrayList<datatype>();
Here, the data type to be mentioned is always non-primitive (reference). For example,
Student, String, etc., which are declared as classes. Primitive data types such as int,
char, and double cannot be used here. If you want to store primitive data types in
ArrayList classes, you’ll need to use their Object cousins, such as Integer, Double,
Float, and Boolean.
So, if you need to create the Generics ArrayList with elements of the following data
types, you can use the following formats:
1. ArrayList of int-type values —
ArrayList<Integer> list = new ArrayList<Integer>( );
2. ArrayList of double-type values —
ArrayList<Double> list = new ArrayList<Double>( );
3. ArrayList of float-type values —
ArrayList<Float> list = new ArrayList<Float>( );
Now, it’s time to write some code to create an ArrayList using generics.
import java.util.ArrayList;
class Source {
random.add(2f);
random.add(4f);
random.add(5f);
random.add(10f);
random.add(99.9f);
printrandom(random);}
for(Object o :abc){
System.out.println(o);
}
Operations on ArrayList: I
Now, since you learnt how to create an ArrayList with a specific data type (using
generics), let’s learn to perform the following basic operations such as:
1. Adding an element at any arbitrary position
2. Removing an element
3. Searching for an element
Let’s start with the first operation — adding an element to any arbitrary position in
ArrayList.
The following methods, predefined in the ArrayList class, will help you add elements
to it:
1. add(Object o): This appends the specified object ‘o’ to the end of ArrayList. Its
return type is Boolean, which returns TRUE when the element is added to the
list.
2. add(int index, Object o): This inserts the specified object into the specified
position in ArrayList.
Answer the following questions to strengthen your understanding of the appropriate
uses of these functions.
Operations on ArrayList: II
Let’s now look at the second operation — removing an element from any index of
ArrayList.
You can remove elements from ArrayList using the following methods:
1. remove(int index): It removes the element from ArrayList, at the specified
index.
2. clear( ): It removes all the elements from ArrayList.
Let’s now move to the third operation — searching for an element in ArrayList.
You can search for an element in ArrayList using the following method:
contains(Object o): It searches for the element in ArrayList and returns ‘true’ if the
element is present.
Additional Reading
You can go through the Method Summary and Method Detail tables on this page to
find out more about other methods in the ArrayList class. You can use these methods
to perform different operations.
Now, it’s time to perform a minor operation on the ArrayList code given below.
Lists and Polymorphism
Till now, you learnt about ArrayList and LinkedList in detail. Now, let’s find out what
lists are exactly and how they are related to both these data structures.
In reality, List is an interface that is implemented by
the ArrayList and LinkedList classes. This is the reason why you can instantiate
both 'ArrayList' and 'LinkedList' by declaring the type of variable as List.
Refer to the diagram below to understand how all these classes and interfaces are
linked to a larger interface named Collection.
Performance Measurement -
I
Let’s start by looking at the code which will help you to compare ArrayList with
LinkedList at the time of performing certain operations.
Our professor has created a skeleton of the code — to estimate the time taken for both
ArrayList as well as LinkedList to perform any operation, and compare their
performances with the time taken by them.
Since we now have the basic skeleton code for measuring the performance in terms of
the time taken to perform an action, let’s start with the first operation — get
operation.
The get(int index) method returns the element at the specified index in the list.
So, you measured the performance of both the lists for the ‘get’ operation. Here
you also saw that in order to retrieve elements from any specific index,
ArrayList is faster than that of LinkedList.
Before we go into the explanation for this, let’s compare the lists for one
operation.
Performance Measurement -
II
Let’s continue our experiment of comparing the performance of ArrayList and
LinkedList with respect to other operations as well. Let’s continue with the add
operation.
add(Object o) appends the specified object ‘o’ to the end of any list.
add(int index, Object o) inserts the specified object at the specified position in
the lis
You measured the performance of both the lists for the add operation as well.
Here you saw that in order to add a new element to any position, LinkedList
is faster than that of ArrayList.
In the next few pages of this session, we will dive deeper into the reason for the
difference in performance by both these lists.
Data structure and algorithm
Definition: It’s a method of solving problem through sequence of instruction.
Algorithm 1
Go through the ThreadLocalRandom class which is used by the professor in the below
video to generate a random number in the specified range.
ThreadLocalRandom is a class in java which extends the Random class, below are
some methods of ThreadLocalRandom class used in the video below
current()
Returns the current thread's ThreadLocalRandom.
nextInt(int origin, int bound)
Returns a pseudorandom int value between the specified origin (inclusive) and the
specified bound (exclusive).
Therefore, “ThreadLocalRandom.current().nextInt(int origin, int bound)” returns a
random value in the specified range.
Now, let's continue our discussion on algorithms and look into an imaginary problem
scenario where you can write an algorithm to solve the same.
import java.util.concurrent.ThreadLocalRandom;
Parameters for Algorithm
Efficiency
time complexity : it is a relation between input size and running time (operation)
In the previous segment, you learnt about two different algorithms finding duplicate
student IDs in the registered student data. Now, in order to find the efficient algorithm
between the two, you need certain parameters to compare them.
In this video, you will be introduced to the parameters to measure the efficiency of an
algorithm.
Replay
Mute
Current Time 1:05
Duration 1:05
Loaded: 100.00%
1.25x
How to declare a linked list?
It is simple to declare an array, as it is of single type, while the declaration of linked list is
a bit more typical than array. Linked list contains two parts, and both are of different
types, i.e., one is the simple variable, while another is the pointer variable. We can
declare the linked list by using the user-defined data type structure.
1. struct node
2. {
3. int data;
4. struct node *next;
5. }
In the above declaration, we have defined a structure named as node that contains two
variables, one is data that is of integer type, and another one is next that is a pointer
which contains the address of next node.
Non contiguous memory = it connects the previous data with the next data
variable size = size is not the biggest concern we can put the element as much as we
want
unless and until the system memory is not full
insert and deletion= it takes big O of (1) constant
search= the time complexity of search of linked list in O(n).
Types of Linked list
Linked list is classified into the following types -
o Circular singly linked list - In a circular singly linked list, the last node of the list
contains a pointer to the first node of the list. We can have circular singly linked
list as well as circular doubly linked list.
o Circular doubly linked list - Circular doubly linked list is a more complex type of
data structure in which a node contains pointers to its previous node as well as
the next node. Circular doubly linked list doesn't contain NULL in any of the
nodes. The last node of the list contains the address of the first node of the list.
The first node of the list also contains the address of the last node in its previous
pointer.
Advantages of Linked list
The advantages of using the Linked list are given as follows -
o Dynamic data structure - The size of the linked list may vary according to the
requirements. Linked list does not have a fixed size.
o Insertion and deletion - Unlike arrays, insertion, and deletion in linked list is easier.
Array elements are stored in the consecutive location, whereas the elements in the linked
list are stored at a random location. To insert or delete an element in an array, we have
to shift the elements for creating the space. Whereas, in linked list, instead of shifting, we
just have to update the address of the pointer of the node.
o Memory efficient - The size of a linked list can grow or shrink according to the
requirements, so memory consumption in linked list is efficient.
o Implementation - We can implement both stacks and queues using linked list.
Singly Linked list
It is the commonly used linked list in programs. If we are talking about the linked list, it means it
is a singly linked list. The singly linked list is a data structure that contains two parts, i.e., one is
the data part, and the other one is the address part, which contains the address of the next or the
successor node. The address part in a node is also known as a pointer.
Suppose we have three nodes, and the addresses of these three nodes are 100, 200 and 300
respectively. The representation of three nodes as a linked list is shown in the below figure:
We can observe in the above figure that there are three different nodes having address
100, 200 and 300 respectively. The first node contains the address of the next node, i.e.,
200, the second node contains the address of the last node, i.e., 300, and the third node
contains the NULL value in its address part as it does not point to any node. The pointer
that holds the address of the initial node is known as a head pointer.
The linked list, which is shown in the above diagram, is known as a singly linked list as it
contains only a single link. In this list, only forward traversal is possible; we cannot
traverse in the backward direction as it has only one link in the list.
Suppose we have three nodes, and the address of these nodes are 100, 200 and 300,
respectively. The representation of these nodes in a doubly-linked list is shown below:
As we can observe in the above figure, the node in a doubly-linked list has two address
parts; one part stores the address of the next while the other part of the node stores
the previous node's address. The initial node in the doubly linked list has
the NULL value in the address part, which provides the address of the previous node.
1. struct node
2. {
3. int data;
4. struct node *next;
5. struct node *prev;
6. }
class ListNode{
public int data;
public ListNode next;
}
//to get the input in LinkedList//
int n = input.nextInt();
head=null;
if (n>0){
head= new ListNode(input.nextInt());
ListNode temp=head;
for (int i =0;i<n;i++){
temp.next= new ListNode(input.nextInt());
temp=temp.next;
}
}
Linked list
implementation………
public class Main {
public static void main(String[] args) {
Main LinkedList = new Main();
Main.addfirst(2);
Main.addfirst(3);
Main.addLast(4);
Main.printList();
Main.deleteFirst();
Main.printList();
while (lastNode.next!=null){
secondLast=secondLast.next;
lastNode= lastNode.next;
}
secondLast.next=null;
}
public static void printList(){
if (head==null){
System.out.println("List is Empty");
return;
}
ListNode currNode =head;
while (currNode!=null){
System.out.print(currNode.data+"-->");
currNode=currNode.next;
}
System.out.print("NUll");
}
}
class ListNode{
public int data;
public ListNode next;
}
}
Playback Rate
Quality Levels
Picture-in-PictureFullscreen
4421328
So, the parameters that determine the efficiency of an algorithm are the time
taken (running time) and the memory space required to execute an algorithm.
Both the time taken and the memory space required to execute an algorithm are
calculated as functions of the input size. Since you typically don’t know what the
input size will be beforehand, you simply use the variable ‘n’ to represent the
potential input size of the algorithm.
Now, the question that arises is — how do you calculate the time taken to execute
an algorithm?
Each instruction in an algorithm takes a specific amount of time to execute and certain
instruction set is executed for more than once i.e.instruction set inside a 'for' or 'while'
loop. To analyse an algorithm, you must calculate the number of times an instruction
set is executed with respect to the input size (n) rather than the exact time values. This
is because the time taken to execute an algorithm depends on various external factors
such as processor speed, the compiler, etc. since these external factors can vary from
computer to computer.
Note: When calculating the number of times an instruction set is executed, software
developers and computer scientists tend to consider the worst case, i.e. the specific
case in which the instruction set is executed a maximum number of times.
There are three cases in the analysis of an algorithm
1. Best case
2. Average case
3. Worst Case
Before going into the depth of the above three cases, once go through the below
program
import java.util.Scanner;
Input 1:
1
Output:
Entered number found at position:1
Input 2:
0
Output:
Entered number found at position:10
Input 3:
15
Output:
Sorry! Entered number not found
Best case:
The lower bound on the time taken by an algorithm is the best case. In the above
program for the Input 1, the output was ‘Entered no found at position:1’ it means that
the number found at first position itself, ‘for’ loop in the program iterates n times until
it finds the required value in the array at ith index. Since the required value is found at
the first index, the ‘for’ loop terminates in the first iteration itself, hence this is the
best case for this program. The no of instructions get executed in the best case is
constant.
Average case:
In this case, consider all possible different types of input data and calculate the time
taken by the algorithm for all those inputs. Now add all those calculated time taken
values and divide it by the total number of inputs, then the obtained value is the
average time.
Worst Case:
The upper bound of the time taken by an algorithm is the worst case. In the above
program for the Input 2, the output was ‘Entered number found at position:10’ and for
Input 3 the output was ‘Sorry! Entered number not found’ for both the inputs the
program searches throughout the array so here it will take the maximum time for the
algorithm to execute. Hence this is the worst case.
Most of the times, the worst case time complexity of an algorithm is used for the
algorithm analysis because the worst case time complexity guarantees the upper
bound of the time that the algorithm takes i.e., the maximum time taken by the
algorithm.
Think and answer the question below to understand the worst case in an algorithm.
Data Structures
01.Abstract Data types is a entity which defines the capabilities of a data arrangement
Eg List
For eg Arraylist,Linkedlist.
1. Finding an element:
ArrayList: O(1)
LinkedList: O(N)
ArrayList: O(N)
LinkedList: O(1)
ArrayList: O(N)
LinkedList: O(N)
Introduction to Stacks
STACK:
Stacks follow LIFO (Last In First Out) order.
Here are the most common definitions:
1. Push: Inserting an element into the stack
2. Pop: Deletes the topmost element from the stack
3. Peek: Returns the topmost element in the stack
Operation Worst Time Complexity
Push O(1)
Pop O(1)
Peek O(1)
QUEUE:
Queues follow FIFO (First In First Out) order.
Here are the most common definitions:
1. Enqueue: Adding an element to the queue
2. Dequeue: Removing an element from the queue.
3. Peek: Returns the frontmost element in the queue
Operation Worst Time Complexity
Enqueue O(1)
Dequeue O(n)
Peek O(1)
The ‘last in, first out’ property of the stack data structure makes it more run-
time efficient than a simple array or a linked list in finding the last function called.
Feedback:
When clothes are kept in a pile, the piece of clothing that goes in last sits at the top of
the pile. Therefore, when you want to get a cloth from the pile, you pick whatever is at
the top. This was the last item that you added to the pile. Therefore, a pile of clothes is
an example of 'last in, first out'.
Feedback:
When you use the Undo option in a text editor, the last change that you made is the
first to be reverted. Therefore, such options are examples of 'last in, first out'.
In simple word the first work is done last and the last work is done first
1.Element inserted and removed from a single end only (top of the Stack).
2. Order of insertion and Deletion follows LIFO(last in, first out ) order.
Insertion of elements is called as push.
Removal of elements is called as pop.
If ‘n’ elements are pushed into a stack, then the element that is pushed in first
will be the last one to be removed (popped) from the stack.
The element that is pushed in first will be at the bottom of the stack if more elements
are pushed in after it. The element that is pushed in last will be the first one to be
popped from the stack. Therefore, the element that is pushed in first will be the last one
to be removed from the stack.
Program Stack
The program stack is also referred to as a call stack, a run-time stack or an execution
stack.
There is an extremely common error called ‘stack overflow’, which occurs when you
use up more memory for a stack than your program is supposed to. For example,
when you frame a recursive logic erroneously and give an infinite recursive call, your
compiler will throw a stack overflow error when the size of the stack grows to exceed
its maximum allowed size.
search(object element) - searches for the element in the stack and returns its location
}
if (count==0){
return true;
}else {
return false;
}
code for parenthesis problem
}
public static boolean match(String paren){
int count =0;
char[] parens = paren.toCharArray();
}
if (count==0){
return true;
}else {
return false;
}
}
Parenthesis Matching
Problem – II
In the previous segment, you saw a solution to the parenthesis matching problem, but
you used only a single type of bracket, i.e., '()'. However, your compilers include more
types of brackets. So, we will begin this segment with the following video, where we
will see how the previous algorithm would have fared if we had considered two
different types of brackets: '{}' and '()'.
int count1 = 0, count2 = 0;
char[] chars = parens.toCharArray();
for (char c : chars) {
if (c == '(') {
count1++;
} else if (c == ')') {
if (count1 > 0) {
count1--;
} else{
return false;
}
}
else if (c == '{') {
count2++;
} else if (c == '}') {
if (count2 > 0) {
count2--;
} else {
return false;
}
} else {
throw new Exception("Invalid character " + c);
}
}
if (count1 == 0 && count2 == 0) {
return true;
} else {
return false;
}
}
}
// you could also initialise the stack from the inbuilt class
// private static Stack<Character> stack = new Stack<Character>();
class MyStack<T> {
private LinkedList<T> list = new LinkedList<T>();
public T pop() {
if (this.list.size() > 0) {
T e = list.get(list.size() - 1);
list.remove(list.size() - 1);
return e;
}
throw new EmptyStackException();
}
The counter-based approach to the parenthesis matching problem cannot account for different
combinations of parentheses, brackets, braces, etc. For example, if the string is '{{{(}}}}', then the
counter-based approach would label it as correct, when, in fact, it is an incorrect string.
System.out.println(stack.isEmpty());
System.out.println(stack.peek());
}
}
class StackUsingArray<T>{
public T[] array;
public int capacity;
public int index;
Emails are sent in FIFO order. So, if you send two emails to a user, then they will receive
them in the same order in which they were sent.
import java.util.*;
}
}
class Stack{
static Queue<Integer> q1 = new LinkedList<>();
static Queue<Integer> q2 = new LinkedList<>();
while (!q1.isEmpty()){
q2.add(q1.peek());
q1.remove();
}
Queue<Integer> q =q1;
q1=q2;
q2=q;
Time Complexity:
Push operation: O(N), As all the elements need to be popped out
from the Queue (q1) and push them back to Queue (q2).
Pop operation: O(1), As we need to remove the front element from
the Queue.
Auxiliary Space: O(N), As we use two queues for the implementation of a
Stack.
import java.util.*;
public class Main {
public static void main(String[] args) {
Stack s = new Stack();
Stack.push(1);
Stack.push(2);
Stack.push(3);
System.out.println("Size = "+Stack.size());
System.out.println(Stack.search(3));
Stack.pop();
System.out.println("Now the top element would be = "+Stack.top());
}
}
class Stack{
q1.remove();
q2.add(temp);
Queue<Integer> q3 =q1;
q1=q2;
q2=q3;
return temp;
}
}
Push operation: O(1), As, on each push operation the new element
is added at the end of the Queue.
Pop operation: O(N), As, on each pop operation, all the elements are
popped out from the Queue (q1) except the last element and pushed
into the Queue (q2).
Auxiliary Space: O(N) since 2 queues are used.
import java.util.*;
public class Main {
public static void main(String[] args) {
Stack s = new Stack();
Stack.push(1);
Stack.push(2);
Stack.push(3);
System.out.println("Size = "+Stack.size());
System.out.println(Stack.search(3));
}
String reverseString ="";
while (!input1.isEmpty()){
reverseString=reverseString+input1.remove();
}
if (reverseString.equals(name)){
System.out.println("It is a palindrome");
}else{
System.out.println("it is not a palindrome");
}
}
}
using Queue
Time complexity is O(n) and space complexity is O(n2).
Identify a Palindromic String
public static void checkPalindrome(String input) {
Stack<Character> input1 = new Stack<>();
for (int i =0;i<input.length();i++){
input1.push(input.charAt(i));
}
String reverseString ="";
while (!input1.isEmpty()){
reverseString=reverseString+input1.pop();
}
if (reverseString.equals(input)){
System.out.println("It is a palindrome");
}else{
System.out.println("it is not a palindrome");
}
}
}
using stack
Time complexity is O(n) and space complexity is O(n2).
Detect Duplicate Parentheses
public static String parenthesis(String inputString) {
Stack<Character> stack = new Stack<>();
for (int i =0;i<inputString.length();i++){
char c = inputString.charAt(i);
if (c!=')'){
stack.push(c);
}else {
char top = stack.peek();
stack.pop();
int count =0;
while (top!='('){
top=stack.peek();
stack.pop();
count++;
}
if (count<=1){
return "String contains duplicate parenthesis";
}
}
}
return " string does not contains duplicate parenthesis";
}
}
time complexity O(n)
space complexity O(n)
Hashing and Hash Functions
Through the following video, you will learn about the concepts of hash tables and
hash functions.
As we have been discussing that the hash table is mainly used to achieve a constant
run-time complexity, you will now see how a hash function is actually used to realise
this run-time of O(1).
A hash table is a data structure that stores records (or elements) according to its
associated “keys”. This is done by designing a hash function that takes the given keys
as input and outputs array indices at which the records are stored. In other words, each
record is stored using the array index obtained by hashing its key.
Note that hash tables stores data in the form of key and value. Each array contains a
hashed key along with a pointer that tells you the location at which the associated
record is stored.
An important point to note:
One important feature that a hash function must have is that it must compute fast,
since a time-expensive hash function would defeat the overall purpose of the fast
retrieval of a hash table. Now that you know what a hash function is, let’s move on to
a different mathematical hash function to add a new dimension to this topic.
Let’s say you’re working with mathematical operations such as divide, a modulus,
etc., which play a great role in a lot of hash functions. For instance, the hash function
H = i%10 would map any integer in the range [0-9], depending on the remainder you
get on dividing i by 10. E.g. i = 23, which gives H = 3 since 23 divided by 10 would
give you a remainder of 3
You heard the term Cardinality in this Session which basically is a measure of a set's
size or meaning the number of elements in the set. Eg. If you are given a set A-
{1,3,4,3,2,5}, the cardinality of set A is 6.
With the three major thing we can understand the hash function
Hash key ={1,2,3,4,5,6,7,8,}
Hash table ={0,1,2,3,4,5,6,7,8,9,10}
Hash function ={[kmod10],[kmodn],[mid square],[fold]}.
With the help of these you can easily get to know how and where the data is
getting stored
Hash collision
There are basically two types of collision
1.open hashing(chaining approach)
2.closed hashing(linear approach)
1.in open hashing suppose if we get same remainder for two values 24,56,
the remainder is 1 then that means it will store the data in chain format
2.closed hashing is about if the next space is empty then it will store the
data over there. Simple and clear concept.
Introduction to HashMap
Similarities between Hashtable and HashMap are as follows:
1. Both are the implementations of Map interface in java.
2. Both of them perform similar functions.
3. Both do not maintain any order of elements.
Differences between Hashtable and HashMap are as follows:
Hashtable HashMap
It is used in the older versions of It exists only in newer versions of Java i.e., it is part of Java since
Some of the commonly used methods of HashMap are:
Methods Operations
put(key,value) This method adds the specified key with the specified value to the HashMap.
If the key is present in the HashMap, then it removes the key along with the value
remove(key)
mapped to it.
containsKey(key) If there is any mapping to the specified key, then it returns true.
size() This returns the number of key-value mappings present in the HashMap.
get(key) Returns the value mapped to the specified key in the HashMap.
HashMap should be used if you want to search in the large amount of data
It is not useful for the small data.
Hashmap doesn’t store the data in the same order the way you have
inserted it
LinkedHashMap will help you store the data in the same order the way you
have inserted
for (Map.Entry<Integer,String>entry:map.entrySet()){
System.out.print(entry.getValue());
}
Set<Integer> keys = map.keySet();
for (int key : keys){
System.out.println(key+map.get(key));
}
this will print in iteration format
Introduction to HashSet
Implementations of the Set
There are three implementations of the Set interface in Java they are:
1. HashSet:
This is the most commonly used implementation of the set. Here the
elements are stored randomly, and duplicates are not allowed.
2. LinkedHashSet
Here, the order of the elements is maintained on the basis of their
insertion order, and no duplicates are allowed.
3. TreeSet
Here, the order of the elements is maintained by the inbuilt ordering or
by the explicit comparator (which can arrange it in any sorted order) of
TreeSet. Here as well duplicates are not allowed.
No. of Levels single level and all the elements are multiple levels and the elements are
Introduction to Trees
Tree Vocabulary
Let's now learn about various components of a tree and what we call them. Go
through the tree given below which follows a hierarchical order:
All the elements in a tree are called nodes. In the tree given above, the nodes are the
ones with the values 1, 2, 3, 4, 5, 6, and 7.
Nodes can be of different types. The topmost node in a tree is called the root node.
From the root node, descend some other nodes. The root node is called a parent if
there is at least one or more nodes descending from it directly. In that case, we can
call root node 1 as the parent node having node 2 as a child node. Node 1 is also
the parent of nodes 3 and 4. Similarly, node 2 is the parent of nodes 5 and 6 and node
4 is the parent of node 7. A node that does not have any child is called a leaf node. In
the tree given above, nodes 3, 5, 6, and 7 are called leaf nodes.
The nodes at the same level descending from the same parent are called siblings. In
the tree given above, nodes 2, 3, and 4 are siblings.
Following is the vocabulary for the tree given above:
Nodes: 1, 2, 3, 4, 5, 6, 7
Root Node: 1
Parent Nodes & their Children Nodes:
Parent - 1,
Children - 2, 3, 4
Parent - 2,
Children - 5, 6
Parent - 4, Child - 7
Leaf Nodes: 3, 5, 6, 7
Sibling Nodes: (2, 3, 4), (5, 6)
Properties of Trees
1. No Cycle in Tree
An important point to remember is that a tree cannot have any cycle. A cycle means
that in a tree, a child node cannot have two parent nodes. Go through the figure given
below:
You can see that the child node 4 has two parents - node 2 and node 3, which builds a
cycle in the figure given above. Thus, the above figure cannot be considered a tree.
Access (or Linked Lists < Trees < Accessing (or searching) an element in a tree is
Search) Arrays quicker than linked lists but slower than arrays.
Insertion / Arrays < Trees < Linked Inserting or deleting an element from a tree is quicker
Deletion Lists (Unordered) than arrays but slower than unordered linked lists.
Number of Linked Lists = Trees ≠ Like linked lists and unlike arrays, trees can consist of
In the binary tree given above, the root node 1 is a parent which has two child nodes -
2 and 3. 2 is the left child of 1 and 3 is the right child of 1. 4 is the left child of parent
node 2 and 5 is the right child of parent node 2. 3 has the right child as 6.
Binary Tree
Let’s assume that a node in a tree structure has data, a pointer to the left child, and a
pointer to the right child as its parts. If the tree’s physical representation with nodes is as
shown in the figure, find its logical representation from the given options.
Types of Binary Trees
Full Binary Tree:
Let's understand from Srishti what a full binary tree is in the following video.
In a full binary tree, every node has 2 child nodes except the leaf nodes.
Example 1:
In the binary tree given above, all the nodes have 2 children except the leaf nodes at
the last level. Thus, it is a full binary tree.
Example 2:
The binary tree given above is a full binary tree because all the trees have 2 children
except the leaf nodes - node 2, node 5, and the nodes at the last level. Remember that
a node is a leaf node if it does not have any child. A leaf node is not necessarily
needed to be present at the last level.
Complete Binary Tree:
Now here you will understand the concept of a complete binary tree.
In a complete binary tree, all the levels are completely filled. The exception to
this exists for the last level meaning that the last level may or may not be
completely filled. This means that the second last level may or may not have both
the children.
With this exception, it is also necessary that the last level must have all the keys
as left as possible. In other words, a node in the second-last level cannot have a right
child without having all the nodes in its left side.
Example 1:
In the example given above, all the levels are completely filled. Even the last level is
completely filled. Thus, the tree given above is a complete binary tree.
Example 2:
In the example given above, all the levels are completely filled except the last level.
The node 3 at the second last level does not have any child. Thus, both the conditions
hold true for the above binary tree to be a complete binary tree.
Example 3:
In the example given above, the last level is not completely filled but that is an
exception and is allowed. However, there are two violations of the rule here. The first
violation is that the second last level is not completely filled because node 8 does not
have 2 children. The second violation is that the last level does not have the leaf nodes
completely on the left side because of node 4 containing a child without node 1
containing 2 children. Because of these violations, the binary tree given above is not a
complete binary tree. Note that even if one of these violations would have occurred,
still the above binary tree would not be called a complete binary tree.
tree.root.left.right=new Node(5);
tree.root.left.left=new Node(4);
tree.root.right.right=new Node(7);
tree.root.right.left=new Node(6);
System.out.println(tree);
}
}
class Node{ // this class is having a data and left side and a right side//
int data; // value contained inside a node
Node left,right; // left & right children of a node
// constructor to set the data of a node to the passed value and make it a
leaf node
Node(int data){
this.data=data;
left=right=null;
}
}
class Tree{
Node root; // root node of the binary tree
It is important to note that sometimes we consider the level of the root node as 0
while at other times, we consider the level of the root node as 1. We usually
follow the latter.
Stage 3: When both its left and right subtrees are visited
Depending on which stage the action is performed at, the algorithm becomes a
particular variant of depth-first traversal. An action can be anything. It may be
printing its value, comparing its value with some other node(s) etc.
DFS: Pseudocode & Code
Preorder code
public void preOrderDFS(Node node){
if (node==null){
return;
}
System.out.print(node.data+" ");
preOrderDFS(node.left);
preOrderDFS(node.right);
}
Inorder Code
public void inOrderDFS(Node node) {
if (node==null){
return;
}
inOrderDFS(node.left);
System.out.print(node.data+" ");
inOrderDFS(node.right);
}
postOrder code
public void postOrderDFS(Node node){
if (node==null){
return;
}
postOrderDFS(node.left);
postOrderDFS(node.right);
System.out.print(node.data+" ");
}
finding height
public int heigth(Node node){
if (node==null)
return 0;
else {
int leftDepth= heigth(node.left);
int rightDepth= heigth(node.right);
if (leftDepth>rightDepth)
return (leftDepth+1);
else
return (rightDepth+1);
}
Tree Traversal: Breadth-First
Search (BFS)
In simple word the BFS traverse through one level of children
So, in a BFS, the nodes at a certain level are visited before moving on to the next
level. So basically, you first visit the root node, then all the nodes at level 1, then all
the nodes at level 2, and so on. Hence, in this algorithm, you move along the breadth
of a tree, before hopping on to the next level. This is the reason why Breadth-First
Search (BFS) traversal is also called the Level-Order traversal.
BFS (Recursive): Pseudocode &
Code
This code will print the data
public void levelOrderOrBFS(){
int h = height(root);
for (int i =1;i<=h;i++){
printNodesAtLevel(root,i,1);
}
}
this code will print the data if level==currentlevel and if it is not equal then
we increment the currentlevel by 1 in both the treeside
public void printNodesAtLevel(Node root,int level,int currentlevel){
if (root==null){
return;
}if (level==currentlevel){
System.out.print(root.data+" ");
}else {
printNodesAtLevel(root.left,level,currentlevel+1);
printNodesAtLevel(root.right,level,currentlevel+1);
}
}
insert node
public Node insertNode(int[] elementsArr, Node node, int i){
if(i < elementsArr.length){
node = new Node(elementsArr[i]);
while (!q.isEmpty()){
Node node= q.peek();
q.remove();
System.out.print(node.data+" ");
if (node.left!=null){
q.add(node.left);
} else if (node.right!=null) {
q.add(node.right);
}
}
}
finding maximum
public int findMax(Node root) {
if (root == null) {
return 0;
} else {
int data = root.data;
int leftSide = findMax(root.left);
int rightSide = findMax(root.right);
if (leftSide > data) {
data = leftSide;
} else if (rightSide>data) {
data=rightSide;
}
return data;
}
}
Mirror a Tree
public void swap(Node node){
if (node==null)
return;
while (queue.size()>0){
Node temp = queue.peek();
queue.remove();
if (temp.left!=null)
queue.add(temp.left);
if (temp.right!=null)
queue.add(temp.right);
}
}
swap1(root.left);
swap1(root.right);
return node;
}
Summary
In this segment you learnt the following:
1. Evolution of tree-based data structures was due to the non-linearity of data.
Every data point in the tree is connected to several other data points such that
there is a specific relation between every connection.
2. Tree vocabulary:-
Nodes are all the elements in the tree with the topmost known as the root node. The node from
which other nodes descend is the parent node and descendent is the child node. Node with no
child is the leaf node and the line that connects two nodes is the edge.
3. We then discussed Binary Trees where you learnt that a node in a binary tree can have at most 2
1. Maximum number of nodes n at level l of a binary tree is 2l−1 and that in a binary tree
of height h is 2h−1.
b. In-Order Traversal: First the node's left subtree, then the node and then its right
subtree is visited.
c. Post-Order Traversal: The node's left and right subtrees are visited followed by the
node.
The nodes at a certain level are visited before moving on to the next level i.e. you move along
discussed in detail.
Introduction to BSTs
To summarise, the two types of tree data structures we discussed till now are as
follows:
1. Binary Tree:
In this tree, every node can have at most two children.
2. Binary Search Tree:
A Binary Search Tree is also called an ordered binary tree because of the
following properties:
The values of all the nodes in the left subtree are less than the root node.
The values of all the nodes in the right subtree are greater than the root
node.
Each subtree in the binary search tree is itself a binary search tree.
You will also learn how search in a binary search tree has a time complexity
of O(logn).
}
return root;
}
Deleting a Node from BST
package bintree;
protected T value;
protected Node<T> parent;
protected Node<T> left;
protected Node<T> right;
public T getValue() {
return this.value;
}
Introduction to Heaps
In the previous videos, you learnt about the list-based implementation of priority
queues. And here is a chart with the time complexities for each basic operation carried
out by ordered and unordered list implementation of priority queues.
Types of heaps
1. Min heap :-
This heap basically represent that’s the parent node should be always
less than or equal to left and right child
It takes O(1) time to search the minimum
2. Max heap:-
This heap exactly the opposite of the minheap which means that the
parent node should be greater than the or equal to the left and the right
child node.
In this lecture, you saw that the binary heap data structure
could implement the basic operations of a priority queue in O(log n)
time, which is a performance improvement over the list implementation
of the priority queue. For list implementation, the time complexity
was O(n).
You learnt the basic structure of heaps and two of its properties:
They are complete binary trees
Order property: They maintain a hierarchical (level-wise) order among the
nodes of their trees (i.e. min heap or max heap).
Note: The minimum value, which is at the root node, cannot be removed
directly. First, it is swapped with the last node and then it is deleted from the last
node because, as discussed, all the insertion and deletions are done at the last node
only.
Also, you have seen a complete example containing both insertion and deletion
operations on a heap.
Implementation of a
Complete Binary Tree
In the previous videos, you have seen the pictorial representation of heaps, how they
are made, and how they perform their operations.
Now, let’s see how heaps can be represented using arrays.
If the array index is starting from 0, then the child nodes (i) will have an index of i = 2*j
+1 or i= 2*j +2, if the parent node’s index is j. So, j = (i-1)/2, thus this is the correct
option.
Now you have seen how any heap is implemented as an array. In the array, the root
node is the initial element of the array and any child node’s index is 2*i
or 2*i+1 if, the parent’s index is i.
Arr[(i -1) / 2] returns its parent node.
Arr[(2 * i) + 1] returns its left child node.
Arr[(2 * i) + 2] returns its right child node.
This are the implementation of min heap side
getMin(): It returns the root element of Min Heap. The Time
Complexity of this operation is O(1).
extractMin(): Removes the minimum element from MinHeap. The
Time Complexity of this Operation is O(Log n) as this operation needs
to maintain the heap property (by calling heapify()) after removing the
root.
insert(): Inserting a new key takes O(Log n) time. We add a new key
at the end of the tree. If a new key is larger than its parent, then we
don’t need to do anything. Otherwise, we need to traverse up to fix the
violated heap property.
Heap Sort
Heap sort is a comparison-based sorting technique based on Binary Heap data
structure. It is similar to the selection sort where we first find the minimum
element and place the minimum element at the beginning. Repeat the same
process for the remaining elements.
Introduction to Graphs
There are basically two types of things in graphs
Nodes
Edges
Nodes represent objects or entities
Edges represent various kind of relationship between this entities
Vertices means nodes
And edges means arcs
1. Graphs are used in Google maps where different locations are represented as
nodes, and the roads connecting between the locations act as edges between
these nodes.
2. Recommendations on e-commerce websites are generated by the graph theory.
When a person checks out a particular category of product, similar kinds of
products appear as suggestions to the user. Here, the products act as nodes and
the similarity between them forms a relationship and connects them to an edge
that leads to related products (nodes).
These are two among many real-life examples where you can use graphs to depict the
scenarios
Figure 1
So, you have now been introduced to the concepts of graphs and have also looked at
various real-world scenarios where graph data structures are used extensively.
In the forthcoming video, you will learn about the classification of graphs and the
differences between the graph and the tree data structure.
Play Video
4421328
Figure 2
Undirected graphs: Graphs that show a symmetrical relationship between two
connected nodes paired by an edge representing a simple line.
G for the above graph is (V, E), where:
V = {1, 2, 3, 4} and
E = {(1, 2), (2, 4), (4, 1), (1, 3)}.
Directed graphs: Graphs that show an asymmetrical relationship between two
connected nodes paired by an edge that indicates the direction of the relationship from
one node to the other.
Note: The pair representing the edge is ordered, and (u, v) ≠ (v, u) in the case of a
directed graph (digraph). In the case of an undirected graph, the order does not matter.
G for the graph above is (V, E), where:
V = {A, B, C, D, E} and
E = {(A, C), (B, C), (C, D), (E, D), (B, E)}.
Directed acyclic graphs (DAGs): These graphs fall under the sub-category of
directed graphs. The only difference between a directed graph and a DAG (a directed
acyclic graph) is that DAGs do not have cycles; this means if you start from any node
in the graph and traverse through its connecting edges, then you can never return to
the same node where you started.
Trees: These are restricted forms of graphs, and they fit the category of directed
acyclic graphs with the restriction that each child node can have only one parent node
in the structure.
Connected graph: A graph is connected if a path exists from every vertex to every
other vertex.
Disconnected graph: A graph is disconnected if at least two nodes exist such that
there is no path connecting them.
There does not exist any path from the set of vertices {1, 2, 4, 3} to vertex {5}.
Terminology
Neighbours: If two nodes are adjacent to each other and connected by an edge, then
those nodes are called neighbours.
Degree: The number of edges that are connected to a node is called the degree of the
node.
Now, let us consider the undirected graph given below. We will now discuss how to
determine the degree of each node in this graph.
Figure 3
Node 1 {2, 3, 4, 5} 4
Node 2 {1, 4} 2
Node 3 {1} 1
Node 4 {1, 2, 5} 3
Node 5 {1, 4} 2
In the case of directed graphs, the degree can be classified as:
In-degree: The number of incoming edges to a node
Out-degree: The number of outgoing edges from a node
For a directed graph:
Degree = In-degree (Edges pointing to the vertex) + Out-degree (Edges pointing away
from the vertex).
Path: When a series of vertices are connected by a sequence of edges between two
specific nodes in a graph, the sequence is called a path. For example, in the
graph above, {2, 1, 4, 5} indicates the path between nodes 2 and 5, and the
intermediate nodes are 1 and 4.
Weighted graph: A graph in which the edges contain some weights or values is
called a weighted graph.
Example: If the nodes in a graph are considered to be cities, and the edges are
considered to be the paths between the cities, then the weights of these edges can be
considered to be the distance between these cities.
Unweighted graph: A graph in which edges contain no weight is called an
unweighted graph.
So, now that you have a clear understanding of a graph’s properties test your
knowledge by attempting the questions given below.
Depth-First Search (DFS) – I
Here’s an image to explain the traversal of the DFS algorithm step-by-step on
an example graph.
Example
In the video, you saw the pseudocode for the depth-first search of a graph. Now, let’s
take a look at the steps in finding the DFS traversal of the graph given in Step 1 in
the image below by taking node 1 as the starting node of the traversal.
Let us apply the pseudocode discussed in the video.
The steps in the image above are explained below:
Step 1: Run the dfs() method on node ‘1’ and add that node to the visited list
Step 2: The dfs(1) method recursively calls for all the unvisited neighbours of node
‘1’:
1. Here, the unvisited neighbours of node ‘1’ are {2, 3}.
2. Let us assume that dfs(1) recursively calls for node ‘2’ first and adds the node
to the visited list.
Step 3: The dfs(2) method recursively calls for all the unvisited neighbours of node
‘2’:
1. Here, the unvisited neighbours of node ‘2’ are {3, 4}.
2. Let us assume that dfs(2) recursively calls for node ‘3’ first and adds the node
to the visited list.
Step 4: The dfs(3) method recursively calls for all the unvisited neighbours of node
‘3’. Since there are no remaining unvisited neighbours of node ‘3’, it returns.
Step 5: The dfs(2) method recursively calls for all the unvisited neighbours of node
‘2’:
1. Here, the unvisited neighbour of node ‘2’ is {4}. So, dfs(2) recursively calls for
node ‘4’ and adds the node to the visited list.
Step 6: The dfs(4) method recursively calls for all the remaining unvisited neighbours
of node ‘4’. Since there are no remaining unvisited neighbours of node ‘4’, it returns.
Step 7: The dfs(2) method recursively calls for all the remaining unvisited neighbours
of node ‘2’. Since there are no remaining unvisited neighbours of node ‘2’, it returns.
Step 8: The dfs(1) method recursively calls for all the remaining unvisited neighbours
of node ‘1’. Since there are no remaining unvisited neighbours of node ‘1’, it returns.
The visited list is the DFS of the graph.
Depth-First Search (DFS) – II
In the previous segment, you learnt about the depth-first traversal of a graph and
the pseudocode for the same in detail.
Now, since you have already seen the DFS of a graph using recursion, and we know
that there is a relation between recursion and stack, let’s see the DFS of a graph using
stacks with an example.
Let’s take a look at the steps in finding the DFS traversal of the graph given in Step 1
in the image below by taking node 1 as the starting node of the traversal.
The steps in the image above are explained below:
Step 1: Push the starting node, which is node 1 here, to stack.
Step 2: Pop the stack; the popped element here is ‘1’:
1. If the popped element is not on the visited list, then add it to the visited list:
1. So, ‘1’ is added to the visited list.
2. Now, push all the neighbours of the popped element that are not on the visited
list:
1. Therefore, 2 and 3 are pushed to the stack.
Step 3: Pop the stack; the popped element here is ‘3’:
1. If the popped element is not on the visited list, then add it to the visited list:
1. So, ‘3’ is added to the visited list.
2. Now, push all the neighbours of the popped element that are not on the visited
list:
1. Therefore, 2 is pushed to the stack.
Step 4: Pop the stack; the popped element here is ‘2’:
1. If the popped element is not on the visited list, then add it to the visited list:
1. So, ‘2’ is added to the visited list.
2. Now, push all the neighbours of the popped element that are not on the visited
list:
1. Therefore, 4 is pushed to the stack.
Step 5: Pop the stack; the popped element here is ‘4’:
1. If the popped element is not on the visited list, then add it to the visited list:
1. So, ‘4’ is added to the visited list.
2. Now, push all the neighbours of the popped element that are not on the visited
list:
1. Since there are no neighbours of the popped element that are not on the
visited list, do nothing.
Step 6: Pop the stack; the popped element here is ‘2’:
1. If the popped element is not on the visited list, then add it to the visited list:
1. Since ‘2’ is already on the visited list, do nothing.
2. Now, push all the neighbours of the popped element that are not on the visited
list:
1. Since there are no neighbours of the popped element that are not on the
visited list, do nothing.
Since the stack is empty, the visited list is the DFS of the graph in Step 1.
Now, test your understanding of the DFS algorithm by attempting the questions given
below.
Breadth-First Search (BFS) – I
In the two previous segments, you learnt about the depth-first search (DFS) algorithm
for graphs.
Now, we will begin this segment with the forthcoming video, which will introduce
you to another graph traversal technique called breadth-first search (BFS).
Breadth-first search is a traversing algorithm, where traversing begins from the start
node and then explores the immediate neighbours of the start node. Then the
traversing moves towards the next-level neighbours of the graph structure. As the
name suggests, traversal across the graph happens breadthwise.
To implement a BFS, you need to consider the stage of each node. Nodes, in general,
are considered to be in the following three different stages:
Not visited
Visited
Completed
In the BFS algorithm, nodes are marked as visited during traversal to avoid the
infinite loops caused due to the possibilities of cycles in a graph structure.
Now, you learnt about the introduction of the BFS algorithm. In the next segment, you
will learn about the BFS algorithm in more detail.
Breadth-First Search (BFS) – II
Here is an image to explain the traversal of the BFS algorithm step by step on an
example graph.
Q ← new Queue
Visited ← { }
enqueue (Q, n)
enqueue(Q, n`)
end if
end for
end while
end procedure
Step 1: The start node is enqueued and also marked as visited in the following set of
instructions:
enqueue (Q, n)
Add n to visited set.
Step 2: The ‘while’ loop instruction set is executed when the queue is not empty.
Step 3: For each iteration of the ‘while’ loop, a node gets dequeued.
Step 4: Now, the ‘for’ loop runs until all the unvisited neighbours of the dequeued
node (n) are enqueued and marked as visited.
Step 5: For the first iteration of the ‘while’ loop, all the neighbour nodes of the start
node are enqueued. And for the second iteration, all the next-level unvisited neighbour
nodes of one of the neighbour nodes are enqueued.
In this way, all the neighbour nodes are enqueued and visited level-wise from the start
node. And after a certain number of iterations, all the nodes are dequeued, and the
algorithm ends.
Summary
In this session, we covered the following topics:
What is a graph abstract data type?
Different types of graphs:
Undirected graphs
Directed graphs:
Directed acyclic graphs
Differences between graphs and trees
Depth-first search:
Pseudocode
Breadth-first search:
Pseudocode
Summary
In this session, we covered the following topics:
What is a graph abstract data type?
Different types of graphs:
Undirected graphs
Directed graphs:
Directed acyclic graphs
Differences between graphs and trees
Depth-first search:
Pseudocode
Breadth-first search:
Pseudocode
Introduction to Edge Lists
So, in the video, you learnt what an edge list is. You also learnt that an edge list fails
in a scenario wherein you have an isolated node in your graph. To overcome this
problem, you will be introduced to a second graph implementation, an ‘adjacency
matrix’, in the next segment.
But before moving on to the segment, attempt the question below to assess your
understanding of the concepts covered so far.
Performance Characteristics
of an Adjacency Matrix
The most important aspect of any algorithm is its performance characteristics, which
determine how it would perform in a given situation or under given circumstances.
Therefore, you need to take into account an algorithm’s performance when you make
your choice.
So, in the forthcoming video, we will calculate the time complexity of certain
operations on an adjacency matrix. Precisely, we will calculate the time complexity of
the following four operations:
getAllNodes: Get all the nodes of the graph
addNode: Add a node to the graph
addEdge: Add an edge between two specified nodes
getAllNeighbours: Get all the neighbours of a specified node
So, in the video, you learnt:
To calculate the time complexity of certain important operations on an
adjacency matrix
The practices that you can follow to improve the time complexities of the
following methods: addEdge and getAllNeighbours.
What are Dense Graphs and Sparse Graphs?
Let us now discuss ‘dense graphs’ and ‘sparse graphs’ in detail.
Dense graphs: A dense graph is a graph in which the number of edges is close to the
maximum possible edges for a given set of nodes. Given below is an example of a
dense graph.
Figure 1
Sparse graphs: Sparse graphs are connected graphs with a minimum or a small
number of edges connecting the nodes. In sparse graphs, there may or may not be an
edge between two nodes. Here, usually, the number of edges is n, which is also the
number of vertices.
Figure 2
Now, let’s see what the time complexity of removing an edge from an adjacency
matrix is if all the vertices are not integral values.
The adjacency matrix and vertex list of the above weighted directed graph are given
below.
If the size of the vertex list is V, then this step takes linear time, which is O(V) in