50+ Data Structures and Algorithms Interview Questions for 2025
By Rohit Sharma
Updated on Apr 15, 2025 | 58 min read | 12.9k views
Share:
For working professionals
For fresh graduates
More
By Rohit Sharma
Updated on Apr 15, 2025 | 58 min read | 12.9k views
Share:
Table of Contents
Did you know? Around 60% of candidates report that the hardest part of coding interviews is solving problems related to Data Structures and Algorithms (DSA).
Preparing for a Data Structures and Algorithms (DSA) interview in 2025 requires a deep understanding of core concepts like arrays, linked lists, trees, and graphs. The questions typically test problem-solving skills, algorithmic efficiency, and the ability to apply DSA principles to real-world scenarios. Mastering DSA will help you excel in technical interviews, where candidates are expected to solve problems while optimizing time and space complexities.
In this article, you’ll look at 50+ DSA interview questions that cover essential concepts, helping you prepare for your next engineering role!
Starting your career as a fresher? DSA interview questions are likely to be a key challenge. These questions test your problem-solving skills, coding efficiency, and understanding of algorithms.
In this section, you’ll focus on the must-know questions to help you ace your first technical interview.
A data structure is a way to organize and store data efficiently, enabling easy access and modification. It's a crucial concept in computer science as it lays the foundation for building efficient algorithms and solving complex problems.
Choosing the right data structure is crucial as it directly impacts the efficiency and scalability of algorithms by optimizing time and space complexity.
Here’s why data structures are important:
Real-World Applications: Data structures like hash tables and trees are used in databases and search engines to manage large volumes of data efficiently.
Data structures vary in type, each optimized for specific tasks. For example, arrays are great for fast indexing, while linked lists are better for dynamic memory allocation. Stacks and queues are essential for order-related tasks like function calls or scheduling, and trees are crucial for efficient searching and sorting.
Choosing the right data structure for the problem at hand is key to optimizing performance and resource usage in real-world applications.
To better understand how these data structures differ, let's break down their characteristics and use cases in the table below.
Type |
Description |
Use Case |
Arrays | A collection of elements stored in contiguous memory locations. | Fast access, but fixed size. |
Linked Lists | A linear data structure where each element points to the next. | Dynamic size, but slower access. |
Stacks | Follows Last In First Out (LIFO) principle. | Undo operations, function calls. |
Queues | Follows First In First Out (FIFO) principle. | Task scheduling, buffer management. |
Trees | Hierarchical structure with nodes connected by edges. | Fast searching and hierarchical data. |
An array is a data structure that stores elements in a contiguous block of memory. It allows fast access to elements via indexing but has a fixed size once initialized.
For example, historical stock prices are stored in arrays for fast access and analysis. These arrays allow efficient querying for tasks like technical analysis, market trend prediction, and real-time decision-making.
The fixed size ensures optimal memory allocation when dealing with a known range of data.
Use Cases:
Scheduling Algorithms: Arrays are used in task scheduling systems, such as in manufacturing, where they track jobs and their execution times.
A stack and a queue are both linear data structures, but they manage the order of elements differently. A stack allows elements to be added or removed from one end only, making it ideal for tasks that require reversing or backtracking.
It's commonly used in tasks like undo functionality in text editors or managing function calls in programming languages.
A queue, on the other hand, adds elements at one end and removes them from the other. This makes it suitable for tasks that require processing elements in a specific sequence, like task scheduling or data buffering.
It’s used in task scheduling systems or managing requests in web servers, ensuring tasks are processed in the order they arrive.
Let's break down their characteristics and use cases in the table below.
Feature |
Stack |
Queue |
Order | Last In, First Out (LIFO) | First In, First Out (FIFO) |
Insertion | Push at the top | Enqueue at the rear |
Deletion | Pop from the top | Dequeue from the front |
Use Cases | Undo operations, browser history | Task scheduling, data streaming |
A linked list is a linear data structure where each element, called a node, contains a value and a reference (or pointer) to the next node in the sequence. This structure allows for efficient insertion and deletion of elements, as it doesn’t require shifting elements like in arrays.
Linked lists are particularly useful when the size of the data is unknown or constantly changing, as they offer dynamic memory allocation. They’re commonly used in applications like implementing queues, stacks, and memory management systems.
Compared to array-based structures, linked lists excel in scenarios where frequent insertion and deletion are needed without the overhead of resizing or shifting elements.
Let's break down their differences in the table below.
Feature |
Linked List |
Array |
Memory | Non-contiguous, dynamic allocation | Contiguous memory allocation |
Access Time | Slower, needs traversal | Faster, direct access via index |
Size | Dynamic size | Fixed size after initialization |
Use Cases | Dynamic data storage, queue management | Static data storage, easy indexing |
Recursion is a technique in programming where a function calls itself to solve a smaller instance of the same problem. It's a useful approach when a problem can be broken down into smaller, identical subproblems.
Here's an example using recursion to calculate the nth Fibonacci number:
# Recursive function to calculate nth Fibonacci number
def fibonacci(n):
# Base case: return n if n is 0 or 1
if n <= 1:
return n
# Recursive case: call the function for (n-1) and (n-2) and add them
return fibonacci(n-1) + fibonacci(n-2)
# Example: Get the 6th Fibonacci number
print(fibonacci(6))
Output:
8
Explanation:
A tree is a hierarchical data structure consisting of nodes connected by edges. Each tree has a root node, and every node has zero or more child nodes. Trees are used extensively in problem-solving for efficient searching, sorting, and organizing data.
Key applications include:
A stack is a linear data structure that follows the Last In, First Out (LIFO) principle. It supports the following basic operations:
Example:
Implementing a stack to reverse a string:
class Stack:
def __init__(self):
self.stack = []
# Push element onto the stack
def push(self, item):
self.stack.append(item)
# Pop element from the stack
def pop(self):
if not self.is_empty():
return self.stack.pop()
return None
# Peek at the top element
def peek(self):
if not self.is_empty():
return self.stack[-1]
return None
# Check if stack is empty
def is_empty(self):
return len(self.stack) == 0
# Reversing a string using a stack
def reverse_string(s):
stack = Stack()
for char in s:
stack.push(char)
reversed_str = ''
while not stack.is_empty():
reversed_str += stack.pop()
return reversed_str
# Example: Reverse the string "hello"
print(reverse_string("hello"))
Output:
olleh
Explanation:
A binary search tree (BST) is a binary tree where each node has at most two children, and the left child’s value is less than the parent node’s value, while the right child’s value is greater.
This property allows for efficient searching, insertion, and deletion of nodes.
Let’s break down how BST differs from other types of trees:
Feature |
Binary Search Tree (BST) |
Binary Tree |
Node Order | Left < Parent < Right | No specific order |
Search Time | O(log n) average, O(n) worst case | O(n) |
Use Case | Fast searching and sorting | General-purpose tree structure |
Also Read: 5 Types of Binary Trees: Key Concepts, Structures, and Real-World Applications in 2025
Arrays are one of the most fundamental data structures, and their operations come with specific time and space complexities.
Here's a breakdown:
Operation |
Time Complexity |
Space Complexity |
Access (indexing) | O(1) | O(1) |
Search (linear) | O(n) | O(1) |
Insertion (at end) | O(1) | O(1) |
Insertion (at index) | O(n) | O(1) |
Deletion | O(n) | O(1) |
Sorting | O(n log n) (best case) | O(1) |
Explanation:
A priority queue is a data structure where each element is assigned a priority, and elements with higher priority are dequeued before elements with lower priority. It works like a regular queue, but the order of removal is determined by priority rather than the order in which elements were added.
Let’s break down some real-life use cases for priority queues:
A graph is a data structure made up of vertices (nodes) and edges (connections between nodes). It’s used to represent relationships between objects, such as social networks or web page links.
Let’s break down the different types of graphs:
Type of Graph |
Description |
Use Case |
Undirected Graph | Edges have no direction, meaning the connection is bidirectional. | Social Networks (friends, followers) |
Directed Graph (Digraph) | Edges have direction, representing one-way relationships. | Web page links, email networks |
Weighted Graph | Edges have weights, representing costs or distances. | Flight networks, transportation routes |
Cyclic Graph | Contains at least one cycle (a path that starts and ends at the same node). | Circuit design, dependency resolution |
Acyclic Graph | No cycles, often used in situations requiring hierarchy. | Task scheduling, tree structures |
A hash table is a data structure that maps keys to values using a hash function. It allows for efficient data retrieval by calculating an index (hash) for each key, making both insertions and lookups average O(1) time complexity.
Here’s how they work in real-world applications:
A circular queue is a type of queue where the last position is connected back to the first position, forming a circular structure. This allows for efficient use of space by reusing empty slots at the front of the queue, unlike a regular queue that may waste space when elements are dequeued.
Here’s the key difference and use case breakdown:
Feature |
Circular Queue |
Regular Queue |
Memory Usage | Reuses empty slots at the front, making it more efficient. | Can waste space if elements are removed from the front. |
Overflow | Prevents overflow by utilizing all available space. | May overflow if front is empty but rear is full. |
Use Case | Efficient for tasks with a fixed buffer size like CPU scheduling. | Used in situations where data is processed sequentially like printing jobs. |
Linear search checks each element in a list sequentially until it finds a match or reaches the end, whereas binary search works on sorted arrays by repeatedly dividing the search interval in half.
Let’s compare them directly:
Feature |
Linear Search |
Binary Search |
Time Complexity | O(n) | O(log n) |
Use Case | Unsorted lists, small datasets | Sorted lists, large datasets |
Efficiency | Slower for large datasets | Much faster for large datasets, but requires sorted data |
Application | Searching for a name in a list | Finding a book in a library catalog |
Also Read: Time and Space Complexity of Binary Search Explained
A linked list is a data structure where each element (node) contains data and a reference (or pointer) to the next node.
Here's a simple implementation in Python:
class Node:
def __init__(self, data):
self.data = data # Stores the data
self.next = None # Points to the next node in the list
class LinkedList:
def __init__(self):
self.head = None # The head node of the linked list
# Add a node at the end of the list
def append(self, data):
new_node = Node(data)
if not self.head:
self.head = new_node # Set the head to the new node if the list is empty
return
last_node = self.head
while last_node.next: # Traverse to the last node
last_node = last_node.next
last_node.next = new_node # Set the next of the last node to the new node
# Print the list
def print_list(self):
current_node = self.head
while current_node:
print(current_node.data, end=" -> ")
current_node = current_node.next
print("None")
# Example usage:
linked_list = LinkedList()
linked_list.append(10)
linked_list.append(20)
linked_list.append(30)
linked_list.print_list()
Output:
10 -> 20 -> 30 -> None
Explanation:
A singly linked list allows traversal in one direction (from head to tail), while a doubly linked list allows traversal in both directions (from head to tail and tail to head). This difference impacts the flexibility and efficiency of operations.
Let’s break down the key differences:
Feature |
Singly Linked List |
Doubly Linked List |
Direction of Traversal | One-way (head to tail) | Two-way (head to tail and tail to head) |
Memory Usage | More memory-efficient (only one pointer per node) | Requires more memory (two pointers per node) |
Operations | Easier for insertions/deletions at the head | More flexible for insertions/deletions anywhere |
Use Case | Basic applications, simpler memory management | More complex applications requiring bi-directional traversal |
With these concepts in mind, you're now prepared to tackle more advanced challenges with confidence. Ready to take it up a notch? Let’s explore data structure interview questions for experienced candidates, where complexity meets strategy.
As you gain more experience, the focus of data structure interview questions shifts to solving complex, real-world problems with optimal solutions. These questions are designed to push your understanding and assess how well you can handle challenges in dynamic, fast-paced environments.
Balancing a binary search tree (BST) keeps its height minimized, improving search times by ensuring operations like search, insert, and delete run in O(log n) time instead of O(n) in the worst case.
However, balancing operations, such as those in AVL or Red-Black trees, require additional time and space after each insertion or deletion, which can slow down frequent updates. The challenge is to balance fast searches with efficient updates, especially in large datasets like databases or file systems.
One way to balance a BST manually is by performing a tree rotation:
# Insertion in a balanced BST (AVL tree)
class Node:
def __init__(self, key):
self.left = None
self.right = None
self.value = key
self.height = 1
# Function for Right Rotation (used to balance the tree)
def right_rotate(y):
x = y.left
T2 = x.right
x.right = y
y.left = T2
y.height = max(get_height(y.left), get_height(y.right)) + 1
x.height = max(get_height(x.left), get_height(x.right)) + 1
return x
Explanation of the Code:
Sorting algorithms arrange elements in a list or array in a specific order, such as ascending or descending. They are crucial for optimizing search and data analysis operations, making data easier to work with. Choosing the right algorithm can greatly improve performance.
Let's break down the most common sorting algorithms and their time complexities:
Algorithm |
Best Time Complexity |
Average Time Complexity |
Worst Time Complexity |
Bubble Sort | O(n) | O(n²) | O(n²) |
Merge Sort | O(n log n) | O(n log n) | O(n log n) |
Quick Sort | O(n log n) | O(n log n) | O(n²) |
Insertion Sort | O(n) | O(n²) | O(n²) |
Selection Sort | O(n²) | O(n²) | O(n²) |
Explanation:
A hash map (or hash table) is a data structure that maps keys to values for fast data retrieval. It uses a hash function to compute an index (or hash) where the value is stored. Hash maps are highly efficient with an average time complexity of O(1) for insert, delete, and search operations.
Handling Collisions: Collisions occur when two keys produce the same hash value. There are a few strategies to handle this:
Here's an implementation of handling collisions using chaining:
class HashMap:
def __init__(self):
self.map = [[] for _ in range(10)] # Create 10 empty lists for chaining
def insert(self, key, value):
index = hash(key) % len(self.map)
for i, (k, v) in enumerate(self.map[index]):
if k == key:
self.map[index][i] = (key, value) # Update existing key
return
self.map[index].append((key, value)) # Add new key-value pair
def get(self, key):
index = hash(key) % len(self.map)
for k, v in self.map[index]:
if k == key:
return v
return None # If key doesn't exist
Explanation of the Code:
Dijkstra's algorithm is a graph search algorithm used to find the shortest path from a source node to all other nodes in a weighted graph. The algorithm maintains a set of nodes whose shortest distance from the source is known and iteratively selects the node with the smallest tentative distance.
Here’s the code for implementing Dijkstra’s algorithm:
import heapq
def dijkstra(graph, start):
# Initialize distances and priority queue
distances = {node: float('inf') for node in graph}
distances[start] = 0
pq = [(0, start)] # (distance, node)
while pq:
current_distance, current_node = heapq.heappop(pq)
# Skip if we have already found a shorter path
if current_distance > distances[current_node]:
continue
for neighbor, weight in graph[current_node]:
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(pq, (distance, neighbor))
return distances
# Example graph representation
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('A', 1), ('C', 2), ('D', 5)],
'C': [('A', 4), ('B', 2), ('D', 1)],
'D': [('B', 5), ('C', 1)]
}
print(dijkstra(graph, 'A'))
Output:
{'A': 0, 'B': 1, 'C': 3, 'D': 4}
Explanation of the Code:
To find the shortest path in an undirected graph using BFS, we treat the graph as unweighted and traverse it level by level. BFS explores all neighbors of a node before moving to the next level, which guarantees that the first time we reach a node, it is through the shortest path.
Here’s an implementation of BFS to find the shortest path in an undirected graph:
from collections import deque
# BFS to find shortest path in an unweighted graph
def bfs_shortest_path(graph, start, goal):
queue = deque([start]) # Queue to store nodes to explore
distances = {start: [None]} # Dictionary to store shortest path and predecessor
distances[start].append(0) # Distance to the start node is 0
while queue:
current_node = queue.popleft()
# Explore all neighbors
for neighbor in graph[current_node]:
if neighbor not in distances:
distances[neighbor] = [current_node, distances[current_node][1] + 1]
queue.append(neighbor)
if neighbor == goal: # If goal node is reached, stop search
return distances[neighbor][1] # Return the shortest distance
return -1 # Return -1 if there is no path
# Example graph representation (undirected graph)
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
# Example usage: Find shortest path between A and F
print(bfs_shortest_path(graph, 'A', 'F'))
Output:
3
Explanation:
AVL trees are a type of self-balancing binary search tree (BST) where the difference between the heights of the left and right subtrees (balance factor) of any node is at most 1. To maintain balance, AVL trees perform rotations during insertions and deletions.
Here's how AVL trees maintain balance:
Example:
# Left Rotation Implementation
class Node:
def __init__(self, key):
self.left = None
self.right = None
self.value = key
self.height = 1
# Left rotation function
def left_rotate(x):
y = x.right
T2 = y.left
y.left = x
x.right = T2
x.height = max(get_height(x.left), get_height(x.right)) + 1
y.height = max(get_height(y.left), get_height(y.right)) + 1
return y
Explanation:
Effect: The left rotation balances an unbalanced tree by shifting the subtree to the left, which is necessary when a node becomes "right-heavy."
Graphs can be represented in two primary ways: adjacency matrix and adjacency list. Both have their pros and cons depending on the use case.
Adjacency Matrix: An adjacency matrix is a 2D array used to represent a graph, where both rows and columns represent graph nodes (vertices). Each element in the matrix indicates whether an edge exists between the corresponding nodes.
In an undirected graph, the matrix is symmetric, meaning if there's an edge between node i and node j, then both matrix[i][j] and matrix[j][i] will have the same value.
Here’s a step-by-step algorithm to create an adjacency matrix for a graph:
Adjacency List: An adjacency list is a collection of lists or arrays where each node in the graph stores a list of its directly connected neighboring nodes. It is more memory-efficient than an adjacency matrix, especially for sparse graphs.
Here’s how it works:
Let’s compare the key differences between adjacency matrices and adjacency lists to better understand when to use each representation:
Feature |
Adjacency Matrix |
Adjacency List |
Space Complexity | O(n²), where n is the number of nodes | O(n + e), where e is the number of edges |
Memory Usage | Higher memory usage for sparse graphs | More memory efficient for sparse graphs |
Time Complexity (Search) | O(1) to check if there’s an edge between two nodes | O(d), where d is the number of neighbors (degree) |
Time Complexity (Insert) | O(1) to add an edge | O(1) for undirected, O(d) for directed (for each insertion) |
Ease of Traversal | Slower for sparse graphs, as you need to check all nodes | Faster for sparse graphs, directly accesses adjacent nodes |
Suitable For | Dense graphs with many edges | Sparse graphs with fewer edges |
Example Use Cases | Matrix-based algorithms (e.g., Floyd-Warshall for shortest paths) | Efficient for breadth-first search (BFS) or depth-first search (DFS) |
Dynamic Programming (DP) is a technique used for solving problems by breaking them down into simpler subproblems and storing their solutions to avoid redundant work. It is ideal for optimization problems.
Knapsack Problem (0/1 Knapsack): The problem asks to select items with given weights and values to maximize value without exceeding the weight capacity.
DP Approach:
def knapsack(weights, values, W):
n = len(weights)
dp = [[0] * (W + 1) for _ in range(n + 1)] # DP table
for i in range(1, n + 1):
for w in range(W + 1):
if weights[i-1] <= w:
dp[i][w] = max(dp[i-1][w], values[i-1] + dp[i-1][w - weights[i-1]])
else:
dp[i][w] = dp[i-1][w]
return dp[n][W]
# Example Usage
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
capacity = 7
print(knapsack(weights, values, capacity))
Output:
9
Explanation:
BFS and DFS are both graph traversal techniques, but BFS explores nodes level by level, ideal for shortest paths, while DFS explores as deep as possible along branches, useful for tasks like topological sorting.
Here are the key differences between BFS and DFS to better understand when to use each traversal method:
Feature |
BFS |
DFS |
Traversal Type | Level by level | Deeply, visiting each node's child first |
Data Structure Used | Queue | Stack (or recursion) |
Time Complexity | O(V + E), where V is vertices and E is edges | O(V + E) |
Space Complexity | O(V) for storing visited nodes | O(V) or O(h) (where h is the tree height) |
Use Case | Finding shortest path, web crawlers | Solving puzzles, topological sorting |
A heap is a complete binary tree that satisfies the heap property: in a max-heap, each parent node’s value is greater than its children, and in a min-heap, it’s the opposite.
Heaps are crucial in implementing priority queues, where the highest (or lowest) priority element can be accessed in O(1) time, and insertion/deletion takes O(log n) time.
Let’s compare it with a binary search tree (BST):
Feature |
Heap |
Binary Search Tree (BST) |
Structure | Complete binary tree, not necessarily balanced | Binary tree, nodes have specific ordering |
Time Complexity | O(log n) for insertion and deletion | O(log n) for balanced trees, O(n) for unbalanced trees |
Use Case | Priority queues, heap sort | Searching, sorting, and dynamic sets |
Also Read: Binary Tree vs Binary Search Tree: Difference Between Binary Tree and Binary Search Tree
Now that you have a solid foundation, let’s look into advanced questions, where you’ll explore more complex problems and strategies to tackle them in interviews.
This section dives into more complex data structure and algorithm problems. These questions require a deeper understanding of concepts such as dynamic programming, graph theory, tree manipulation, and advanced searching techniques.
Here, you’ll look into problems that test your ability to optimize algorithms, handle large datasets, and implement more sophisticated solutions.
A hash set is a collection that stores unique elements, and a hash map (or hash table) is a collection that stores key-value pairs. Both utilize a hash function to quickly access elements, but their operations differ slightly in terms of time complexity.
Time Complexities:
Both hash set and hash map operations are efficient in most cases, but their worst-case complexities can be O(n) when collisions occur.
Optimizing algorithms for large datasets requires a strategic approach to both time and space complexities. Let’s break down some key strategies:
Time Complexity Optimization:
Space Complexity Optimization:
By applying these strategies, you can ensure your algorithms are both time and space efficient, making them scalable for large datasets.
Divide and Conquer is a powerful problem-solving technique where a large problem is divided into smaller, more manageable subproblems. Each subproblem is solved independently, often recursively, and their solutions are combined to form the solution to the original problem.
This strategy is particularly effective for problems that can be broken down into similar, repetitive tasks.
The key steps in Divide and Conquer are:
This approach is widely used in sorting algorithms like MergeSort and QuickSort, where the array is repeatedly split into smaller parts and then recombined in sorted order.
Example: Merge Sort
def merge_sort(arr):
if len(arr) > 1:
mid = len(arr) // 2 # Find the middle
left_half = arr[:mid] # Divide the array into two halves
right_half = arr[mid:]
merge_sort(left_half) # Recursively sort the first half
merge_sort(right_half) # Recursively sort the second half
i = j = k = 0
# Merge the sorted halves
while i < len(left_half) and j < len(right_half):
if left_half[i] < right_half[j]:
arr[k] = left_half[i]
i += 1
else:
arr[k] = right_half[j]
j += 1
k += 1
while i < len(left_half): # Copy any remaining elements
arr[k] = left_half[i]
i += 1
k += 1
while j < len(right_half): # Copy any remaining elements
arr[k] = right_half[j]
j += 1
k += 1
# Example Usage
arr = [38, 27, 43, 3, 9, 82, 10]
merge_sort(arr)
print(arr)
Output:
[3, 9, 10, 27, 38, 43, 82]
Explanation:
The A* Algorithm is a popular pathfinding and graph traversal technique that finds the shortest path by considering both the actual cost to reach a node (g(n)) and the estimated cost to reach the goal (h(n)).
The formula f(n) = g(n) + h(n) guides the algorithm to prioritize promising paths, making it more efficient than other algorithms like Dijkstra’s.
Working:
The algorithm selects the node with the lowest f(n) value, expanding its neighbors until the goal is found.
Code Example:
import heapq
def a_star(start, goal, graph, heuristic):
open_list = []
heapq.heappush(open_list, (0 + heuristic[start], start)) # f(n) = g(n) + h(n)
g_scores = {start: 0}
came_from = {}
while open_list:
_, current = heapq.heappop(open_list)
if current == goal:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
return path[::-1]
for neighbor, cost in graph[current]:
tentative_g = g_scores[current] + cost
if neighbor not in g_scores or tentative_g < g_scores[neighbor]:
came_from[neighbor] = current
g_scores[neighbor] = tentative_g
f_score = tentative_g + heuristic[neighbor]
heapq.heappush(open_list, (f_score, neighbor))
# Example graph and heuristic
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('A', 1), ('C', 2), ('D', 5)],
'C': [('A', 4), ('B', 2), ('D', 1)],
'D': [('B', 5), ('C', 1)]
}
heuristic = {'A': 7, 'B': 6, 'C': 2, 'D': 0}
print(a_star('A', 'D', graph, heuristic))
Output:
['A', 'B', 'C', 'D']
Explanation:
Use Cases:
Tree traversal refers to visiting each node in a tree in a specific order. There are several ways to traverse a tree, depending on the problem you’re solving.
Types of Tree Traversals:
1. In-order Traversal:
def inorder(root):
if root:
inorder(root.left)
print(root.value, end=" ")
inorder(root.right)
2. Pre-order Traversal:
def preorder(root):
if root:
print(root.value, end=" ")
preorder(root.left)
preorder(root.right)
3. Post-order Traversal:
def postorder(root):
if root:
postorder(root.left)
postorder(root.right)
print(root.value, end=" ")
4. Level-order Traversal (Breadth-First Search):
from collections import deque
def level_order(root):
if not root:
return
queue = deque([root])
while queue:
node = queue.popleft()
print(node.value, end=" ")
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
A self-balancing binary search tree (BST) automatically adjusts its structure during insertion and deletion to maintain a balanced height, ensuring that operations like search, insertion, and deletion are efficient (O(log n)).
The two most common types of self-balancing BSTs are AVL trees and Red-Black trees.
Key Steps:
upGrad’s Exclusive Data Science Webinar for you –
Transformation & Opportunities in Analytics & Insights
Topological sorting is a technique used to arrange the vertices of a Directed Acyclic Graph (DAG) in a linear order such that for every directed edge u -> v, vertex u appears before vertex v in the ordering.
This order respects the dependencies between tasks or processes, making it particularly useful in applications where certain tasks must be completed before others.
Key Concepts:
Example:
For the graph:
A -> B -> D
A -> C -> D
Topological sort could be: A, B, C, D.
Explanation: A must come before both B and C, and B and C must come before D.
A greedy algorithm makes a series of decisions by choosing the best option available at each step, aiming for a globally optimal solution. Dynamic programming (DP), on the other hand, solves problems by breaking them down into overlapping subproblems and storing the results of subproblems to avoid redundant work.
Here’s a breakdown of the differences:
Feature |
Greedy Algorithm |
Dynamic Programming |
Approach | Makes decisions based on the current state without reconsidering previous choices. | Solves problems by breaking them into overlapping subproblems and storing results to avoid redundant work. |
Solution Optimality | Doesn't always guarantee an optimal solution. | Guarantees an optimal solution by considering all possibilities. |
Time Complexity | Typically O(n log n) or O(n), depending on the problem. | Often higher, typically O(n²) or O(nW) (for problems like Knapsack). |
Problem Type | Best for problems where local optimization leads to global optimization. | Best for problems with overlapping subproblems and optimal substructure. |
Example | Activity Selection Problem (selecting the maximum number of non-overlapping activities). | Knapsack Problem (maximize value while staying within a weight limit). |
Memory Usage | Low memory usage as it processes one step at a time. | Higher memory usage as it stores results of subproblems. |
A Bloom filter is a probabilistic data structure used to test if an element is a member of a set. It’s space-efficient but allows false positives, meaning it may incorrectly report that an element is in the set, but it never gives false negatives.
How it works:
Use Case:
The Longest Increasing Subsequence (LIS) is a subsequence of an array that is strictly increasing and has the longest possible length. You can solve this using dynamic programming.
Approach:
def lis(arr):
n = len(arr)
dp = [1] * n # Initialize dp array
for i in range(1, n):
for j in range(i):
if arr[i] > arr[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
# Example usage
arr = [10, 22, 9, 33, 21, 50, 41, 60, 80]
print(lis(arr))
Output:
6
Explanation:
The Knapsack problem is a classic optimization problem where the goal is to select a subset of items that fit within a given weight limit, such that the total value of the selected items is maximized. The items have two properties: weight and value.
The problem can be solved using dynamic programming for the 0/1 version of the problem, where each item can either be included or excluded.
Working (Dynamic Programming Approach):
Code Implementation:
def knapsack(weights, values, W):
n = len(weights)
dp = [[0] * (W + 1) for _ in range(n + 1)] # DP table initialization
for i in range(1, n + 1):
for w in range(W + 1):
if weights[i-1] <= w:
dp[i][w] = max(dp[i-1][w], values[i-1] + dp[i-1][w - weights[i-1]])
else:
dp[i][w] = dp[i-1][w]
return dp[n][W]
# Example Usage
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
capacity = 7
print(knapsack(weights, values, capacity))
Output:
9
Explanation:
The Bellman-Ford algorithm is used to find the shortest paths from a single source vertex to all other vertices in a weighted graph, even if the graph contains negative weight edges.
Unlike Dijkstra's algorithm, Bellman-Ford can handle graphs with negative weight edges and can also detect negative weight cycles.
Working (Bellman-Ford Algorithm):
Advantages over Dijkstra’s:
A segment tree is a binary tree used for storing intervals or segments. It allows querying and updating of elements efficiently, making it ideal for problems that require frequent range queries and updates, such as finding the sum or minimum of elements in a range.
How it works:
Use Case:
An LRU (Least Recently Used) cache stores a fixed number of items and removes the least recently used items when the cache exceeds its capacity. To achieve efficient access and updates, we can use a combination of a hash map and a doubly linked list.
Design Explanation:
Code Implementation:
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key: int) -> int:
if key in self.cache:
self.cache.move_to_end(key) # Move accessed item to the end
return self.cache[key]
return -1
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache.move_to_end(key) # Move existing item to the end
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # Pop the first (least recently used) item
# Example Usage
cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
print(cache.get(1)) # Returns 1
cache.put(3, 3) # Evicts key 2
print(cache.get(2)) # Returns -1 (not found)
Output:
1
-1
Explanation:
Ready to tackle the next set of questions? Let’s look into some viva questions, designed to test your grasp of both theoretical concepts and practical skills in college-level exams.
This section will focus on commonly asked during college practical exams. These questions test not only your theoretical understanding but also your ability to implement and manipulate data structures efficiently.
These questions will help you refine your skills and boost your confidence in handling data structures in real life scenarios.
To implement a stack using a linked list, we use the linked list's properties to manage stack operations like push, pop, and peek. The stack follows the LIFO (Last In, First Out) principle, where the most recently added element is accessed first.
Working:
Code Implementation:
class Node:
def __init__(self, data):
self.data = data
self.next = None
class Stack:
def __init__(self):
self.top = None
def push(self, data):
new_node = Node(data)
new_node.next = self.top
self.top = new_node
def pop(self):
if self.top:
temp = self.top
self.top = self.top.next
return temp.data
return None # Stack is empty
def peek(self):
return self.top.data if self.top else None
# Example Usage
stack = Stack()
stack.push(10)
stack.push(20)
print(stack.pop()) print(stack.peek())
Output:
20
10
Explanation:
Memory Allocation refers to how memory is assigned to variables during program execution. For arrays, this can be either static or dynamic, each with its own use cases and behavior.
Static Memory Allocation: The memory is allocated at compile-time, and the size of the array must be known in advance.
Dynamic Memory Allocation: The memory is allocated at runtime using pointers, and the array size can change during execution.
Let’s break it down:
Aspect |
Static Memory Allocation |
Dynamic Memory Allocation |
Memory Allocation Time | Compile-time | Runtime |
Size | Fixed size at compile time | Size can be changed during runtime |
Efficiency | Faster, no overhead | Slower, requires more management |
Example | int arr[10]; | int* arr = new int[n]; |
Depth-First Search (DFS) is a graph traversal algorithm where we explore as deeply as possible along each branch before backtracking. In the recursive version, we explore the node, then recursively explore its neighbors.
Working:
Code Implementation:
def dfs(graph, node, visited=None):
if visited is None:
visited = set()
visited.add(node)
print(node, end=" ")
for neighbor in graph[node]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
# Example Graph
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A'],
'D': ['B'],
'E': ['B']
}
dfs(graph, 'A')
Output:
A B D E C
Explanation:
DFS starts from node A, marking it as visited, then recursively visits each neighbor (B, D, E, and C). Each node is visited once, making the algorithm efficient for tree/graph traversal.
Tree traversal methods are used to visit all the nodes of a tree in a specific order. There are three common types of depth-first traversal:
Implementations:
# In-order Traversal
def inorder(root):
if root:
inorder(root.left)
print(root.value, end=" ")
inorder(root.right)
# Pre-order Traversal
def preorder(root):
if root:
print(root.value, end=" ")
preorder(root.left)
preorder(root.right)
# Post-order Traversal
def postorder(root):
if root:
postorder(root.left)
postorder(root.right)
print(root.value, end=" ")
Output:
For the tree:
1
/ \
2 3
/ \
4 5
A queue follows the FIFO (First In, First Out) principle. To implement a queue using two stacks, one stack is used for enqueueing (pushing elements) and the other stack is used for dequeuing (popping elements).
Working in Steps:
Code Implementation:
class QueueUsingStacks:
def __init__(self):
self.stack1 = []
self.stack2 = []
def enqueue(self, data):
self.stack1.append(data)
def dequeue(self):
if not self.stack2:
while self.stack1:
self.stack2.append(self.stack1.pop())
if self.stack2:
return self.stack2.pop()
return None # Queue is empty
# Example Usage
queue = QueueUsingStacks()
queue.enqueue(10)
queue.enqueue(20)
print(queue.dequeue()) # Outputs: 10
print(queue.dequeue()) # Outputs: 20
Explanation:
Output:
10
20
Memory management in the context of linked lists refers to the process of efficiently allocating and deallocating memory for nodes. Linked lists consist of nodes where each node points to the next. Proper memory management ensures that:
For singly linked lists, each node holds:
When nodes are added or removed, memory is allocated or deallocated dynamically, making it ideal for scenarios with uncertain data sizes.
Reversing a linked list means changing the direction of the pointers so that each node points to the previous node instead of the next node.
Iterative Method (Steps):
Code (Iterative):
class Node:
def __init__(self, data):
self.data = data
self.next = None
def reverse_linked_list_iteratively(head):
prev = None
current = head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
return prev # New head
Recursive Method (Steps):
Code (Recursive):
def reverse_linked_list_recursively(head):
if not head or not head.next:
return head
rest = reverse_linked_list_recursively(head.next)
head.next.next = head
head.next = None
return rest # New head
Output Example:
For the list 1 -> 2 -> 3 -> None, after reversing:
Explanation:
A priority queue is a data structure where each element has a priority associated with it. Elements with higher priority are dequeued before elements with lower priority. It can be implemented efficiently using a heap, which is a binary tree with specific properties that allow fast retrieval of the highest (or lowest) priority element.
Code Implementation (Min-Heap Priority Queue):
import heapq
class PriorityQueue:
def __init__(self):
self.heap = []
def enqueue(self, item, priority):
# We store tuples of (priority, item) so that heapq uses priority to sort
heapq.heappush(self.heap, (priority, item))
def dequeue(self):
if self.heap:
return heapq.heappop(self.heap)[1] # Return only the item, not the priority
return None # Queue is empty
def peek(self):
if self.heap:
return self.heap[0][1] # Peek at the item with the highest priority
return None # Queue is empty
# Example Usage
pq = PriorityQueue()
pq.enqueue("task1", 2)
pq.enqueue("task2", 1)
pq.enqueue("task3", 3)
print(pq.dequeue()) # Outputs: task2 (smallest priority)
print(pq.peek()) # Outputs: task1 (next highest priority)
Output:
task2
task1
Explanation:
A circular linked list is a variation of the linked list where the last node points back to the first node, forming a circular structure. This means that there is no None or null pointer at the end of the list; instead, the last node’s next pointer refers to the first node.
This structure is particularly useful in applications that require continuous traversal, such as round-robin scheduling.
Here’s a breakdown of the difference between circular and a regular linked list:
Feature |
Circular Linked List |
Regular Linked List |
Last Node's Pointer | Points to the first node, forming a loop. | Points to None or null (end of list). |
Traversal | Can be traversed in a circular manner, looping back to the first node. | Ends after visiting the last node. |
Use Case | Useful for applications that need continuous looping, like circular queues. | Typically used for linear traversal. |
Memory Efficiency | More efficient in circular scenarios where circular traversal is needed. | Can be less efficient in such cases, as it requires checking for end. |
A hash table is a data structure that stores key-value pairs, where each key is hashed to determine the index at which the value should be stored. It allows for fast retrieval of data based on the key.
However, collisions can occur when two keys hash to the same index. These collisions can be handled using methods like chaining or open addressing.
Collision Handling:
Code Implementation (Using Chaining):
class HashTable:
def __init__(self, size):
self.size = size
self.table = [[] for _ in range(size)] # Create a table with empty lists
def hash(self, key):
return hash(key) % self.size # Simple hash function (modulo size)
def insert(self, key, value):
index = self.hash(key)
for pair in self.table[index]:
if pair[0] == key:
pair[1] = value # Update value if key already exists
return
self.table[index].append([key, value]) # Insert new key-value pair
def get(self, key):
index = self.hash(key)
for pair in self.table[index]:
if pair[0] == key:
return pair[1] # Return value if key found
return None # Return None if key not found
def remove(self, key):
index = self.hash(key)
for i, pair in enumerate(self.table[index]):
if pair[0] == key:
del self.table[index][i] # Remove key-value pair
return
# Example Usage
ht = HashTable(5)
ht.insert("name", "Ajay")
ht.insert("age", 30)
print(ht.get("name")) # Outputs: Alice
ht.remove("name")
print(ht.get("name")) # Outputs: None
Output:
Ajay
None
Explanation:
These concepts form the backbone of many technical interviews, sobeing proficient in them will undoubtedly boost your problem-solving skills.
Let’s move on to how to prepare for DSA Interviews, where we’ll cover key strategies to help you tackle tough questions and excel in your interviews.
When preparing for DSA interviews, the key to success lies in understanding the concepts clearly before diving into problem-solving. Data Structures and Algorithms (DSA) are foundational to coding interviews, and having a strong grip over them takes consistent effort.
Think of it like training for a marathon—it's about pacing yourself, building strength, and progressively tackling more challenging concepts.
Before you jump into solving problems, it's crucial to have a solid grasp of the basics. Understanding these concepts ensures you know the "why" behind algorithms, not just the "how."
Key concepts every candidate should know:
Learning these will give you the foundation you need for solving basic DSA interview questions and moving to more advanced topics.
Now that you understand the core concepts, it's time to create a plan. A structured approach will help you cover all the important topics efficiently.
Here's a simple breakdown:
By following this structured approach, you’ll be better prepared to handle any DSA interview questions and answers that come your way.
Starting with easy problems helps build your confidence. Solving them boosts your problem-solving skills and prepares you for harder challenges.
Gradually increase the difficulty to ensure you’re well-prepared for the full spectrum of DSA interview questions and answers.
In interviews, time and space complexity are as important as getting the right answer. Employers want to know if your solution will scale well with large inputs.
Common complexity pitfalls:
To simulate real interview scenarios, mock interviews are invaluable. They help you practice problem-solving under time constraints and improve your communication skills.
Mock interviews are the closest you’ll get to an actual interview setting, and they’ll help you refine your approach for DSA interview questions.
As you continue practicing, it’s essential to track your progress. Create a DSA error log where you jot down the problems you struggled with and analyze your mistakes. Revisiting these will help you identify common mistakes and improve.
This reflective practice will improve your ability to solve data structure algorithm interview questions more effectively.
In interviews, it's not just about solving problems; it’s about demonstrating how you think. Being able to clearly explain your approach is crucial.
Being able to explain your approach will set you apart in DSA interviews and improve your chances of success.
Also Read: Tech Interview Preparation Questions & Answers
The key is not just solving problems but communicating your thought process clearly and efficiently. Let’s explore the best resources for practicing DSA, where you can apply your skills and tackle real life problems.
To truly excel at DSA interview questions, having the right resources is key. Prioritize platforms with clean interfaces and plenty of practice problems.
A mix of free and paid resources will give you both breadth and depth, helping you build a strong understanding of Data Structures and Algorithms (DSA).
Books provide in-depth coverage of DSA concepts and offer structured learning. Here are some great options to help you get started:
Online courses are excellent for structured learning and hands-on practice. Here are some top courses to enhance your skills:
Visual learners can benefit from expert explanations and problem-solving sessions on YouTube. Here are some top channels for DSA:
Having a clear roadmap is crucial for effective DSA preparation. It ensures you tackle topics in the right order, preventing overwhelm and promoting consistent progress.
With the right resources at your disposal, you’re well on your way to perfecting DSA. The combination of theory, practical problems, and a roadmap ensures you’re thoroughly prepared for DSA interview questions.
Also Read: Explore the Top 30+ DSA projects with source code in 2025
Now that you have your resources, it's time to practice. Let’s look at the best platforms for practicing DSA questions to sharpen your skills.
To excel in DSA interview questions, consistent practice is key. Prioritize platforms that offer a variety of problems, from easy to challenging, so you can gradually build your proficiency.
In addition, platforms with premium resources can help you simulate real interview environments, offering timed challenges and mock sessions that mirror actual coding interviews.
Here’s a breakdown of some top platforms where you can practice Data Structures and Algorithms (DSA):
Specialized degrees and short courses on data structures, system design, and coding interviews, ideal for perfecting DSA interview questions.
Extensive collection of problems, from easy to difficult, with solutions and community discussions, perfect for preparing for data structure algorithm interview questions.
Offers problems across difficulty levels and timed challenges to simulate real interview scenarios, improving speed and accuracy.
Focuses on competitive programming, providing tough problems that enhance problem-solving speed and efficiency for DSA interview questions.
Comprehensive platform with video explanations and advanced techniques, designed for tackling complex DSA interview questions.
With consistent practice, you’ll be well-equipped to tackle even the toughest DSA interview questions and stand out in your coding interviews.
In conclusion, mastering Data Structures and Algorithms (DSA) is crucial for acing technical interviews and building efficient solutions. By understanding key concepts like trees, graphs, and dynamic programming, and practicing various algorithms, you’ll enhance your problem-solving skills and speed.
If you want to deepen your understanding of DSA or explore other areas in the tech field, upGrad’s career counseling services can guide you in choosing the right path. Visit your nearest upGrad center today for in-person guidance and take the next step in advancing your career!
Unlock the power of data with our popular Data Science courses, designed to make you proficient in analytics, machine learning, and big data!
Elevate your career by learning essential Data Science skills such as statistical modeling, big data processing, predictive analytics, and SQL!
Stay informed and inspired with our popular Data Science articles, offering expert insights, trends, and practical tips for aspiring data professionals!
References:
https://www.qureos.com/career-guide/job-interview-statistics
Get Free Consultation
By submitting, I accept the T&C and
Privacy Policy
Start Your Career in Data Science Today
Top Resources