Dsa Practical Notes
Dsa Practical Notes
arrays that can have more than one dimension. While a one-dimensional array is a simple list of
elements, an N-dimensional array can be visualized as data arranged in N dimensions,
providing a way to represent more complex structures.
● 1-D Array: A linear array with elements arranged in a single row (like a list).
● 2-D Array: Represents data in rows and columns, like a matrix or table (e.g., int
arr[3][4] for a 3x4 array).
● 3-D Array and Beyond: Extends further, such as a 3-D array representing a cube-like
structure (int arr[3][4][5]), and so on for higher dimensions.
2. Memory Representation
3. Accessing Elements
4. Applications
● Advantages:
○ Useful for organizing complex data structures.
○ Allows modeling of real-world problems like multi-dimensional datasets.
● Disadvantages:
○ Higher memory usage for large dimensions.
○ Complex indexing can lead to errors.
6. Operations on N-dimensional Arrays
cpp
Copy code
#include <iostream>
using namespace std;
int main() {
int arr[3][4][2] = {{{1, 2}, {3, 4}, {5, 6}, {7, 8}},
{{9, 10}, {11, 12}, {13, 14}, {15, 16}},
{{17, 18}, {19, 20}, {21, 22}, {23, 24}}};
// Accessing an element
cout << "Element at arr[2][3][1]: " << arr[2][3][1] << endl;
return 0;
}
This code initializes a 3D array and accesses the element at index [2][3][1].
8. Use Cases
1. Types of Expressions
● Infix Expression: Operators are placed between operands (e.g., A + B). This is the
usual way we write expressions.
● Prefix Expression (Polish Notation): Operators precede operands (e.g., +AB).
● Postfix Expression (Reverse Polish Notation): Operators follow operands (e.g., AB+).
● Use a stack to evaluate postfix or prefix expressions. Operands are pushed onto the
stack, and operators pop operands off to apply the operation.
● Real-life example: Expression evaluation in programming languages where the
compiler or interpreter evaluates mathematical expressions based on operator
precedence.
Recursion in DSA
Recursion is a technique where a function calls itself to solve smaller instances of a problem. It
continues to break down the problem into smaller sub-problems until reaching a base case.
1. Basic Concepts
● Base case: The condition at which the recursive function stops calling itself.
● Recursive case: The part where the function calls itself with a modified argument.
● Matryoshka Dolls (Nested Dolls): Each doll contains a smaller doll inside, down to the
smallest doll. This is similar to breaking a problem down recursively.
● Directory Structure: Folders containing subfolders, which may contain more subfolders.
● Fibonacci Sequence: Calculating Fibonacci numbers where F(n) = F(n-1) +
F(n-2) involves solving smaller Fibonacci problems.
Backtracking in DSA
1. How It Works
● Maze Solving: Navigating through a maze involves trying different paths and
backtracking if a path is blocked.
● Sudoku Solver: Filling a Sudoku grid involves trying numbers in empty cells and
backtracking if a number violates Sudoku rules.
● Word Search Puzzles: Searching for words by exploring all possible paths and
backtracking when reaching dead-ends.
if solve_sudoku(board):
return True
board[row][col] = 0 # Backtrack
return False
This code tries to fill each empty cell in a Sudoku grid and backtracks if a number placement
doesn't satisfy the conditions.
1. Expression Evaluation:
○ Compilers and Interpreters: Used in converting and evaluating expressions in
programming languages.
○ Spreadsheet Calculations: Evaluating formulas in Excel sheets.
2. Recursion:
○ Game Development: AI algorithms, such as the minimax algorithm for
decision-making in games.
○ Mathematical Computations: Calculating factorials, Fibonacci numbers, or
solving complex mathematical problems.
3. Backtracking:
○ Constraint Satisfaction Problems: Problems like the N-Queens puzzle,
crosswords, and other puzzles.
○ Combinatorial Optimization: Generating permutations, combinations, or
subsets.
These concepts form the backbone of problem-solving in DSA, with each having practical
applications in different domains.
Queue in DSA
A Queue is a linear data structure that follows the FIFO (First In, First Out) principle. Elements
are added at the rear (enqueue) and removed from the front (dequeue). It's commonly used in
scenarios where the order of processing needs to be maintained.
A Double-Ended Queue (Deque) is a special type of queue where elements can be added or
removed from both the front and rear. It combines the functionalities of both stacks and queues.
1. Operations on a Deque
● InsertFront(value): Adds an element at the front.
● InsertRear(value): Adds an element at the rear.
● DeleteFront(): Removes an element from the front.
● DeleteRear(): Removes an element from the rear.
● Web Browsers: The forward and backward navigation of web pages can be
implemented using a deque, where new pages are added at the rear, and the front
pages can be accessed when navigating back.
● Text Editors (Undo/Redo operations): The history of actions can be stored in a deque,
allowing users to move back and forth between states.
● Car Parking: In a narrow parking lot where cars can exit from both ends.
int main() {
std::deque<int> dq;
// Inserting elements
dq.push_back(10); // Insert at rear
dq.push_front(20); // Insert at front
// Deleting elements
dq.pop_back(); // Remove from rear
dq.pop_front(); // Remove from front
return 0;
}
Self-Referencing Classes
Self-referencing classes are those that contain a reference (or pointer) to another instance of
the same class. These are crucial for creating linked data structures such as linked lists, trees,
and graphs.
● Each node in a linked list contains data and a reference to the next node.
●
● Here, Node* next is a pointer to the next node in the list.
● Family Tree Representation: Each person (node) may have pointers to parents,
children, and siblings.
● Train Carriages: Each carriage can be connected to the next, forming a linked chain of
carriages.
Dynamic memory allocation allows programs to request memory during runtime, which is
useful when the size of data structures cannot be determined at compile time. Memory is
allocated on the heap and can be freed manually.
// Assign values
for (int i = 0; i < 5; ++i) {
arr[i] = i + 1;
}
When combining these DSA concepts, we can create powerful data structures:
1. Deque with Dynamic Memory: Implementing a deque using dynamic arrays allows
resizing as elements are added.
2. Self-Referencing Classes with Deque: A doubly linked list can be used to implement a
deque where each node has pointers to the previous and next nodes.
3. Dynamic Allocation in Self-Referencing Structures: Linked lists allocate memory
dynamically for each node, making them ideal for handling unknown data sizes.
Summary
● Deque provides flexibility for adding and removing elements at both ends, with real-life
uses in browsers, undo operations, and car parking.
● Self-referencing classes are foundational for linked structures like linked lists and
family trees.
● Dynamic memory allocation offers memory flexibility and efficiency, crucial for dynamic
data structures like vectors and linked lists.
These concepts help efficiently manage data and solve complex problems in programming and
software development.
A Linked List is a linear data structure where elements (known as nodes) are stored in
sequence. Each node contains two main components:
Unlike arrays, linked lists do not store elements in a contiguous block of memory, allowing for
dynamic memory allocation and flexible sizing.
A Singly Linked List is a type of linked list where each node points to the next node in the
sequence, forming a one-way chain. The first node is called the head, and the last node points
to null, indicating the end of the list.
Operations:
● Insertion: Adding a new node at the beginning, end, or any specific position.
● Deletion: Removing a node from the list.
● Traversal: Visiting each node to perform some operation, like printing the values.
Real-Life Examples:
class SinglyLinkedList:
def __init__(self):
self.head = None
In a Circular Linked List, the last node points back to the first node, forming a circular
structure. There is no null reference in this type of list.
Operations:
● Similar to singly linked lists, but with the added benefit that we can loop back to the start
of the list.
● Can start traversal from any node and still cover all elements.
Real-Life Examples:
● Round-robin Scheduling: Tasks are assigned in a circular fashion, like players taking
turns in a game.
● Traffic Lights: Lights change in a circular sequence from red to green to yellow, and
then back to red.
Simple Example:
● Similar to the singly linked list, but in the append method, the next pointer of the last
node is set to the head.
Real-Life Example:
● Undo Feature in Text Editors: Recent actions are undone first, just like how the last
item added to a stack is removed first.
Linked Queue
Real-Life Example:
Double-Ended List
● Allows insertion and deletion from both the front and rear.
● Useful for implementing a Deque (Double-Ended Queue).
A Doubly Linked List is a type of linked list where each node contains two pointers:
Operations:
Real-Life Examples:
● Browser History Navigation: Allows moving forward and backward between pages.
● Music Playlist (Bidirectional Navigation): Skip to the next or previous song.
class DoublyLinkedList:
def __init__(self):
self.head = None
Key Differences:
● Singly vs. Doubly Linked List: Singly linked lists have one pointer per node, while
doubly linked lists have two, allowing for traversal in both directions.
● Circular vs. Non-Circular Linked List: In circular lists, there is no end; it loops back to
the start.
Linked lists are flexible data structures that provide dynamic memory allocation and efficient
insertion/deletion. Each type serves different use cases depending on the problem
requirements.
Trees in DSA
A tree is a hierarchical data structure that consists of nodes connected by edges. The top node
is called the root, and each node can have zero or more children. Trees are used to represent
relationships, such as folder structures in a computer or an organizational chart.
1. Binary Trees
A Binary Tree is a tree where each node has at most two children, referred to as the left child
and right child.
Characteristics:
Real-Life Examples:
Basic Operations:
● Traversal: Visiting each node in a specific order (e.g., in-order, pre-order, post-order).
● Insertion/Deletion: Adding or removing nodes.
A Binary Search Tree (BST) is a type of binary tree that follows a specific order:
● The left child of a node contains values less than the node's value.
● The right child contains values greater than the node's value.
Characteristics:
Real-Life Examples:
● Phone Contacts: Contacts can be organized in alphabetical order for quick searching.
● Dictionaries: Words are stored in sorted order to facilitate fast look-up.
Basic Operations:
● Search: Start at the root and move left or right depending on whether the search value is
smaller or larger than the current node.
● Insertion: Place a new value at the correct position to maintain the order.
● Deletion: Remove a node while ensuring the tree remains a valid BST.
A Height Balanced Tree is a tree where the height difference between the left and right
subtrees of any node is kept minimal (usually no more than 1).
Purpose:
● To avoid skewed trees (like linked lists), which would make operations inefficient.
4. AVL Trees
An AVL Tree is a type of height-balanced binary search tree named after its inventors
Adelson-Velsky and Landis. In an AVL tree:
● The height difference (balance factor) between the left and right subtrees of any node
is at most 1.
Characteristics:
Real-Life Examples:
5. Heaps
A Heap is a special tree-based data structure that satisfies the heap property:
● In a Max-Heap, the parent node is always greater than or equal to its children.
● In a Min-Heap, the parent node is always less than or equal to its children.
Characteristics:
● Typically represented as a binary tree, but stored in an array for easy access.
● The root node represents the maximum (in Max-Heap) or minimum (in Min-Heap)
value.
Real-Life Examples:
● Priority Queues: Tasks are executed based on priority (e.g., highest priority first).
● Scheduling Algorithms: Choose the next task with the highest or lowest priority.
A Priority Queue is a data structure where elements are removed based on their priority rather
than their insertion order. In a Heap-based Priority Queue:
Real-Life Examples:
● Emergency Rooms: Patients are treated based on the severity of their condition, not
the order they arrive.
● CPU Task Scheduling: Processes with higher priority are executed before others.
A Double-Ended Priority Queue (DEPQ) allows for retrieving both the minimum and
maximum elements efficiently.
Characteristics:
● Supports insertion, deletion of the minimum element, and deletion of the maximum
element.
● Can be implemented using two heaps or specialized balanced trees.
Real-Life Examples:
● Financial Applications: Keeping track of both the highest and lowest stock prices.
● Event Management Systems: Managing both urgent and least urgent tasks.
1. Binary Tree:
○ Each node has at most two children.
○ Useful for simple hierarchical structures.
2. Binary Search Tree (BST):
○ Left child values are smaller, right child values are larger.
○ Efficient searching, insertion, and deletion.
3. Height Balanced Trees (e.g., AVL Tree):
○ Keeps the height difference between left and right subtrees minimal.
○ Self-balancing for maintaining efficiency.
4. Heaps:
○ Maintains a heap property (parent-child relationship).
○ Used in implementing priority queues.
5. Priority Queue (Heap-based):
○ Elements are accessed based on priority.
○ Common in scheduling tasks.
6. Double-Ended Priority Queue:
○ Supports operations to get both minimum and maximum efficiently.
○ Useful in scenarios where both extremes are needed.
Trees are essential in organizing data hierarchically and solving problems where ordering,
searching, and prioritizing are needed. Each tree type has its unique advantages and
applications.
Searching in DSA
Searching refers to finding a specific element within a collection of elements. The most
common searching techniques are Linear Search and Binary Search.
1. Linear Search
Linear Search is a simple search technique where we go through each element in the
collection one by one until we find the target element or reach the end.
Characteristics:
Real-Life Example:
● Finding a person's name in a list of unsorted contacts: You would check each
contact one by one until you find the name.
2. Binary Search
Binary Search is a more efficient searching technique that works on sorted lists. It repeatedly
divides the list into halves and compares the target value with the middle element.
Characteristics:
How It Works:
Real-Life Example:
● Finding a word in a dictionary: Since the dictionary is sorted alphabetically, you can
start in the middle and move left or right based on the word you are looking for.
1. Primary Index:
2. Secondary Index:
3. Multilevel Index:
● Index in a book: Helps you quickly find the page number of a topic without reading the
entire book.
Hashing
Hashing is a technique to map data to a fixed-size table (called a hash table) using a hash
function.
1. Hash Function
2. Collision Resolution
A collision occurs when two keys produce the same index. There are ways to handle these
collisions:
Characteristics:
Real-Life Example:
● Cubbies in a gym: Multiple items can be stored in the same cubby, each one stacked
inside (like a linked list).
In Closed Hashing, if a collision occurs, the next available slot in the table is used.
Methods:
Real-Life Example:
● Parking in a full parking lot: If your favorite spot is taken, you look for the next closest
empty spot.
Summary
● Linear Search: Simple, works on any list, but is slow for large collections.
● Binary Search: Efficient, works on sorted lists, quickly reduces the search space.
● Indexing: Improves search efficiency using primary, secondary, or multilevel structures.
● Hashing: Uses hash functions to map keys to a hash table, resolving collisions using
methods like open or closed hashing.
These techniques are used to optimize search and retrieval operations in different data
structures, making them crucial for tasks like database management, data storage, and
everyday programming tasks.
Sorting in DSA
1. Selection Sort
Selection Sort works by repeatedly finding the minimum (or maximum) element from the
unsorted portion and moving it to the sorted portion.
How It Works:
Characteristics:
Real-Life Example:
● Organizing books on a shelf from shortest to tallest by repeatedly picking the shortest
book and placing it in the correct position.
2. Bubble Sort
Bubble Sort works by repeatedly swapping adjacent elements if they are in the wrong order.
It "bubbles" the largest element to the end of the list.
How It Works:
Characteristics:
● Time Complexity: O(n²).
● Stable: Equal elements retain their original order.
● Simple but Inefficient: Not suitable for large lists.
Real-Life Example:
3. Insertion Sort
Insertion Sort works by building a sorted section of the list one element at a time. It picks an
element from the unsorted section and inserts it into the correct position in the sorted section.
How It Works:
Characteristics:
Real-Life Example:
● Sorting playing cards: When organizing cards in your hand, you pick one card at a time
and place it in its correct position relative to the other cards.
4. Shell Sort
Shell Sort is an improved version of insertion sort. It sorts elements that are far apart and then
reduces the gap between elements to sort. This approach helps to partially sort the list, making
it easier to fully sort later.
How It Works:
1. Start with a large gap and compare elements that are that far apart.
2. Sort the elements using insertion sort.
3. Reduce the gap and repeat the process.
4. Continue until the gap is 1 and the list is fully sorted.
Characteristics:
● Time Complexity: Varies depending on the gap sequence used (typically O(n log n) for
some sequences).
● Not Stable: May not maintain the original order of equal elements.
● More Efficient Than Insertion Sort: Particularly for large lists.
Real-Life Example:
● Sorting library books by comparing and rearranging books that are far apart, then
refining the order for nearby books.
5. Radix Sort
Radix Sort is a non-comparative sorting algorithm that sorts numbers digit by digit, starting from
the least significant digit (rightmost) to the most significant digit (leftmost).
How It Works:
Characteristics:
● Time Complexity: O(d * (n + k)), where d is the number of digits, n is the number of
elements, and k is the range of digits.
● Not In-Place: Uses extra space for grouping.
● Stable: Maintains the original order of equal elements.
Real-Life Example:
● Sorting a list of phone numbers by first grouping by the last digit, then the second last,
and so on.
6. Merge Sort
Merge Sort is a divide and conquer algorithm that splits the list into smaller sublists, sorts
each sublist, and then merges the sorted sublists back together.
How It Works:
Characteristics:
Real-Life Example:
7. Quick Sort
Quick Sort is another divide and conquer algorithm. It picks a pivot element and partitions
the list into two sublists: elements smaller than the pivot and elements larger than the pivot. The
process repeats for each sublist.
How It Works:
1. Choose a pivot.
2. Partition the list into elements smaller than and greater than the pivot.
3. Recursively sort the sublists.
4. Combine the sorted sublists.
Characteristics:
● Time Complexity: O(n log n) on average, but O(n²) in the worst case.
● In-Place: Uses no extra space for sorting.
● Not Stable: Does not maintain the original order of equal elements.
Real-Life Example:
● Arranging books on a shelf: Pick a book as a reference (pivot) and arrange books
smaller than the reference to one side and larger to the other.
8. Heap Sort
Heap Sort uses a heap data structure to sort the list. A heap is a complete binary tree where
the parent node is either greater than (Max-Heap) or smaller than (Min-Heap) its child nodes.
How It Works:
Characteristics:
Real-Life Example:
● Organizing a tournament bracket: The strongest competitor (heap's root) wins and is
moved to the final position, then the bracket is reorganized.
1. Selection Sort:
○ Finds the minimum element and swaps it.
○ Simple but inefficient for large lists.
2. Bubble Sort:
○ Swaps adjacent elements.
○ Simple but inefficient for large lists.
3. Insertion Sort:
○ Builds a sorted list one element at a time.
○ Efficient for small or nearly sorted lists.
4. Shell Sort:
○ An improved version of insertion sort with varying gaps.
○ Faster than insertion sort for larger lists.
5. Radix Sort:
○ Sorts numbers digit by digit.
○ Useful for sorting large numbers.
6. Merge Sort:
○ Divides the list and merges sorted halves.
○ Efficient but uses extra space.
7. Quick Sort:
○ Partitions around a pivot and sorts sublists.
○ Efficient but can be slow in the worst case.
8. Heap Sort:
○ Uses a heap to sort the list.
○ Efficient and works in-place.
Each sorting algorithm has its own strengths and weaknesses. The choice of which to use
depends on the size of the list, whether the list is already partially sorted, and memory
constraints.
Graphs in DSA
A graph is a data structure that consists of a set of nodes (vertices) and edges that connect
them. Graphs are used to represent various real-world relationships and structures, like
networks, social connections, maps, etc.
Graph Terminology
1. Vertex (Node): A point in the graph where an element is stored. For example, a city in a
map.
2. Edge (Connection): A line that connects two vertices, representing a relationship or link
between them. For example, a road between two cities.
3. Degree: The number of edges connected to a vertex. For example, if a city has three
roads leading out, its degree is 3.
4. Path: A sequence of edges that connects two vertices. For example, the route you take
from one city to another.
5. Cycle: A path that starts and ends at the same vertex. For example, a round trip that
returns to the starting city.
Graph Representation
There are two main ways to represent graphs: Adjacency List and Adjacency Matrix.
1. Adjacency List
● An Adjacency List represents the graph using a list where each vertex has a list of all
the other vertices it's connected to.
● It is efficient for sparse graphs (graphs with fewer edges).
Example:
● A is connected to B and C
● B is connected to A
● C is connected to A
makefile
Copy code
A: B, C
B: A
C: A
Real-Life Example:
2. Adjacency Matrix
● An Adjacency Matrix is a 2D array where the rows and columns represent the vertices.
● Each cell (i, j) contains a 1 if there is an edge between vertex i and vertex j, otherwise, it
contains a 0.
● It is efficient for dense graphs (graphs with many edges).
Example:
css
Copy code
A B C
A [0 1 1]
B [1 0 0]
C [1 0 0]
Real-Life Example:
● Flight connections: A matrix can represent if there is a direct flight between two cities.
Elementary Graph Operations
Graphs can be explored using Breadth-First Search (BFS) and Depth-First Search (DFS).
BFS explores the graph level by level, starting from a chosen vertex and visiting all its neighbors
before moving on to their neighbors.
How It Works:
Characteristics:
Real-Life Example:
● Finding the shortest path in a maze: BFS would explore all possible directions step by
step until it finds the exit.
DFS explores as far down a branch as possible before backtracking to explore other branches.
How It Works:
Characteristics:
Real-Life Example:
● Solving a puzzle: Trying different paths one by one until the puzzle is solved.
Spanning Trees
A spanning tree is a subgraph of a graph that includes all the vertices with the minimum
number of edges needed to connect them, forming a tree (a graph with no cycles).
Real-Life Example:
● Electrical wiring in a building: A spanning tree would ensure all rooms are connected
with the minimum amount of wiring.
Summary
These graph concepts and operations help solve problems related to networks, paths, and
connectivity in real-world applications like social networks, maps, and computer networks.