Data Structure Basic Interview Question
Data Structure Basic Interview Question
📘 Fundamentals
1. What is a Data Structure?
A data structure is a way of organizing, storing, and managing data in a computer so that it
can be used efficiently.
In Simple Terms:
Think of a data structure like a container that holds data in a specific format, making it easier
to access and perform operations like searching, inserting, deleting, and updating.
Example:
To store a list of student names:
Data structures can be broadly categorized into several types based on how they organize
data and how they operate. Here's a clear breakdown:
Integer
Float
Character
Boolean
Pointer
Summary Table:
Category Examples
Primitive int, char, float, boolean
Linear Array, Linked List, Stack, Queue
Non-Linear Tree, Graph
Hash-based Hash Table, Hash Map, Hash Set
The difference between linear and non-linear data structures lies mainly in how the data
elements are organized and accessed. Here's a clear comparison:
📌 Characteristics:
Elements are stored one after another.
Traversal is done in a single level.
Typically one element per level.
Simple implementation and memory usage.
📋 Examples:
Array
Linked List
Stack
Queue
🧠 Analogy:
Think of it like a line of people in a queue—each person has one person before and after
them.
🔸 Non-Linear Data Structures
✅ Definition:
In non-linear data structures, elements are not arranged sequentially. They are organized
hierarchically or in complex relationships (e.g., parent-child or graph connections).
📌 Characteristics:
Elements can be connected in multiple ways.
Traversal is done across multiple levels.
Complex structure and memory use.
More powerful for representing real-world relationships.
📋 Examples:
Tree
Binary Search Tree
Heap
Graph
Trie
🧠 Analogy:
Think of it like a family tree or a web where one person can have multiple children or
connections.
An Abstract Data Type ( ADT ) is a logical description of how data is organized and
what operations can be performed on it, without specifying how it's implemented.
🔍 Key Idea:
ADT focuses on what operations are to be performed, not how they are
implemented.
It hides the internal details (data storage and algorithms) and only shows the functionality.
📌 Characteristics of ADT :
🧠 Real-Life Analogy:
Think of a vending machine:
📌 Characteristics:
Fixed memory size
Easy to implement
Less flexible but more memory-efficient if size is known in advance
Faster access due to predictable memory layout
📋 Examples:
Array
Static Stack (implemented with an array)
Static Queue
🧠 Analogy:
Think of it like a train with a fixed number of seats — you can't add or remove seats once
it’s built.
📌 Characteristics:
Memory is managed during runtime
More flexible and scalable
Slightly more complex to implement
May use more memory due to overhead (pointers, etc.)
📋 Examples:
Linked List
Dynamic Stack (linked list-based)
Dynamic Queue
Trees
Graphs
🧠 Analogy:
Like a building made of LEGO bricks — you can add or remove parts as needed.
✅ Summary:
Use static structures when the size is known and fixed.
Use dynamic structures when the size may change during execution.
📦 Arrays
6. What is an array?
✅ What is an Array?
An array is a linear data structure that stores a collection of elements (typically of the
same data type) in contiguous memory locations.
🔍 Key Characteristics:
Feature Description
Fixed size Once defined, the size of the array cannot change (in most
languages).
Same data type All elements must be of the same type (e.g., all integers, all floats).
Index-based Elements can be accessed directly using an index (starting at 0).
access
Efficient traversal Arrays support fast access and iteration.
📌 Syntax Example
Python:
C++:
🧠 Visual Representation:
Index: 0 1 2 3
Element: 10 20 30 40
✅ Advantages of Arrays:
Fast access using index ( O(1) time).
Easy to implement and use.
Memory efficiency (compact storage).
❌ Disadvantages:
Fixed size (can't grow or shrink dynamically).
Costly insertions/deletions (especially in the middle).
Wasted memory if the array is not fully used.
📋 Common Operations:
Operation Description Time Complexity
Access Get element by index O(1)
Insertion Add element (middle or front) O(n)
Deletion Remove element (middle or front) O(n)
Search Find element by value O(n)
Traverse Go through all elements O(n)
Here's a quick breakdown of all three topics you mentioned, with clear explanations and
comparisons. You can dive deeper into any of them afterward.
🔷 1. Multi-Dimensional Arrays
✅ What is it?
A multi-dimensional array is an array of arrays—used to represent data in matrix or tabular
form ( 2D , 3D , etc.).
Python:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(matrix[1][2]) # Output: 6
C++:
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
cout << matrix[1][2]; // Output: 6
✅ Real-world use:
Game boards (chess, tic-tac-toe)
Matrices in mathematics
Grids in images or maps
🔷 2. Dynamic Arrays
✅ What is it?
A dynamic array is an array that can grow or shrink at runtime. It overcomes the size
limitation of static arrays.
🧠 Example:
In Python, lists are dynamic arrays.
In C++, you can use std::vector .
Python:
arr = [1, 2, 3]
[Link](4)
print(arr) # Output: [1, 2, 3, 4]
C++:
#include <vector>
vector<int> arr = {1, 2, 3};
arr.push_back(4);
✅ Internally:
Dynamic arrays resize automatically when full (usually by doubling the size), but this can
have performance costs occasionally.
🧠 Analogy:
Array = Books on a shelf: Easy to find by position, but hard to insert in between.
Linked List = Chain of paper clips: Easy to add/remove clips, but hard to jump to the
50th one.
✅ Summary:
Use multi-dimensional arrays when modeling tables, grids, or matrices.
Use dynamic arrays when the size of the array isn't known in advance.
Use linked lists when you need frequent insertions/deletions, especially in the middle of
data.
✅ Advantages of Arrays
Advantage Description
🔹 Fast Access (O(1)) You can access any element instantly using its index.
Example: arr[3]
🔹 Memory Efficiency Elements are stored in contiguous memory, which allows
efficient memory usage.
🔹 Ease of Iteration Easy to loop through using loops like for or while .
🔹 Simple Arrays are supported directly in most programming languages
Implementation with simple syntax.
🔹 Efficient for Static Ideal when the number of elements is fixed or predictable.
Data
❌ Disadvantages of Arrays
Disadvantage Description
🔸 Fixed Size Once declared, the size cannot be changed (in
static arrays). This can lead to wasted memory or
overflow.
🔸 Costly Insertion/Deletion Adding or removing elements (especially in the
middle) requires shifting elements → O(n) time.
🔸 No Dynamic Memory Can't grow or shrink at runtime unless using a
Management (in static arrays) dynamic version (like Python lists or C++ vectors).
🔸 Wasted Space (if over- If you overestimate the size needed, unused space
allocated) sits idle.
🔸 Same Data Type All elements must be of the same type in most
languages (e.g., all integers).
🧠 Example:
Imagine an array of size 5:
➕ Steps:
1. Check if the array has space (for static arrays).
2. Shift elements to the right from the insertion point.
3. Insert the new element at the target index.
🧠 Example:
Insert 25 at index 2 in:
🧮 Time Complexity:
Best case (insert at end): O(1)
Worst case (insert at start or middle): O(n)
➖ Steps:
1. Identify the index to delete.
2. Shift all elements to the left, starting from the next index.
3. Optionally, reduce the size (if dynamic).
🧠 Example:
Delete element at index 1 in:
[10, 20, 30, 40]
🧮 Time Complexity:
Best case (delete last element): O(1)
Worst case (delete first or middle): O(n)
🔍 Code Examples
🐍 Python (using list)
Insert:
Delete:
💡 Summary Table
Operation Description Time Complexity
Insert at end Add to last position O(1)
Insert at start/middle Shift elements, insert new one O(n)
Delete from end Remove last element O(1)
Delete from start/middle Shift elements to fill gap O(n)
9. What is the time complexity for insertion, deletion, and access in arrays?
⏱️ Time Complexity in Arrays
Operation Time Complexity Explanation
Access O(1) Direct access via index, e.g., arr[5] .
Insertion
- At end O(1) (if space Just place element at the next free index.
available)
- At beginning or O(n) Need to shift elements to the right to
middle make space.
Deletion
- At end O(1) Remove last element, no shifting needed.
- At beginning or O(n) Need to shift elements to the left to fill the
middle gap.
Why?
Access is O(1) because arrays store elements in contiguous memory, so the address of
any element can be calculated directly.
Insertion/deletion at the end is O(1) for dynamic arrays that have reserved space.
Insertion/deletion elsewhere is O(n) due to shifting elements to maintain the order.
Example:
Example:
matrix = [
[1, 2, 3],
[4, 5, 6]
]
print(matrix[1][2]) # Output: 6
Example (Python-like):
jagged = [
[1, 2],
[3, 4, 5],
[6]
]
print(jagged[1][2]) # Output: 5
🔄 Summary Table
Feature 1D Array 2D Array Jagged Array
Dimensions One Two Two (rows of varying
lengths)
Element arr[i] arr[i][j] arr[i][j]
Access
Feature 1D Array 2D Array Jagged Array
Length of N/A All rows have equal Rows can have different
Rows length lengths
Memory Contiguous Typically contiguous Sub-arrays can be non-
Layout block contiguous
Use Case Simple lists Grids, matrices Variable-length rows,
irregular data
🔗 Linked List
11. What is a linked list?
🔍 Key Features:
Dynamic size: Can grow or shrink at runtime by allocating/ deallocating nodes.
Non-contiguous storage: Nodes can be anywhere in memory, connected via pointers.
Efficient insertions/deletions: Especially at the beginning or middle (no shifting
required).
🧠 Visual Representation:
[Data | Next] -> [Data | Next] -> [Data | Next] -> NULL
For example, a linked list with elements 10 -> 20 -> 30 looks like:
Advantages:
Dynamic size
Easy insertion/deletion without shifting elements
Flexible memory use
Disadvantages:
No random access (must traverse from head to find an element) — O(n) access time
Extra memory needed for pointers
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
implementation
full implementation of a Singly Linked List in Python with basic operations: insertion,
deletion, and traversal.
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
class LinkedList:
def __init__(self):
[Link] = None
# Insert at the end
def append(self, data):
new_node = Node(data)
if not [Link]:
[Link] = new_node
return
last = [Link]
while [Link]:
last = [Link]
[Link] = new_node
# Example usage:
ll = LinkedList()
[Link](10)
[Link](20)
[Link](5)
ll.print_list() # Output: 5 -> 10 -> 20 -> None
[Link](10)
ll.print_list() # Output: 5 -> 20 -> None
Visualization:
Visualization:
NULL <- [Prev | Data | Next] <-> [Prev | Data | Next] <-> [Prev | Data |
Next] -> NULL
Summary Table:
Detecting a cycle in a linked list is a classic problem! The most common and efficient method
is called Floyd’s Cycle-Finding Algorithm (also known as the Tortoise and Hare
algorithm).
Steps:
1. Initialize slow and fast pointers to the head of the list.
2. Move slow by one node and fast by two nodes in each iteration.
3. If fast becomes NULL or [Link] is NULL , the list has no cycle.
4. If slow equals fast at any point, a cycle exists.
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
def has_cycle(head):
slow = head
fast = head
if slow == fast:
return True # Cycle detected
# Example Usage:
# Creating a cycle linked list for testing
head = Node(1)
[Link] = Node(2)
[Link] = Node(3)
[Link] = [Link] # Cycle here
Reversing a linked list is a common operation! The idea is to reverse the direction of the
next pointers so that the last node becomes the head, and the head becomes the last
node.
Step-by-step:
1. Initialize:
prev = None
current = head
2. Iterate while current is not None :
Store next_node = [Link] (save next node)
Reverse the pointer: [Link] = prev
Move prev to current node: prev = current
Move current to next node: current = next_node
3. At the end, prev will be the new head of the reversed list.
Python Code:
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
def reverse_linked_list(head):
prev = None
current = head
while current:
next_node = [Link] # Save next node
[Link] = prev # Reverse pointer
prev = current # Move prev forward
current = next_node # Move current forward
# Example Usage:
# Creating a linked list 1 -> 2 -> 3 -> None
head = Node(1)
[Link] = Node(2)
[Link] = Node(3)
# Reverse it
new_head = reverse_linked_list(head)
Key Operations:
Push: Add an element to the top of the stack.
Pop: Remove the top element from the stack.
Peek/Top: View the top element without removing it.
IsEmpty : Check if the stack is empty.
Visualization:
Top
↑
| 5 | <-- Last pushed, first to pop
| 4 |
| 3 |
| 2 |
| 1 |
Applications of Stack
1. Function Call Management:
The system uses a call stack to keep track of active functions and return addresses.
2. Expression Evaluation and Syntax Parsing:
Converting infix expressions to postfix/prefix .
Evaluating postfix expressions.
3. Backtracking Algorithms:
Maze solving, puzzles (like Sudoku), undo mechanisms.
4. Browser History Navigation:
Keeps track of visited pages to allow going back.
5. Recursive Algorithm Implementation:
Can replace recursion with an explicit stack.
6. Balanced Parentheses Checking:
Verify if every opening bracket has a corresponding closing bracket.
Implementation
class Stack:
def __init__(self):
[Link] = []
def is_empty(self):
return len([Link]) == 0
def pop(self):
if self.is_empty():
raise IndexError("Pop from empty stack")
return [Link]()
def peek(self):
if self.is_empty():
raise IndexError("Peek from empty stack")
return [Link][-1]
def size(self):
return len([Link])
# Example Usage:
stack = Stack()
[Link](10)
[Link](20)
[Link](30)
print([Link]()) # Output: 30
print([Link]()) # Output: 30
print([Link]()) # Output: 20
Explanation:
push: Adds an element to the top.
pop: Removes and returns the top element.
peek: Returns the top element without removing it.
is_empty: Checks if the stack has no elements.
size: Returns number of elements in the stack.
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
class Stack:
def __init__(self):
[Link] = None # Points to the top node of the stack
def is_empty(self):
return [Link] is None
def pop(self):
if self.is_empty():
raise IndexError("Pop from empty stack")
popped_node = [Link]
[Link] = [Link] # Move top to next node
return popped_node.data
def peek(self):
if self.is_empty():
raise IndexError("Peek from empty stack")
return [Link]
def size(self):
count = 0
current = [Link]
while current:
count += 1
current = [Link]
return count
# Example Usage:
stack = Stack()
[Link](10)
[Link](20)
[Link](30)
print([Link]()) # Output: 30
print([Link]()) # Output: 30
print([Link]()) # Output: 20
How it works:
The top pointer always points to the latest added node.
push adds a new node at the front.
pop removes the node at the front.
peek returns the data of the top node without removing it.
What is a Queue?
A queue is a linear data structure that follows the FIFO (First In, First Out) principle. This
means the first element added to the queue is the first one to be removed.
Key Operations:
Enqueue: Add an element to the rear (end) of the queue.
Dequeue: Remove an element from the front of the queue.
Peek/Front: View the front element without removing it.
IsEmpty : Check if the queue is empty.
Visualization:
Front Rear
↓ ↓
[ 10 ] -> [ 20 ] -> [ 30 ] -> [ 40 ]
class Queue:
def __init__(self):
[Link] = []
def is_empty(self):
return len([Link]) == 0
def dequeue(self):
if self.is_empty():
raise IndexError("Dequeue from empty queue")
return [Link](0) # Remove from the front (index 0)
def peek(self):
if self.is_empty():
raise IndexError("Peek from empty queue")
return [Link][0] # Front element
def size(self):
return len([Link])
# Example Usage:
q = Queue()
[Link](10)
[Link](20)
[Link](30)
print([Link]()) # Output: 10
print([Link]()) # Output: 10
print([Link]()) # Output: 20
print(q.is_empty())# Output: False
print([Link]()) # Output: 30
print(q.is_empty())# Output: True
Note:
Using a list’s pop(0) for dequeue is not very efficient (O(n) time) because it shifts all
elements left.
For better performance, use [Link] which provides O(1) time for append
and pop from both ends.
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
class Queue:
def __init__(self):
[Link] = None # Points to the front node
[Link] = None # Points to the rear node
def is_empty(self):
return [Link] is None
def dequeue(self):
if self.is_empty():
raise IndexError("Dequeue from empty queue")
data = [Link]
[Link] = [Link]
if [Link] is None: # Queue became empty
[Link] = None
return data
def peek(self):
if self.is_empty():
raise IndexError("Peek from empty queue")
return [Link]
def size(self):
count = 0
current = [Link]
while current:
count += 1
current = [Link]
return count
# Example Usage:
q = Queue()
[Link](10)
[Link](20)
[Link](30)
print([Link]()) # Output: 10
print([Link]()) # Output: 10
print([Link]()) # Output: 20
print(q.is_empty())# Output: False
print([Link]()) # Output: 30
print(q.is_empty())# Output: True
How it works:
enqueue: Adds an element at the rear.
dequeue: Removes an element from the front.
peek: Returns the front element without removing it.
is_empty: Checks if the queue is empty.
size: Counts the number of elements in the queue.
16. Difference between stack and queue.
Summary:
Stack is about reversing order (LIFO).
Queue is about preserving order (FIFO).
Characteristics:
Fixed-size buffer.
Two pointers:
front: points to the front element.
rear: points to the last element.
When rear reaches the end of the array, it wraps around to 0 if there’s free space.
Empty condition: front == -1 or front == rear + 1 (depending on implementation).
Full condition: (rear + 1) % size == front .
Visualization:
Index: 0 1 2 3 4
Queue: [10, 20, -, -, -]
front -> 0
rear -> 1
Enqueue 30:
Queue: [10, 20, 30, -, -]
front -> 0
rear -> 2
Enqueue 60 (wrap-around):
Queue: [60, -, 30, 40, 50]
front -> 2
rear -> 0
Use Cases:
Buffering data streams.
CPU scheduling.
Handling real-time streaming data.
Network data packets buffering.
class CircularQueue:
def __init__(self, size):
[Link] = size
[Link] = [None] * size
[Link] = -1
[Link] = -1
def is_empty(self):
return [Link] == -1
def is_full(self):
return ([Link] + 1) % [Link] == [Link]
def dequeue(self):
if self.is_empty():
raise IndexError("Queue is empty")
data = [Link][[Link]]
if [Link] == [Link]: # Queue has only one element
[Link] = [Link] = -1
else:
[Link] = ([Link] + 1) % [Link]
return data
def peek(self):
if self.is_empty():
raise IndexError("Queue is empty")
return [Link][[Link]]
def display(self):
if self.is_empty():
print("Queue is empty")
return
print("Queue elements:", end=" ")
i = [Link]
while True:
print([Link][i], end=" ")
if i == [Link]:
break
i = (i + 1) % [Link]
print()
# Example usage:
cq = CircularQueue(5)
[Link](10)
[Link](20)
[Link](30)
[Link](40)
[Link](50)
print([Link]()) # Output: 10
print([Link]()) # Output: 20
[Link](60)
[Link](70)
Explanation:
The element with the highest priority is dequeued before elements with lower priority.
If two elements have the same priority, they are served according to their order in the
queue (depending on implementation).
Key Characteristics:
Insertion (enqueue): Add elements with a priority.
Deletion (dequeue): Remove the element with the highest priority.
Can be implemented using:
Arrays or linked lists with sorting.
Heap data structures (most efficient).
Use Cases:
CPU scheduling (processes with different priorities).
Dijkstra’s shortest path algorithm.
Huffman coding tree.
Managing tasks in real-time systems.
Example:
Element Priority
Task A 2
Task B 1
Task C 3
When dequeuing , Task C (priority 3) will be served first, then Task A (2), then Task B
(1).
simple Python implementation of a priority queue using the
built-in heapq module, which provides an efficient min-
heap:
import heapq
class PriorityQueue:
def __init__(self):
[Link] = []
def is_empty(self):
return len([Link]) == 0
def dequeue(self):
if self.is_empty():
raise IndexError("Dequeue from empty priority queue")
priority, item = [Link]([Link])
return item
def peek(self):
if self.is_empty():
raise IndexError("Peek from empty priority queue")
priority, item = [Link][0]
return item
# Example Usage:
pq = PriorityQueue()
Explanation:
The heapq module uses a min-heap by default, so the smallest priority number is
treated as highest priority.
You insert items as tuples (priority, item) .
Dequeue removes the item with the smallest priority number.
⚙️ Intermediate Level
🌲 Trees
22. What is a binary tree?
A binary tree is a type of data structure in computer science where each node has at most
two children. These children are usually referred to as the left child and the right child.
Visual example:
A
/ \
B C
/ / \
D E F
Binary Tree
Structure: Each node can have at most two children (left and right).
Ordering: No specific order or property about the values of nodes.
Purpose: General-purpose tree structure for hierarchical data.
Example:
10
/ \
5 20
/ \
7 3
10
/ \
5 20
/ \
3 7
Summary Table:
Feature Binary Tree Binary Search Tree (BST)
Max children per 2 2
node
Node ordering No specific order Left < Parent < Right
Purpose General hierarchical Efficient searching & sorting
structure
Search performance O(n) (linear search) O(log n) average, O(n) worst-
case
implementation
simple Python implementation of a Binary Search Tree (BST) with basic operations:
insertion, search, and an in-order traversal to print the values in sorted order.
class Node:
def __init__(self, key):
[Link] = key
[Link] = None
[Link] = None
class BST:
def __init__(self):
[Link] = None
def inorder_traversal(self):
elements = []
self._inorder([Link], elements)
return elements
# Example usage:
bst = BST()
[Link](10)
[Link](5)
[Link](20)
[Link](3)
[Link](7)
Explanation:
Node class: Represents each node in the tree.
BST class: Manages the BST operations.
insert : Adds a new key in the right position to keep the BST property.
search : Checks whether a key exists in the BST.
inorder_traversal : Returns all keys sorted (in-order traversal).
23. What is tree traversal? Explain inorder,
preorder, and postorder traversal .
There are several traversal methods, but the most common for binary trees are:
Inorder
Preorder
Postorder
Use case: In a binary search tree (BST), inorder traversal visits nodes in ascending sorted
order.
Example:
10
/ \
5 20
/ \
3 7
Example:
For the same tree,
Use case: Postorder is useful for deleting the tree or getting a postfix expression of an
expression tree.
Example:
For the same tree,
Summary Table:
class Node:
def __init__(self, key):
[Link] = key
[Link] = None
[Link] = None
def inorder(node):
if node:
inorder([Link]) # Left
print([Link], end=' ') # Root
inorder([Link]) # Right
def preorder(node):
if node:
print([Link], end=' ') # Root
preorder([Link]) # Left
preorder([Link]) # Right
def postorder(node):
if node:
postorder([Link]) # Left
postorder([Link]) # Right
print([Link], end=' ') # Root
print("Inorder traversal:")
inorder(root) # Output: 3 5 7 10 20
print("\nPreorder traversal:")
preorder(root) # Output: 10 5 3 7 20
print("\nPostorder traversal:")
postorder(root) # Output: 3 7 5 20 10
A balanced binary tree is a type of binary tree where the tree is structured to minimize its
height, ensuring that the depths of the left and right subtrees of every node differ by no
more than a certain amount (usually 1).
Common Definition:
For each node in a balanced binary tree:
[
| \text{height(left subtree)} - \text{height(right subtree)} | \leq 1
]
Examples:
Balanced tree:
10
/ \
5 20
/ \
3 7
Unbalanced tree:
10
\
20
\
30
In the unbalanced tree, the right subtree is much deeper than the left, making operations
less efficient.
Checking if a binary tree is a valid Binary Search Tree ( BST ) means verifying whether it
satisfies the BST property:
For every node, all values in its left subtree are less than the node’s value, and all
values in its right subtree are greater than the node’s value.
def is_bst_inorder(root):
inorder_vals = []
def inorder(node):
if node:
inorder([Link])
inorder_vals.append([Link])
inorder([Link])
inorder(root)
# Check if sorted strictly increasing
for i in range(1, len(inorder_vals)):
if inorder_vals[i] <= inorder_vals[i-1]:
return False
return True
def is_bst(root):
return is_bst_util(root, float('-inf'), float('inf'))
Explanation:
For the root, allowed range is (-∞, +∞).
For left child, max allowed value is the parent node's key.
For right child, min allowed value is the parent node's key.
The terms height and depth are related but distinct concepts in tree data structures.
Height of a Tree
The height of a tree is the length of the longest path from the root node down to the
farthest leaf node.
It’s usually measured in edges or nodes, but commonly in edges.
If the tree has only one node (root), the height is 0 (no edges).
Example:
Height of this tree = 2 (from root 10 down to leaf nodes 3 or 7 through two edges).
Depth of a Node
The depth of a node is the number of edges from the root node to that node.
The root node’s depth is 0.
For example, in the above tree:
Depth of node 10 = 0
Depth of node 5 = 1
Depth of node 3 = 2
Summary
An AVL tree is a type of self-balancing binary search tree. It was the first such data
structure invented and is named after its inventors Adelson-Velsky and Landis .
These rotations restructure the tree to restore the balance factor condition.
Summary:
Feature Description
Type Self-balancing binary search tree
Balance condition Balance factor ∈ {-1, 0, 1} for all nodes
Balancing method Rotations (single and double)
Time complexity O(log n) for search, insert, delete
A heap is a special type of binary tree that satisfies the heap property, and it’s most
commonly implemented as a binary heap using an array.
✅ Key Features of a Heap:
Complete binary tree:
All levels are fully filled except possibly the last, which is filled from left to right.
Heap property (determines the type of heap):
Max-Heap: Parent nodes are greater than or equal to their children.
Min-Heap: Parent nodes are less than or equal to their children.
🔁 Types of Heaps:
Type Heap Property Root Contains
Min-Heap Every parent ≤ its children Minimum value
Max-Heap Every parent ≥ its children Maximum value
📦 Example: Min-Heap
2
/ \
4 5
/ \
10 15
Here:
2 ≤ 4 and 5
4 ≤ 10 and 15
🔢 Heap as an Array
Heaps are usually implemented using arrays (not pointers like trees), where:
1
/ \
3 5
/ \
7 9
Max-Heap:
9
/ \
5 6
/ \
3 2
implementation
Python implementation of both a Min-Heap and a Max-Heap using lists and manual
operations.
✅ Min-Heap and Max-Heap (Custom Python
Implementation)
We’ll use a common class and change the comparison logic for min vs. max behavior.
🔧 Base Class
class BinaryHeap:
def __init__(self, is_min_heap=True):
[Link] = []
self.is_min_heap = is_min_heap
def extract(self):
if not [Link]:
return None
root = [Link][0]
last = [Link]()
if [Link]:
[Link][0] = last
self._heapify_down(0)
return root
def peek(self):
return [Link][0] if [Link] else None
if smallest_or_largest != index:
[Link][index], [Link][smallest_or_largest] =
[Link][smallest_or_largest], [Link][index]
self._heapify_down(smallest_or_largest)
def display(self):
print([Link])
🔁 Graphs
31. What is a graph?
Types of Graphs:
1. Undirected Graph:
Edges have no direction.
If there is an edge between node A and node B, it means A is connected to B and B
is connected to A.
2. Directed Graph (Digraph):
Edges have a direction.
An edge from A to B (written A → B) means the connection goes one way, from A to
B.
3. Weighted Graph:
Edges have weights (or costs), often used to represent things like distance, time, or
cost.
4. Unweighted Graph:
All edges are considered equal; no weights are assigned.
Real-world Examples:
Social Networks: People are nodes; friendships or connections are edges.
Maps: Locations are nodes; roads are edges (can be weighted by distance or time).
Internet: Webpages are nodes; hyperlinks are edges.
Visualization:
A graph can be drawn as a diagram with dots (vertices) connected by lines or arrows
(edges), making it easier to understand complex relationships.
31. Difference between directed and undirected graphs.
The key difference between directed and undirected graphs lies in whether the edges
(connections between nodes) have direction or not.
🔁 Undirected Graph
Definition: Edges have no direction.
Connection: If node A is connected to node B, then B is also connected to A.
Edge Representation: As unordered pairs: (A, B)
Use Case Examples:
Friendships in social networks (if A is friends with B, B is friends with A)
Road maps where roads go both ways
Example:
A ----- B
Example:
A → B
Adjacency matrix and adjacency list are two common ways to represent graphs in
computer programs.
📊 1. Adjacency Matrix
✅ Definition:
An adjacency matrix is a 2D array (or matrix) used to represent which vertices (nodes)
are connected to which other vertices.
A --- B
| |
C ----
Nodes: A, B, C → [0, 1, 2]
Adjacency Matrix:
A B C
A [ 0, 1, 1 ]
B [ 1, 0, 1 ]
C [ 1, 1, 0 ]
✅ Pros:
Very fast to check if two nodes are connected → O(1) time.
Easy to implement.
❌ Cons:
Uses O(V²) space even if the graph is sparse (few edges).
Not efficient for graphs with many nodes but few connections.
📋 2. Adjacency List
✅ Definition:
An adjacency list stores each node and a list of its neighbors (connected nodes).
Graph:
A --- B
| |
C ----
Adjacency List:
A: [B, C]
B: [A, C]
C: [A, B]
Or as a dictionary in code:
graph = {
'A': ['B', 'C'],
'B': ['A', 'C'],
'C': ['A', 'B']
}
✅ Pros:
Space-efficient: Uses O(V + E) space (better for sparse graphs).
Easier to iterate over neighbors of a node.
❌ Cons:
Slower to check if an edge exists → O(degree of node).
Less suitable for dense graphs where edge lookups are frequent.
🔍 Summary Table:
Feature Adjacency Matrix Adjacency List
Space Complexity O(V²) O(V + E)
Check Edge Existence O(1) O(k), where k = degree
Iterating Neighbors O(V) O(k)
Best For Dense graphs Sparse graphs
implementation
Basic implementation of adjacency matrix and adjacency list in Python for an undirected
and unweighted graph.
✅ Graph:
Let's take this simple graph as an example:
A --- B
| |
C D
Nodes: A, B, C, D
Edges: A-B, A-C, B-D
🔢 1. Adjacency Matrix Implementation
# Map node names to indices
nodes = ['A', 'B', 'C', 'D']
index_map = {node: i for i, node in enumerate(nodes)}
for u, v in edges:
i, j = index_map[u], index_map[v]
adj_matrix[i][j] = 1
adj_matrix[j][i] = 1 # because it's undirected
🔍 Output:
Adjacency Matrix:
[0, 1, 1, 0]
[1, 0, 0, 1]
[1, 0, 0, 0]
[0, 1, 0, 0]
# Add edges
for u, v in edges:
adj_list[u].append(v)
adj_list[v].append(u) # because it's undirected
🔍 Output:
Adjacency List:
A: ['B', 'C']
B: ['A', 'D']
C: ['A']
D: ['B']
Nodes: A, B, C, D
Edges:
A→B
A→C
B→D
🔍 Output:
Directed Graph - Adjacency Matrix:
[0, 1, 1, 0]
[0, 0, 0, 1]
[0, 0, 0, 0]
[0, 0, 0, 0]
🔍 Output:
Directed Graph - Adjacency List:
A: ['B', 'C']
B: ['D']
C: []
D: []
✅ Summary of Key Differences for Directed Graphs:
Feature Adjacency Matrix Adjacency List
A → B only matrix[A][B] = 1 B is added to A 's list
DFS (Depth-First Search) is a fundamental graph traversal algorithm used to explore nodes
and edges of a graph in depthward motion — meaning it explores as far as possible along
a branch before backtracking.
🧠 Key Concepts:
Feature DFS
Data Structure Stack (explicit or via recursion)
Strategy Go deep before going wide
Use Case Examples Pathfinding, cycle detection, topological sort, maze solving
Works On Both directed and undirected graphs
✅ Example Graph (Undirected):
A
/ \
B C
/ \
D E
Adjacency List:
graph = {
'A': ['B', 'C'],
'B': ['A'],
'C': ['A', 'D', 'E'],
'D': ['C'],
'E': ['C']
}
while stack:
node = [Link]()
if node not in visited:
print(node, end=' ')
[Link](node)
# Add neighbors in reverse to visit leftmost first
[Link](reversed(graph[node]))
# Run DFS
dfs_iterative(graph, 'A')
🧠 Key Concepts:
Feature BFS
Data Structure Queue
Strategy Level-order traversal
Works On Directed and Undirected graphs
Finds Shortest path in unweighted graphs
Type Iterative (not recursive)
Adjacency List:
graph = {
'A': ['B', 'C'],
'B': ['A'],
'C': ['A', 'D', 'E'],
'D': ['C'],
'E': ['C']
}
def has_cycle_undirected(graph):
visited = set()
# Example usage
graph_undirected = {
'A': ['B', 'C'],
'B': ['A', 'D'],
'C': ['A', 'D'],
'D': ['B', 'C']
}
print(has_cycle_undirected(graph_undirected)) # Output: True (cycle
exists)
Explanation:
If a back edge (to an ancestor) is found during DFS, the graph has a cycle.
Python Implementation:
def has_cycle_directed(graph):
visited = set()
rec_stack = set()
def dfs(node):
[Link](node)
rec_stack.add(node)
rec_stack.remove(node)
return False
# Example usage
graph_directed = {
'A': ['B'],
'B': ['C'],
'C': ['A'] # Cycle A->B->C->A
}
Summary:
Dijkstra’s algorithm is a classic algorithm used to find the shortest path from a single
source node to all other nodes in a weighted graph with non-negative edge weights.
🔑 Key Points:
Works only if edge weights are non-negative.
Uses a greedy strategy by always expanding the closest unvisited node.
Can be implemented with a priority queue (efficiently) or a simple list (less efficient).
📋 Example Graph:
(1)
A-------B
|\ |
| \ |(4)
(3) \ |
| \ |
C-------D
(2)
A → B: 1
A → C: 3
B → D: 4
C → D: 2
while priority_queue:
current_distance, current_node = [Link](priority_queue)
# Check neighbors
for neighbor, weight in graph[current_node]:
distance = current_distance + weight
return distances
🔍 Output:
{'A': 0, 'B': 1, 'C': 3, 'D': 5}
⏩ Summary:
Feature Description
Input Weighted graph, source node
Output Shortest path distances
Edge weights Must be non-negative
Time complexity O((V + E) log V) with priority queue
Uses GPS, network routing, game AI
Topological Sort is an algorithm that orders the vertices of a Directed Acyclic Graph
(DAG) in a linear sequence such that for every directed edge u → v , vertex u comes
before vertex v in the ordering.
⚠️ Important:
Works only on Directed Acyclic Graphs (DAGs) — graphs with no cycles.
If there’s a cycle, a topological sort is not possible.
Common Approaches:
1. Kahn’s Algorithm (BFS-based)
Maintain a queue of nodes with zero incoming edges.
Repeatedly remove a node from the queue, add it to the result, and reduce the in-degree
of its neighbors.
If at the end some nodes remain, the graph has a cycle.
def topological_sort(graph):
in_degree = {u: 0 for u in graph}
for u in graph:
for v in graph[u]:
in_degree[v] += 1
while queue:
u = [Link]()
topo_order.append(u)
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
[Link](v)
if len(topo_order) == len(graph):
return topo_order
else:
# Graph has at least one cycle
return None
# Example graph
graph = {
'A': ['C'],
'B': ['C', 'D'],
'C': ['E'],
'D': ['F'],
'E': ['H', 'F'],
'F': ['G'],
'G': [],
'H': []
}
print(topological_sort(graph))
🔍 Possible Output:
['A', 'B', 'D', 'C', 'E', 'H', 'F', 'G']
Summary:
Aspect Description
Input Directed Acyclic Graph (DAG)
Output Linear ordering of nodes
Use case Task scheduling, dependency resolution
Detects cycles? Yes (if cycle exists, returns None)
Algorithms Kahn’s Algorithm, DFS-based
🧮 Hashing
39. What is hashing?
Hashing is a technique used to convert data of arbitrary size into a fixed-size value —
called a hash value or hash code — usually for fast data retrieval, comparison, or indexing.
🔍 What is hashing?
It uses a hash function to map input data (like strings, numbers, objects) to a number
(the hash).
This hash is typically used as an index into a data structure called a hash table (or hash
map).
Hashing helps achieve fast lookup, insertion, and deletion — often close to O(1) time
on average.
Example:
Suppose you want to store student records by their ID.
Important Concepts:
Term Meaning
Hash Function Function converting input to hash code
Hash Table Data structure storing key-value pairs using hashes
Collision When two keys produce the same hash
Collision Resolution Techniques like chaining or open addressing
Load Factor Ratio of number of elements to hash table size
size = 10
key = "apple"
print(f"Hash for '{key}':", simple_hash(key, size))
Hash functions are special functions that take input data (of any size) and produce a fixed-
size string or number — called a hash value or hash code — that uniquely (ideally)
represents the input.
Input: "apple"
Hash value: sum of ASCII codes of letters modulo table size
Want me to:
🔍 What is a collision?
A collision happens when two different keys produce the same hash value (or hash
code).
Since a hash function maps a large set of possible inputs to a limited range of outputs (like
an array index), collisions are inevitable.
1. Chaining
Each slot in the hash table stores a linked list (or other data structure) of entries.
When multiple keys hash to the same index, they are stored in the list at that index.
Lookup involves scanning the list to find the right key.
Example:
2. Open Addressing
If a collision occurs, find another empty slot in the table using a probing sequence.
Common probing methods:
Linear Probing: Check the next slot sequentially (index+1, index+2, ...)
Quadratic Probing: Check slots using quadratic function (index + 1², index + 2², ...)
Double Hashing: Use a second hash function to compute the step size.
Summary Table:
Pros:
Simple to implement.
Good cache performance (due to locality).
Cons:
Can cause primary clustering — long runs of occupied slots, slowing down insertion
and search.
Example:
If collision at index 5, try 6, then 7, etc.
Pros:
Reduces clustering compared to linear probing.
Avoids primary clustering but can still have secondary clustering.
Cons:
More complex probing sequence.
May not always find an empty slot if the table isn’t sized properly.
3️⃣ Chaining
Each slot in the hash table stores a linked list (or another data structure) of all keys that
hash to the same index.
On collision, just append the new key to the list at that slot.
Lookup involves searching the list for the key.
Pros:
Simple and effective.
Table size can remain smaller.
Can handle high load factors gracefully.
Cons:
Uses extra memory for pointers.
Lookup time depends on the length of the chain (can degrade to O(n) if many collisions).
Quick Comparison:
Examples:
Hash Map:
Store student grades: { "Alice": 85, "Bob": 92 }
You can get Bob’s grade by key "Bob" .
Hash Set:
Store list of unique users who visited a website: { "Alice", "Bob", "Charlie" }
You can quickly check if "Alice" has visited.