DSA
DSA (Data Structures and Algorithms) is the study of organizing data efficiently using data structures like arrays, stacks, and trees, paired with step-by-step procedures (or algorithms) to solve problems effectively. Data structures manage how data is stored and accessed, while algorithms focus on processing this data.
==========================================================================
Analysis of Algorithms
How do we compare two order of growths?
The following are some standard terms that we must remember for comparison.
c < Log Log n < Log n < n1/3 < n1/2 < n < n Log n < n2 < n2 Log n < n3 < n4 < 2n < nn
Here c is a constant
To analyze loops for complexity:
1. Understand the loop bounds
Linear loop:
for i in range(n)→ O(n).
The time complexity of a function (or set of statements) is considered as O(1) if it doesn't contain a loop, recursion, and call to any other non-constant time function.i.e. set of non-recursive and non-loop statements
Nested loop:
for i in range(n): for j in range(n)→ O(n²)
The Time Complexity of a loop is considered as O(n) if the loop variables are incremented/decremented by a constant amount.
The time complexity is defined as an algorithm whose performance is directly proportional to the squared size of the input data, as in nested loops it is equal to the number of times the innermost statement is executed. For example, the following sample loops have O(n2) time complexityLogarithmic loop:
while i < n: i *= 2→ O(log n)
The time Complexity of a loop is considered as O(Logn) if the loop variables are divided/multiplied by a constant amount. And also for recursive calls in the recursive function, the Time Complexity is considered as O(Logn).Logarithmic Time Complexity O(Log Log n):
The Time Complexity of a loop is considered as O(LogLogn) if the loop variables are reduced/increased exponentially by a constant amount.
2. Look at nested vs sequential loops
Sequential loops (one after another): Add their complexities →
O(n) + O(n²) = O(n²)Nested loops: Multiply complexities →
O(n) * O(n) = O(n²)
3. Consider conditional branches (if-else)
- Focus on the worst-case path when calculating worst-case complexity.
Asymptotic Notations for Analysis of Algorithms
Asymptotic notations describe how an algorithm’s time or space complexity grows as the input size (n) increases — particularly when n becomes very large. These notations help us compare the efficiency of algorithms, independent of machine and implementation details.There are mainly three asymptotic notations:
- Big-O Notation (O-notation) - worst case
- Omega Notation (Ω-notation) - best case
- Theta Notation (Θ-notation) - average case
🔺 1. Big-O Notation – O(f(n))
Definition: Describes the upper bound (worst-case) of an algorithm.
Use: Guarantees that the algorithm won’t take more than this time.
Example:
If an algorithm takes at most5n^2 + 3n + 7steps, we write:O(n²)(drop constants and lower-order terms).
✅ Think: "At most this fast."
🔻 2. Omega Notation – Ω(f(n))
Definition: Describes the lower bound (best-case) of an algorithm.
Use: Guarantees that the algorithm takes at least this much time.
Example:
If in the best case, it takesnsteps, we write:Ω(n)
✅ Think: "At least this fast."
⚖️ 3. Theta Notation – Θ(f(n))
Definition: Describes the tight bound — both upper and lower bounds.
Use: When the algorithm always takes about this much time, regardless of best or worst case.
Example:
If an algorithm always takes3n + 2steps, then:Θ(n)
✅ Think: "Exactly this fast (asymptotically)."
Properties of Asymptotic Notations
✅ 1. General Property (Multiplicative Constants)
If
f(n) = O(g(n)),
then
a × f(n) = O(g(n)) (where a is a positive constant)
✅ 2. Transitive Property
If
f(n) = O(g(n)) and g(n) = O(h(n)),
then
f(n) = O(h(n))
Searching Algorithms
Sorting Techniques
Linear search
Consider the array arr[] = {10, 50, 30, 70, 80, 20, 90, 40} and key = 30
=============================Binary search
used in a sorted array by repeatedly dividing the search interval in half.
It works by repeatedly dividing the search interval in half:
If the target value is equal to the middle element — ✅ return its index.
If the target is less than the middle element — 🔽 search the left half.
If the target is greater — 🔼 search the right half.
⏱️ Step-by-Step Breakdown
Let’s say the array has n elements.
1st step: search space =
n2nd step: search space =
n/23rd step: search space =
n/4...
After
ksteps:n / (2^k) = 1
Solve for k:
=============================
Quick sort
✅ Steps:
Pick a Pivot: Choose an element from the array.
Common choices: first, last, middle, or random.
Partition the Array:
Rearrange the elements so that:
All elements less than or equal to the pivot go to the left.
All elements greater than the pivot go to the right.
Recursively apply Quick Sort to the left and right sub-arrays.
Combine the sorted sub-arrays and the pivot to get the final sorted list.
Step 1: Choose a pivot
Let’s pick the last element, 5, as the pivot.
Step 2: Partition the list
Elements less than or equal to 5 →
[1]Pivot →
5Elements greater than 5 →
[10, 7, 8, 9]
New list:
Step 3: Recursively sort sublists
Sort [1] → already sorted.
Sort [10, 7, 8, 9], pivot = 9:
Left of 9:
[7, 8]Pivot:
9Right of 9:
[10]
Sort [7, 8] (pivot = 8):
Left:
[7]Pivot:
8Right:
[]
Now everything is sorted:
Result:
=============================
Merge sort
🧠 Key Idea:
Breaking down the problem into small subproblems
Merging two sorted arrays efficiently (like zippering)
Bubble Sort - O(n^2) Time and O(1) Space
🔁 How Bubble Sort Works
Start from the beginning of the list.
Compare the first two elements:
If the first is greater than the second, swap them.
Move to the next pair (2nd and 3rd), and repeat step 2.
Continue until the end of the list (this completes one pass).
Repeat the process for the remaining elements (
n-1times in total).
👀 Visualization
For this list:[5, 3, 8, 4, 2]
Pass 1:
Compare 5 and 3 → Swap →
[3, 5, 8, 4, 2]Compare 5 and 8 → OK
Compare 8 and 4 → Swap →
[3, 5, 4, 8, 2]Compare 8 and 2 → Swap →
[3, 5, 4, 2, 8]
✅ Largest element (8) is now at the end.
Pass 2:
Compare 3 and 5 → OK
Compare 5 and 4 → Swap →
[3, 4, 5, 2, 8]Compare 5 and 2 → Swap →
[3, 4, 2, 5, 8]
(No need to compare 8, it's already sorted)
...and so on until the list is sorted.
Insertion Sort - O(n^2) Time and O(1) Space
⚙️ How Insertion Sort Works
Given an array:[7, 3, 5, 2]
You treat the first element as sorted, then:
Pick the next element (
3) and compare it with the sorted portion (7), insert it in the correct position →[3, 7, 5, 2]Pick
5, compare with7and3, and insert it →[3, 5, 7, 2]Pick
2, compare with all before, and insert it →[2, 3, 5, 7]
🔁 You shift larger elements to the right to make space for the insertion.
=============================
Selection Sort - O(n^2) Time and O(1) Space
👣 Step-by-Step Example
Sort [64, 25, 12, 22, 11] using selection sort:
Step 1: Find min (11), swap with 64 →
[11, 25, 12, 22, 64]Step 2: Find min (12), swap with 25 →
[11, 12, 25, 22, 64]Step 3: Find min (22), swap with 25 →
[11, 12, 22, 25, 64]Step 4: Already sorted
==========================================================================
Hashing in Data Structure
Hashing is a technique used in data structures that efficiently stores and retrieves data in a way that allows for quick access.
- Hashing refers to the process of generating a small sized output (that can be used as index in a table) from an input of typically large and variable size. Hashing uses mathematical formulas known as hash functions to do the transformation. This technique determines an index or location for the storage of an item in a data structure called Hash Table.
- Hashing involves mapping data to a specific index in a hash table (an array of items) using a hash function. It enables fast retrieval of information based on its key.
- The great thing about hashing is, we can achieve all three operations (search, insert and delete) in O(1) time on average.
- Hashing is mainly used to implement a set of distinct items (only keys) and dictionaries (key value pairs).
There are mainly two forms of hash typically implemented in programming languages.
Hash Set : Collection of unique keys Implemented as Set in Python.
Hash Map : Collection of key value pairs with keys being unique (Implemented as dictionary in Python)
🔐 What is a Hash Function?
A hash function is a function that takes an input (or "key") and returns a fixed-size string or number, usually used to index data in a hash table.
Efficient data lookup
Quick insertion and deletion
Avoid collisions (ideally)
What is Collision in Hashing?
When two or more keys have the same hash value, a collision happens.
Collision Resolution Techniques
✅ Collision Resolution Techniques
There are two main categories:
1. Open Hashing (Chaining)
Each index in the hash table points to a linked list (or chain) of keys that hash to that index.
2. Closed Hashing (Open Addressing)
All elements are stored within the table.
On collision, find another empty slot using a probing technique.
==========================================================================
🔍 What is the Two Pointers Technique?
It involves using two variables (pointers) that move through the data structure (like an array) either:
From both ends toward the center, or
Sequentially through the array with different speeds or position
used for Two Sum in Sorted Arrays, Closest Two Sum, Three Sum, Four Sum, Trapping Rain Water
1.Two Sum - Pair sum in sorted array
Output: 1 2
Explanation: Since the array is 1-indexed, arr[1] + arr[2] = 2 + 7 = 9
Input: arr[] = [1, 3, 4, 6, 8, 11] target = 10
Output: 3 4
Explanation: Since the array is 1-indexed, arr[3] + arr[5] = 4 + 6 = 10
Output: Yes
Explanation : There is a pair (20, 50) with given target.
Two-Pointer Technique - O(n) time and O(1) space
The idea of this technique is to begin with two corners of the given array. We use two index variables left and right to traverse from both corners.
Initialize: left = 0, right = n - 1
Run a loop while left < right, do the following inside the loop
- Compute current sum, sum = arr[left] + arr[right]
- If the sum equals the target, we’ve found the pair.
- If the sum is less than the target, move the left pointer to the right to increase the sum.
- If the sum is greater than the target, move the right pointer to the left to decrease the sum.
3. Remove Duplicates from Sorted Array
4.Two Sum - Pair Closest to 0
Explanation: The min absolute sum pair is (5, -6)
5.Sum - Triplet Sum in Array
Output: true
Explanation: The triplet [1, 4, 8] sums up to 13
Input: arr[] = [3, 0, 1, 0, 4, 0, 2]
Output: 10
Explanation: The expected rainwater to be trapped is shown in the above image.
🔑 Idea:
At each index, the amount of water trapped depends on the minimum of the maximum heights to its left and right, minus the height of the bar at that index.
At each index, water trapped =sum of min(highest bar on left, highest bar on right) - current height
Steps:
Initialize two pointers:
left = 0andright = len(height) - 1Maintain
left_maxandright_maxMove the pointer pointing to the shorter bar inward
If
height[left] < height[right]:If
height[left] >= left_max, updateleft_maxElse, add
left_max - height[left]to result
Similar logic if
height[right] < height[left]
==========================================================================
Sliding Window Technique
Sliding Window Technique is a method used to solve problems that involve subarray or substring or window.
- The main idea is to use the results of previous window to do computations for the next window.
- This technique is commonly used in algorithms like finding subarrays with a specific sum, finding the longest substring with unique characters, or solving problems that require a fixed-size window to process elements efficiently.
Input : arr[] = {100, 200, 300, 400}, k = 2
Output : 700
We get maximum sum by considering the subarray [300, 400]
1.Maximum Sum of a Subarray with K Elements
Output : 700
We get maximum sum by considering the subarray [300, 400]
Slide the Window
In each step, remove the element going out from the left and add the new element coming from the right.
==========================================================================
==========================================================================
Prefix Sum Array
Output: [10, 30, 40, 45, 60]
Explanation: For each index i, add all the elements from 0 to i:
prefixSum[0] = 10,
prefixSum[1] = 10 + 20 = 30,
prefixSum[2] = 10 + 20 + 10 = 40 and so on.
==========================================================================
String in Data Structure
==========================================================================
ARRAY
Array is a collection of items of the same variable type that are stored at contiguous memory locations.🎯 Why Do We Need Arrays?
Imagine this situation:
🧠 Without Arrays:
You want to store marks for 5 students:
✅ Works for a small number.
❌ But what if there are 1000 students?
You’d have to create 1000 variables manually — that's impractical and unmanageable.
🔹 1. Fixed Size Arrays
arr = [0] * 5 # Creates: [0, 0, 0, 0, 0]
⚠️ Problems with fixed-size arrays:
Wasted memory if array is too big.
Insufficient memory if array is too small.
Need to know the size in advance.
🔸 2. Dynamic Size Arrays
✅ Definition: Arrays that can grow or shrink in size during runtime.
QUEUE
A Queue is a linear data structure that stores elements in the order they were added.
It follows the FIFO (First In, First Out) principle:
The first element inserted is the first one to be removed.
📦 Think of it like a line at a ticket counter — first person in the line is served first.
Python Queue can be implemented by the following ways:
- list
- collections.deque
- queue.Queue
1.Implementation using collections.deque
There are various functions available in this module:
- maxsize – Number of items allowed in the queue.
- empty() – Return True if the queue is empty, False otherwise.
- full() – Return True if there are maxsize items in the queue. If the queue was initialized with maxsize=0 (the default), then full() never returns True.
- get() – Remove and return an item from the queue. If queue is empty, wait until an item is available.
- get_nowait() – Return an item if one is immediately available, else raise QueueEmpty.
- put(item) – Put an item into the queue. If the queue is full, wait until a free slot is available before adding the item.
- put_nowait(item) – Put an item into the queue without blocking. If no free slot is immediately available, raise QueueFull.
- qsize() – Return the number of items in the queue.
Deque in Python
🔁 What Makes a deque Special?
Unlike regular queues or stacks, a deque allows:
Appending at both ends:
append()andappendleft()Popping from both ends:
pop()andpopleft()
This makes it useful for implementing both:
FIFO → like a regular queue (
append+popleft)LIFO → like a stack (
append+pop)
Types of Restricted Deque Input
- Input Restricted Deque: Input is limited at one end while deletion is permitted at both ends.
- Output Restricted Deque: output is limited at one end but insertion is permitted at both ends.
Appending and Deleting Dequeue Items
- append(x): Adds x to the right end of the deque.
- appendleft(x): Adds x to the left end of the deque.
- extend(iterable): Adds all elements from the iterable to the right end.
- extendleft(iterable): Adds all elements from the iterable to the left end (in reverse order).
- remove(value): Removes the first occurrence of the specified value from the deque. If value is not found, it raises a ValueError.
- pop(): Removes and returns an element from the right end.
- popleft(): Removes and returns an element from the left end.
- clear(): Removes all elements from the deque.
Accessing Item and length of deque
- Indexing: Access elements by position using positive or negative indices.
- len(): Returns the number of elements in the deque.
Count, Rotation and Reversal of a deque
- count(value): This method counts the number of occurrences of a specific element in the deque.
- rotate(n): This method rotates the deque by n steps. Positive n rotates to the right and negative n rotates to the left.
- reverse(): This method reverses the order of elements in the deque.
Stack Data Structure
🥞 What is a Stack?
A Stack is a linear data structure where:
Insertion and deletion of elements happen at only one end: the top.
It follows the LIFO (Last In, First Out) principle.
Think of it like:
A stack of books or plates.
You can only take the top one off first.
You can only add one on top.
Basic Operations on Stack:
In order to make manipulations in a stack, there are certain operations provided to us.
- push() to insert an element into the stack
- pop() to remove an element from the stack
- top() Returns the top element of the stack.
- isEmpty() returns true if stack is empty else false.
- isFull() returns true if the stack is full else false.
✅ Algorithm for Push Operation
Check for Overflow:
If the stack has reached its maximum capacity (
top == capacity - 1), it’s full, and we cannot push any more items.This condition is called Stack Overflow.
Insert the Element:
If there's space, increment
topby 1:top = top + 1.Place the new element at the
topindex:stack[top] = value.
Implementation using Fixed Sized Array
Stack - Linked List Implementation
Stack Implementation using Deque
Linked List
📌 What is a Singly Linked List?
A Singly Linked List is a linear data structure consisting of a sequence of nodes, where each node contains:
Data – the value stored in the node.
Next – a reference (or pointer) to the next node in the sequence.
The last node has its next set to None, indicating the end of the list.
Step-by-step approach at start:
- Create a new node with the given value.
- Set the next pointer of the new node to the current head.
- Move the head to point to the new node.
- Return the new head of the linked list.
Step-by-step approach to insert at last:
- Create a new node with the given value.
- Check if the list is empty:
- If it is, make the new node the head and return.
- Traverse the list until the last node is reached.
- Link the new node to the current last node by setting the last node's next pointer to the new node.
Step-by-step approach at any position:
- Create a new node and assign it a value.
- If inserting at the beginning (position = 1):
- Point the new node’s
nextto the current head. - Update the head to the new node.
- Return (Insertion done).
- Point the new node’s
- Otherwise, traverse the list:
- Start from the head and move to the
(position - 1)ᵗʰnode (just before the desired position). - If the position is beyond the list length, return an error or append at the end.
- Start from the head and move to the
- Insert the new node:
- Point the new node’s
nextto the next node of the current position. - Update the previous node’s
nextto the new node.
- Point the new node’s
- Return the updated list.
5.Reverse in Singly Linked List
Step-by-step approach:
- Initialize three pointers:
prev = NULL(to track the previous node)current = head(starting point)next = NULL(to store the next node temporarily)
- Iterate through the list:
- Store
next = current->next(save next node). - Reverse the link:
current->next = prev. - Move
prevandcurrentforward (prev = current,current = next).
- Store
- Update
headtoprev(new head is the last node).
Steps-by-step approach at start:
- Check if the head is NULL.
- If it is, return NULL (the list is empty).
- Store the current head node in a temporary variable temp.
- Move the head pointer to the next node.
- Delete the temporary node.
- Return the new head of the linked list.
Step-by-step approach at end:
- Check if the head is NULL.
- If it is, return NULL (the list is empty).
- Check if the head's next is NULL (only one node in the list).
- If true, delete the head and return NULL.
- Traverse the list to find the second last node (second_last).
- Delete the last node (the node after second_last).
- Set the next pointer of the second last node to NULL.
- Return the head of the linked list.
Step-by-step approach at pos:
- Check if the list is empty or the position is invalid, return if so.
- If the head needs to be deleted, update the head and delete the node.
- Traverse to the node before the position to be deleted.
- If the position is out of range, return.
- Store the node to be deleted.
- Update the links to bypass the node.
- Delete the stored node.
Doubly Linked List Tutorial
1.Step-by-Step Approach for Traversal:
- Start from the head of the list.
- Traverse forward:
- Visit the current node and process its data (e.g., print it).
- Move to the next node using
current = current->next. - Repeat the process until the end of the list (
current == NULL).
- Optionally, traverse backward:
- Start from the tail (last node).
- Visit the current node and process its data.
- Move to the previous node using
current = current->prev. - Repeat the process until the beginning of the list (
current == NULL).
2. Backward Traversal of Doubly Linked List
2. Finding Length of Doubly Linked List
3. Insertion in a Doubly Linked List
1. Insertion at the Beginning
- Create a new node with the given data.
- Set the
nextpointer of the new node to the current head. - If the list is not empty, update the
prevpointer of the current head to point to the new node. - Update the head of the list to the new node.
2. Insertion at a Specific Position
- Create a new node with the given data.
- If inserting at the beginning, follow the steps for insertion at the start.
- Traverse the list to find the node after which insertion is needed.
- Set the
nextpointer of the new node to the next node of the current position. - Set the
prevpointer of the new node to the current node. - Update the
prevpointer of the next node to point to the new node (if it exists). - Update the
nextpointer of the previous node to point to the new node.
4. Deletion in a Doubly Linked List
1. Deletion at the Beginning
- Check if the list is empty; if it is, return as there is nothing to delete.
- Store the current head node in a temporary variable.
- Move the head pointer to the next node.
- If the new head exists, update its
prevpointer toNULL. - Delete the old head node to free memory.
🔁 What is a Circular Linked List?
A Circular Linked List is a variation of a linked list in which the last node points back to the first node, forming a closed loop.
There is no
NULLat the end.Traversal can start at any node and continue indefinitely.
Can be singly or doubly circular.
1. Circular Singly Linked List
In Circular Singly Linked List, each node has just one pointer called the "next" pointer. The next pointer of the last node points back to the first node and this results in forming a circle. In this type of Linked list, we can only move through the list in one direction.

2. Circular Doubly Linked List:
In circular doubly linked list, each node has two pointers prev and next, similar to doubly linked list. The prev pointer points to the previous node and the next points to the next node. Here, in addition to the last node storing the address of the first node, the first node will also store the address of the last node.

1. Insertion in an empty List in the circular linked list
2. Insertion at the beginning in circular linked list
3. Insertion at the end in circular linked list
4. Insertion at specific position in circular linked list
Deletion from a Circular Linked List
1. Delete the first node in circular linked list
2. Delete a specific node in circular linked list
Searching in Circular Linked list
Approach: The approach to find an element in Circular Linked List can be visualized as follows:
- Keep a pointer to the head of the Circular Linked List. This will help us to determine if the Linked List has been traversed already.
- Now traverse the Linked List one by one and check if the element is present or not.
- If the element is present, return true.
- Now if the element is not present and the traversal pointer reaches the head again, it means the element is not present in the CLL. Therefore return false.
Comments
Post a Comment