3. DATA STRUCTURE
• a way to organize and store data so that it can be accessed and
worked with efficiently
Types of Data
Structure
Primitive
Integer Float Character Boolean
Non-
primitive
Array Linked List Queue Trees
4. ARRAY
Definition
A collection of elements
identified by index or
key.
Fixed in size and stores
elements of the same
type.
Features
Random access via
indices.
Fixed size (determined at
the time of creation).
Can store primitive
data types or objects.
Operation
Access: O(1) time
complexity.
Insert/Remove: O(n) for
worst-case scenarios
(inserting/removing in
the middle or
beginning).
Search: O(n) for
unsorted arrays; O(log
n) for sorted arrays
using binary search
6. MORE ON ARRAY
PROS
• Fast access time using indices.
• Simple and efficient when the
number of elements is fixed.
CONS
• Fixed size (cannot resize
dynamically).
• Insertion and deletion can be slow if
not at the end.
7. LINKED LISTS
Definition
a linear data structure
where elements (nodes) are
stored in objects, and each
node points to the next
node in the list
types:
Singly Linked List: Each
node points to the next.
Doubly Linked List: Each
node points to both the
next and previous nodes.
Circular Linked List: The
last node points to the
first node.
Features
Dynamic size (can grow or
shrink).
Efficient insertions and
deletions.
Operation
Insert at beginning: O(1)
Insert at end: O(n) for singly
linked lists (O(1) for doubly
linked lists with tail pointer).
Delete: O(1) if node reference
is known, otherwise O(n) to
find the node.
Search: O(n)
8. EXAMPLE OF SINGLY LINKED LIST IN
JAVA
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
next = null;
}
}
class LinkedList {
Node head;
// Add element at the front
public void push(int new_data) {
Node new_node = new Node(new_data);
new_node.next = head;
head = new_node;
}
// Print the linked list
public void printList() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data + "
");
temp = temp.next;
}
}
}
public class Main {
public static void main(String[] args)
{
LinkedList llist = new
LinkedList();
llist.push(10);
llist.push(20);
llist.push(30);
llist.printList(); // Output: 30 20
10
}
9. MORE ON LINKED LIST
PROS
• Dynamic size (can grow or shrink as
needed).
• Efficient insertions and deletions
from the beginning (or end if doubly
linked).
CONS
• Access time is slower (sequential
search).
• Extra space required for pointers.
10. STACKS
Definition
linear data
structure that
follows the Last In,
First Out (LIFO)
principle.
the last element
inserted is the first
one to be removed
Feature
Only the top
element is
accessible.
Used for problems
like recursion,
undo operations,
and parsing
expressions.
Operation
Push: Add an element
to the top of the stack
(O(1)).
Pop: Remove the top
element (O(1)).
Peek: View the top
element without
removing it (O(1)).
isEmpty: Check if the
stack is empty (O(1)).
11. EXAMPLE OF STACK IN JAVA
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println(stack.peek()); // Output: 30
System.out.println(stack.pop()); // Output: 30
System.out.println(stack.peek()); // Output: 20
}
}
12. MORE ON STACKS
PROS
• Efficient for problems requiring
reverse order (e.g., undo
operations).
• Fast access to the top element.
CONS
• Limited in functionality to LIFO
operations.
• Only allows access to the top
element (no direct access to other
elements).
13. QUEUE
Definition
a collection that
follows the First In,
First Out (FIFO)
principle
the first element
inserted is the first
one to be removed
Types
Simple Queue: Basic
FIFO behavior.
Circular Queue: The end
of the queue connects
back to the beginning.
Priority Queue:
Elements are
dequeued based on
priority rather than the
order they were added.
Operation
Enqueue: Add an
element to the queue
(O(1)).
Dequeue: Remove the
front element (O(1)).
Peek: View the front
element without
removing it (O(1)).
isEmpty: Check if the
queue is empty (O(1)).
14. EXAMPLE OF SIMPLE QUEUE IN JAVA
import java.util.LinkedList;
import java.util.Queue;
public class QueueExample {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
queue.offer(10);
queue.offer(20);
queue.offer(30);
System.out.println(queue.poll()); // Output: 10
System.out.println(queue.peek()); // Output:
20
}
}
15. MORE ON QUEUES
PROS
• Ideal for scheduling problems and
handling requests in order.
• Efficient enqueue and dequeue
operations.
CONS
• Limited to FIFO operations.
• Not efficient for random access or
deletions from arbitrary positions.
16. HASH MAP
Definition
• collection of key-value
pairs
• implemented using a
hash table.
Feature
• allows fast retrieval of
elements using a key
Operation
Put: Add or update a key-
value pair (O(1) on
average).
Get: Retrieve the value for
a given key (O(1) on
average).
Remove: Remove the key-
value pair (O(1) on
average).
• ContainsKey: Check if a
key exists (O(1) on
average).
17. EXAMPLE OF HASH MAP IN JAVA
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
System.out.println(map.get("apple")); // Output: 1
System.out.println(map.containsKey("banana")); //
Output: true
}
}
18. MORE ON HASH MAPS
PROS
• Efficient for key-value mapping.
• Provides constant time access for
lookups, insertions, and deletions.
CONS
• Hash collisions can degrade
performance (though this is rare with
good hashing).
• Not ordered (if you need ordered
traversal, consider TreeMap).
19. TREES
Definition
• A hierarchical structure
made up of nodes
connected by edges.
Types
Binary Tree (Each node
has at most two children)
Binary Search Tree (BST)
(Left child < parent < right
child)
AVL Tree (Self-balancing
BST)
• Heap (Complete binary
tree, used for priority
queues)
Operation
Insert: Add a node (O(log
n) in a balanced tree).
Search: Find a node (O(log
n) in a balanced tree).
• Delete: Remove a node
(O(log n) in a balanced
tree).
21. MORE ON TREES
PROS
• Fast searching and insertion (O(log
n) for balanced trees).
• Hierarchical structure that can
represent hierarchical data.
CONS
• Can become unbalanced (leading to
O(n) complexity for skewed trees).
Use self-balancing trees (like AVL or
Red-Black Trees) to mitigate this.
22. ASSIGNMENT
To fully understand graphs as a data structure, you need to cover
the following topics systematically:
• Definition of a Graph
• Types of Graphs
• Pros and Cons of Graphs
• Write a Java implementation for basic graph operations such as
graph creation, graph traversal (DFS, BFS), and graph representation.
23. GRAPHS
Definition
• a non-linear data
structure that consists of
a collection of nodes
(vertices) and a
collection of edges
connecting pairs of
nodes
• used to represent
various real-world
problems like social
networks,
transportation
networks, and web
pages
Types
Directed Graph (Digraph):
edges have a direction,
i.e., they go from one
vertex to another.
• Undirected Graph:
edges have no direction;
they simply connect two
vertices.
Operation
Add Vertex
Add Edge
Remove Vertex
Remove Edge
BFS (Breadth-First Search
DFS (Depth-First Search
Shortest Path
Cycle Detection
• Graph Traversal
Editor's Notes
#1:Data Structures and Algorithms (DSA) are fundamental concepts in computer science that help you efficiently store, organize, and manipulate data. Mastering DSA is essential for solving complex programming problems, especially in languages like Java.
#3:What Are Data Structures?
Data structures are specialized formats for organizing and storing data in a way that enables efficient access, modification, and management. In other words, a data structure is a way to organize and store data so that it can be accessed and worked with efficiently.
The choice of a data structure plays a crucial role in how effectively an algorithm performs. It determines the efficiency of operations such as searching, insertion, deletion, and sorting.
Types of Data Structures
Data structures can be broadly categorized into two types:
Primitive Data Structures:
These are the most basic data types provided by most programming languages. They represent the simplest form of data.
Examples: Integers, floats, characters, and booleans.
Non-Primitive Data Structures:
These are more complex and are built using primitive data structures. They are used to store collections of data and are designed to organize data efficiently for specific applications.
Examples: Arrays, linked lists, stacks, queues, trees, graphs, hash tables, etc.
#4:O(1): Constant time - means that the time it takes to perform an operation does not depend on the size of the input. No matter how large or small the data set is, the time to complete the operation remains constant.
O(n): Linear time - means that the time required for an operation grows linearly with respect to the size of the input data. In other words, if the input size doubles, the time required to complete the operation also doubles.
O(log n): Logarithmic time (e.g., binary search) - means that as the size of the input data increases, the time to perform an operation increases at a much slower rate compared to linear time complexity (O(n)).In simple terms, an O(log n) operation reduces the problem size exponentially with each step. Instead of processing every element one-by-one (like in O(n)), it typically "halves" the problem space at each step.
O(n^2): Quadratic time (e.g., bubble sort)
#5:represents the creation and initialization of an array in Java.
1. int[] — Declaring the Type of the Array
int[] indicates that the array is of type int, meaning the array will store integer values.
The square brackets [] indicate that this is an array, not a single int. This tells the compiler that arr will hold multiple integers.
So, int[] means you're declaring an array of integers.
2. arr — The Name of the Array
arr is the name of the array variable. You can name your array anything you want, but in this case, it’s named arr.
3. {1, 2, 3, 4} — Initializing the Array
The curly braces {} contain the initial values you want the array to hold. In this case, the array arr is initialized with the values 1, 2, 3, and 4.
This is called array initialization. When you initialize an array with values inside the curly braces, the Java compiler will automatically create an array of the correct size (in this case, size 4), and place the values into the array.
#7:O(1): Constant time - means that the time it takes to perform an operation does not depend on the size of the input. No matter how large or small the data set is, the time to complete the operation remains constant.
O(n): Linear time - means that the time required for an operation grows linearly with respect to the size of the input data. In other words, if the input size doubles, the time required to complete the operation also doubles.
Singly Linked Lists: Inserting at the beginning is O(1), but inserting at the end requires traversing the list, resulting in O(n). Deletion and search operations are either O(1) or O(n) depending on whether the node reference is known.
Doubly Linked Lists: With a tail pointer, insertion at the end becomes O(1). Deletion is efficient when we know the node to delete, and searching still requires traversing the list in O(n) time.
#8:Class Node
This class represents a node in the linked list. Each node stores some data and a reference (or link) to the next node in the list.
int data: This is where the actual data for the node is stored (in this case, an integer).
Node next: This is a reference (pointer) to the next node in the linked list. Initially, it is set to null, meaning the node doesn't point to anything.
Constructor: When a new Node object is created, it is initialized with a data value, and the next pointer is set to null.
Class LinkedList
This class represents the singly linked list itself. It has a reference to the head node and provides methods for adding nodes and printing the list.
Node head: The head of the list, which points to the first node.
push(int new_data): This method adds a new node at the front of the list.
A new node is created with the given data.
The next pointer of the new node is set to the current head (so the new node points to the old first node).
The head pointer is updated to point to the new node.
This operation always happens in O(1) time because it involves just a few pointer updates, regardless of the list's size.
printList(): This method prints the data of each node in the linked list, starting from the head.
It uses a temporary node (temp) to traverse the list from the head to the end (where temp.next == null).
As it moves through the list, it prints each node's data.
Class Main
This is the entry point for the program, where the linked list is created, and the methods are called.
LinkedList llist = new LinkedList();: Creates a new instance of the LinkedList class. Initially, the linked list is empty, so head is null.
llist.push(10);: Adds a node with the value 10 at the front of the list. Now the list is: 10.
llist.push(20);: Adds a node with the value 20 at the front of the list. Now the list is: 20 -> 10.
llist.push(30);: Adds a node with the value 30 at the front of the list. Now the list is: 30 -> 20 -> 10.
llist.printList();: Prints the entire list. It starts from the head, which is 30, and prints each node’s data in the order they are linked. The output will be: 30 20 10.
Explanation of the List Operations:
push(): The push() method adds a new node at the front of the list, effectively making the list a stack-like structure (Last In, First Out or LIFO). Each time a new node is pushed, it becomes the new head of the list.
printList(): This method prints the data of each node, starting from the head and moving through the list using the next pointer. The traversal stops when it reaches the last node (when temp.next == null).
#10:O(1): Constant time - means that the time it takes to perform an operation does not depend on the size of the input. No matter how large or small the data set is, the time to complete the operation remains constant.
O(n): Linear time - means that the time required for an operation grows linearly with respect to the size of the input data. In other words, if the input size doubles, the time required to complete the operation also doubles.
All these operations involve a fixed number of steps:
For push and pop, you're simply updating or reading from the top of the stack (adding/removing elements).
For peek, you're just reading the value at the top.
For isEmpty, you're checking the top index or pointer.
None of these operations require iterating through the stack or performing complex calculations, so the time complexity for all of them is O(1), which means the time taken is constant and does not depend on the size of the stack.
#11:Importing Stack Class
Here, we import the Stack class from the java.util package. This class provides a collection that implements a LIFO (Last In, First Out) stack, meaning the last element pushed onto the stack will be the first to be popped off.
Creating the Stack Stack<Integer> stack = new Stack<>();
This line creates a new stack called stack that will store Integer values. Stack<Integer> specifies the type of elements that the stack will hold, which in this case is Integer.
Pushing Elements onto the Stack
push() adds elements to the top of the stack.
First, 10 is added to the stack, making the stack: [10].
Then, 20 is added, making the stack: [10, 20].
Finally, 30 is added, making the stack: [10, 20, 30].
After these operations, the top of the stack is 30.
Peeking the Top Element
The peek() method returns the top element of the stack without removing it. At this point, the top element is 30, so this line will output 30.
Popping the Top Element
The pop() method removes and returns the top element of the stack.
Since 30 is at the top, it will be removed and returned.
The stack now looks like this: [10, 20].
The output of this line will be 30
Peeking the New Top Element
After 30 was popped, the new top element is 20.
The peek() method will now return 20, and the output will be 20
After all operations, the stack is [10, 20].
#13:O(1): Constant time - means that the time it takes to perform an operation does not depend on the size of the input. No matter how large or small the data set is, the time to complete the operation remains constant.
Why Are These Operations O(1)?
Enqueue:
Adding an element to the rear of the queue takes constant time because you simply add it to the end of the structure. No other elements are involved in the process.
Dequeue:
Removing the front element takes constant time when you simply update the front pointer (in the case of a linked list). Even in an array-based queue, with a circular or deque approach, you can remove the front in constant time.
Peek:
Accessing the front element is a simple operation that doesn’t require iterating through the queue or making any modifications, so it takes constant time.
isEmpty:
Checking whether the queue is empty involves a simple comparison to a null value (or size check), which is a constant time operation.
These operations are all designed to be very efficient and do not depend on the size of the queue, hence the time complexity is O(1) for each of them.
Queue operations like enqueue, dequeue, peek, and isEmpty are all O(1) because they involve accessing or modifying only the front or rear of the queue, with no iteration or complex operations involved. This ensures that these operations are performed in constant time, regardless of the queue's size.
#14:Explanation of Key Concepts
Queue Interface in Java:
In Java, Queue is an interface that is part of the java.util package. A queue follows the FIFO (First In, First Out) principle, meaning the element added first will be the first to be removed.
The Queue interface defines the basic operations, like adding, removing, and accessing elements from the queue.
LinkedList Implementation:
LinkedList is a class that implements the Queue interface. It is commonly used for implementing queues because of its efficient O(1) time complexity for adding and removing elements at both ends (front and rear).
A LinkedList provides efficient access to both ends of the list, making it a good choice for queue operations.
Queue<Integer> queue = new LinkedList<>();
This line creates a queue of type Integer using LinkedList.Queue<Integer> is the interface, and LinkedList<> is the concrete class implementing the queue operations.
Offering Elements to the Queue:
offer() is a method defined in the Queue interface, used to add an element to the queue. It is similar to the add() method but is preferred in a queue because it doesn't throw an exception if the element cannot be added (e.g., if the queue is full).The elements 10, 20, and 30 are added to the queue. The front of the queue is 10, and the rear is 30.
Polling (Removing the Front Element):
poll() is a method that removes and returns the front element of the queue. It removes the first element (in this case, 10).After the operation, the queue will now look like [20, 30]. The output will be 10.
Peeking (Viewing the Front Element):
peek() returns the front element of the queue without removing it. It allows you to see which element is currently at the front of the queue.At this point, the front element is 20, so the output will be 20.
#16:O(1): Constant time - means that the time it takes to perform an operation does not depend on the size of the input. No matter how large or small the data set is, the time to complete the operation remains constant.
Why are these operations O(1) on average?
These operations are O(1) on average because of the hashing mechanism used by the HashMap:
Hashing:
Each key is hashed, and the hash code is used to quickly find the corresponding index in the underlying array or table.
Average Case:
In the average case, there are few collisions, and the hash table is efficiently organized. This allows the operations to be performed in constant time.
Load Factor and Resizing:
HashMap dynamically resizes its underlying array when the load factor (ratio of size to capacity) becomes too high. The resizing operation might be expensive, but it happens infrequently, so it does not affect the average time complexity for individual operations.
Worst Case:
The worst-case time complexity for these operations could be O(n) (where n is the number of entries) if there are many hash collisions and the map degenerates into a linked list. However, with a good hash function and proper resizing, the worst-case scenario is rare.
#17:Importing the HashMap: import java.util.HashMap;
This line imports the HashMap class from the java.util package. The HashMap is part of the Collections Framework and is used to store data in key-value pairs.
Creating the HashMap: HashMap<String, Integer> map = new HashMap<>();
Here, we create an instance of the HashMap with String as the key type and Integer as the value type. This means each entry in the map will have a String as the key and an Integer as the value.
Initially, the map is empty: {}.
Adding Key-Value Pairs using put() Method:
The put() method is used to add key-value pairs to the map. In each put() call:
First call: Adds the pair ("apple", 1).
Second call: Adds the pair ("banana", 2).
Third call: Adds the pair ("cherry", 3).
After these operations, the map contains:
{"apple": 1, "banana": 2, "cherry": 3}
Retrieving the Value Using get() Method:
The get() method retrieves the value associated with the specified key.
Here, we are asking for the value corresponding to the key "apple". Since we previously added the pair ("apple", 1), the value 1 is returned.
Output: 1
Checking if the Key Exists Using containsKey() Method:
The containsKey() method checks whether the map contains the specified key.
Here, we are checking if the map contains the key "banana". Since the map contains the pair ("banana", 2), the method returns true.
Output: true
#19:O(1): Constant time - means that the time it takes to perform an operation does not depend on the size of the input. No matter how large or small the data set is, the time to complete the operation remains constant.
Binary Tree
Definition: A tree where each node has at most two children (left and right).
Binary Search Tree (BST)
Definition: A binary tree where the left child is smaller than the parent node, and the right child is greater than the parent.
AVL Tree:
An AVL Tree (Adelson-Velsky and Landis Tree) is a type of self-balancing binary search tree. The key feature of an AVL tree is that it maintains the balance of the tree by ensuring that, for every node, the heights of the left and right subtrees differ by at most one. If the balance factor (difference in heights of left and right subtrees) of any node becomes greater than 1 or less than -1, the tree is rebalanced through rotations.
Heap:
A Heap is a special tree-based data structure that satisfies the heap property. There are two types of heaps:
Max-Heap: In a max-heap, for every node, the value of the node is greater than or equal to the values of its children.
Min-Heap: In a min-heap, for every node, the value of the node is less than or equal to the values of its children.
Why O(log n) in a Balanced Tree?
The height of a balanced tree is logarithmic in terms of the number of nodes, so any operation (insert, search, delete) that requires traversing the tree is limited by the height.
For n nodes, a balanced tree will have a height of approximately O(log n), and thus any operation that involves traversing the height of the tree will have a time complexity of O(log n).
In Unbalanced Trees:
In an unbalanced tree (e.g., a regular binary search tree), the height could grow linearly with the number of nodes, resulting in a worst-case time complexity of O(n) for insertion, search, or deletion. This happens when the tree becomes skewed (like a linked list), which is why balanced trees (like AVL or Red-Black trees) are preferred for ensuring efficient operations.
#20:Class Overview
Node Class: This is a helper class that defines the structure of a node in the binary search tree. Each node contains:
data: The value stored in the node.
left: A reference to the left child node.
right: A reference to the right child node.
BinarySearchTree Class: This is the main class that defines a Binary Search Tree with the following methods:
insert(int data): To insert a value into the tree.
inorder(): To perform an inorder traversal of the tree and print the elements in sorted order.
search(int data): To search for a value in the tree.
Node Class:
The Node class represents a single element in the tree.
It has three fields:
data: stores the value of the node.
left: points to the left child node.
right: points to the right child node.
The constructor initializes the node with a value (data) and sets both the left and right children to null.
BinarySearchTree Class:
The root is the entry point of the tree. Initially, it is set to null, meaning the tree is empty.
The constructor initializes the tree with the root set to null.
public BinarySearchTree() {
root = null;
}
The insert method inserts a value into the BST. It calls a recursive helper function insertRec, which performs the actual insertion into the tree.root = insertRec(root, data) is used to ensure that the correct root is returned after the recursive insertion.
Recursive Insert Function:
This is a recursive function that traverses the tree to find the correct spot for the new node.Base Case: If the current root is null, it means we’ve found the correct spot for the new node, so we create a new node and return it.Recursive Case:If the data to insert is less than the current node's data, it will be placed in the left subtree.
If the data is greater, it will be placed in the right subtree.
The function finally returns the updated node after insertion.
Inorder Traversal Method:
The inorder method is used to print the elements of the tree in sorted order.
It calls the helper method inorderRec, passing the root node to start the traversal.
Recursive Inorder Function:
This function performs inorder traversal of the tree. The order is:
Traverse the left subtree.
Visit the root node.
Traverse the right subtree.
This results in the nodes being printed in ascending order for a BST.
Search Method:
The search method is used to check whether a value exists in the BST.
It calls the helper function searchRec, passing the root node and the data to search for.
Recursive Search Function:
This is a recursive function that searches for a node with the given data.
If the current node is null, the value isn’t found, so it returns false.
If the current node’s data matches the value we're looking for, it returns true.
If the value is smaller than the current node’s data, it searches the left subtree, otherwise, it searches the right subtree.
Main Method (Test the BST):
The main method creates a new instance of BinarySearchTree and inserts values into the tree.
It then performs an inorder traversal to print the tree in sorted order.
Finally, it searches for two values (40 and 25) and prints whether they are found in the tree.
Sample Output:
Inorder traversal:
20 30 40 50 60 70 80
Search 40: true
Search 25: false
Summary of Operations:
Insertion: Inserting a new value is done by comparing the value with the root and recursively placing it in the correct position (left or right).
Inorder Traversal: This is used to print the nodes in ascending order (left subtree, root, right subtree).
Search: Searching for a value is done recursively by comparing the value with the node's data and navigating to the left or right child accordingly.
#23:Add Vertex: Adds a new vertex to the graph. Add Edge: Adds an edge between two vertices. Remove Vertex: Removes a vertex and its associated edges. Remove Edge: Removes an edge between two vertices. BFS (Breadth-First Search): Traverses the graph level by level. DFS (Depth-First Search): Traverses the graph deeply, visiting one vertex before backtracking. Shortest Path: Finds the shortest path between two vertices (e.g., Dijkstra’s algorithm). Cycle Detection: Detects whether a cycle exists in the graph. Graph Traversal: Traverses the graph to visit all nodes.