Back to Portal

๐Ÿง  Ultimate DS Exam Guide

Everything you need โ€” theory, diagrams, code, and viva Q&A โ€”
for your practical exam. Read on the bus, nail it in the hall.


๐Ÿ—บ๏ธ Master Plan

5-hour study battle plan

๐ŸŽฏ DATA STRUCTURES โ€” MASTER EXAM PLAN

Exam: Tomorrow | Time Available: ~5 Hours | Language: Python


---


โฑ๏ธ YOUR 5-HOUR BATTLE PLAN


Hour Focus Files

|------|-------|-------|

Hour 1 (10:00 PM โ€“ 11:00 PM) Unit 1: Big O + Recursion + ADT concepts 01_unit1_foundations.md
Hour 2 (11:00 PM โ€“ 12:00 AM)Unit 2: Linked Lists, Stack, Queue02_unit2_linear_ds.md
Hour 3 (12:00 AM โ€“ 1:00 AM)CODE โ†’ Linked List + Queue + Infix-Postfixprograms/ folder
Hour 4 (1:00 AM โ€“ 2:00 AM)Unit 3: Sorting + CODE Bubble/Merge/Quick03_unit3_sorting.md + programs
Hour 5 (2:00 AM โ€“ 3:00 AM)Unit 4: Trees, BST, Graphs, Hashing + CODE BST04_unit4_nonlinear.md + programs
Final 30 minViva Q&A blast โ€” read fast and memorize key answers05_viva_questions.md

---


๐Ÿ”ฅ TEACHER'S PRIORITY PROGRAMS (Nail These!)


  1. โœ… Linked List โ†’ programs/01_linked_list.py
  2. โœ… Queue โ†’ programs/02_queue.py
  3. โœ… Infix to Postfix using Stack โ†’ programs/03_infix_to_postfix.py
  4. โœ… Bubble, Merge, Quick Sort โ†’ programs/04_sorting.py
  5. โœ… Binary Search Tree โ†’ programs/05_bst.py

---


๐Ÿ“Œ MOST IMPORTANT CONCEPTS (If you only have 30 min)


  1. Big O Notation โ€” O(1), O(log n), O(n), O(n log n), O(nยฒ)
  2. Linked List โ€” singly, insert, delete, traverse
  3. Stack โ€” LIFO, push/pop, infixโ†’postfix conversion
  4. Queue โ€” FIFO, enqueue/dequeue
  5. Bubble Sort โ€” O(nยฒ), stable
  6. Merge Sort โ€” O(n log n), divide & conquer, stable
  7. Quick Sort โ€” O(n log n) avg, pivot, in-place
  8. BST โ€” left < root < right, insert/search/delete
  9. BFS vs DFS โ€” queue vs stack
  10. Hashing โ€” hash function, collision resolution

---


๐Ÿ’ก VIVA GOLDEN RULES



---


Files in this folder:

final-material/
โ”œโ”€โ”€ 00_MASTER_PLAN.md          โ† YOU ARE HERE
โ”œโ”€โ”€ 01_unit1_foundations.md    โ† Big O, Recursion, ADT
โ”œโ”€โ”€ 02_unit2_linear_ds.md      โ† Arrays, LL, Stack, Queue
โ”œโ”€โ”€ 03_unit3_sorting.md        โ† All sorting algorithms
โ”œโ”€โ”€ 04_unit4_nonlinear.md      โ† Trees, Graphs, Hashing
โ”œโ”€โ”€ 05_viva_questions.md       โ† Q&A for exam viva
โ”œโ”€โ”€ CHEATSHEET.md              โ† Last-minute 1-page reference
โ””โ”€โ”€ programs/
    โ”œโ”€โ”€ 01_linked_list.py
    โ”œโ”€โ”€ 02_queue.py
    โ”œโ”€โ”€ 03_infix_to_postfix.py
    โ”œโ”€โ”€ 04_sorting.py
    โ””โ”€โ”€ 05_bst.py


๐Ÿ“ Unit 1 โ€” Foundations

Big O, Recursion, ADT

๐Ÿ“˜ UNIT 1 โ€” Foundations & Algorithmic Analysis

Target: Read in 60 minutes | CO1

---


1.1 Data Structures & ADTs


What is a Data Structure?

A Data Structure is a way of organizing and storing data in memory so that it can be accessed and modified efficiently.


Two types:


What is an ADT (Abstract Data Type)?

An ADT is a logical description of how data is organized and what operations can be performed โ€” WITHOUT worrying about implementation details.


ADT = Data + Operations (no implementation detail)
Example: Stack ADT โ†’ push(), pop(), peek(), isEmpty()
         You don't care if it's implemented via array or linked list

Why ADTs? โ†’ Separation of concerns. The user of the ADT doesn't need to know HOW it works.


Common Operations on Data Structures:

Operation Description

|-----------|-------------|

Traversal Visit every element once
InsertionAdd a new element
DeletionRemove an element
SearchingFind an element
SortingArrange elements in order
MergingCombine two structures

---


1.2 Algorithm Analysis โ€” The BIG PICTURE


"Before coding, ask: HOW LONG will this take? HOW MUCH MEMORY?"

Time Complexity

How the runtime grows as input size n increases.


Space Complexity

How memory usage grows as input size n increases.


---


1.3 Big O Notation โ€” MASTER THIS!


Big O describes the upper bound (worst case).


O(f(n)) = algorithm's growth rate

The Complexity Ladder (Best โ†’ Worst):


Notation Name Example

|----------|------|---------|

O(1) Constant Array access arr[i], hash lookup
O(log n)LogarithmicBinary search
O(n)LinearLinear search, single loop
O(n log n)LinearithmicMerge sort, Quick sort (avg)
O(nยฒ)QuadraticBubble sort, nested loops
O(nยณ)CubicTriple nested loops
O(2โฟ)ExponentialFibonacci (naive recursive)
O(n!)FactorialPermutations, traveling salesman brute force

Visual Memory Trick:

1 < log n < n < n log n < nยฒ < nยณ < 2โฟ < n!
FAST โ†โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ†’ SLOW

Big Omega (ฮฉ) โ€” Lower Bound (Best Case)

ฮฉ(n) = best-case scenario
Example: Linear search finds element at index 0 โ†’ ฮฉ(1)

Big Theta (ฮ˜) โ€” Tight Bound (Average Case)

ฮ˜(n) = both upper AND lower bound match
Example: Linear search in average case โ†’ ฮ˜(n)

Simplification Rules:

# Rule 1: Drop constants
O(2n) โ†’ O(n)
O(100) โ†’ O(1)

# Rule 2: Drop lower order terms
O(nยฒ + n) โ†’ O(nยฒ)
O(n + log n) โ†’ O(n)

# Rule 3: Different inputs = different variables
def func(a, b):        # O(a + b), NOT O(n)
    for x in a: ...
    for y in b: ...

Examples to Know:


# O(1) โ€” constant
def get_first(arr):
    return arr[0]

# O(n) โ€” linear
def find(arr, target):
    for item in arr:
        if item == target:
            return True

# O(nยฒ) โ€” quadratic
def bubble(arr):
    for i in range(len(arr)):
        for j in range(len(arr)-1):  # nested loop!
            ...

# O(log n) โ€” binary search
def binary_search(arr, target):
    lo, hi = 0, len(arr)-1
    while lo <= hi:
        mid = (lo + hi) // 2
        if arr[mid] == target: return mid
        elif arr[mid] < target: lo = mid + 1
        else: hi = mid - 1

---


1.4 Cases: Best / Average / Worst


Case Meaning Example (Linear Search)

|------|---------|------------------------|

Best Most favorable input Element is at index 0 โ†’ O(1)
AverageTypical/random inputElement is in the middle โ†’ O(n/2) = O(n)
WorstLeast favorable inputElement not in list โ†’ O(n)

---


1.5 Algorithm Design Paradigms


1. Divide & Conquer

Split problem โ†’ Solve subproblems recursively โ†’ Combine results
Examples: Merge Sort, Quick Sort, Binary Search

2. Greedy

At each step, pick the locally optimal choice
Hope: local optima โ†’ global optimum
Examples: Dijkstra's shortest path, Huffman encoding
Note: Doesn't always give the BEST answer globally

3. Dynamic Programming (DP)

Break into overlapping subproblems โ†’ Store results (memoization)
Avoid recomputing the same subproblems
Examples: Fibonacci, Knapsack, Longest Common Subsequence

Paradigm Key Idea Examples

|----------|----------|----------|

Divide & Conquer Split, solve, merge Merge Sort, Quick Sort
GreedyBest local choiceDijkstra, Huffman
Dynamic ProgrammingMemoize overlapping subproblemsFibonacci, Knapsack

---


1.6 Recursion


What is Recursion?

A function that calls itself to solve a smaller version of the same problem.


Two Essential Parts:

  1. Base Case โ€” The stopping condition (prevents infinite loop)
  2. Recursive Case โ€” The function calling itself with a smaller input

# Classic: Factorial
def factorial(n):
    if n == 0:          # BASE CASE
        return 1
    return n * factorial(n - 1)  # RECURSIVE CASE

# Classic: Fibonacci
def fib(n):
    if n <= 1:          # BASE CASE
        return n
    return fib(n-1) + fib(n-2)  # RECURSIVE CASE

The Call Stack

factorial(4)
  โ†’ 4 * factorial(3)
       โ†’ 3 * factorial(2)
            โ†’ 2 * factorial(1)
                 โ†’ 1 * factorial(0)
                      โ†’ returns 1

Each function call is pushed onto the call stack and popped when it returns.


โš ๏ธ Stack Overflow = too many recursive calls, call stack runs out of memory!

Recursion vs Iteration

Recursion Iteration

|-|-----------|-----------|

Memory O(n) call stack O(1) usually
ReadabilityCleaner for trees/graphsBetter for simple loops
RiskStack overflowNone

---


๐ŸŽฏ UNIT 1 VIVA QUICK-FIRE


Q: What is Big O?

A: Upper bound of an algorithm's time complexity โ€” worst-case growth rate.


Q: What's O(log n)?

A: Binary search โ€” each step halves the input.


Q: Difference between O, ฮฉ, ฮ˜?

A: O = worst case upper bound, ฮฉ = best case lower bound, ฮ˜ = tight/average bound.


Q: What is an ADT?

A: A mathematical model of a data type with defined operations but no implementation details.


Q: What is the base case in recursion?

A: The condition that stops recursion. Without it, you get infinite recursion / stack overflow.


Q: What is Divide & Conquer?

A: Break problem into smaller subproblems, solve recursively, then combine results. Example: Merge Sort.



๐Ÿ”— Unit 2 โ€” Linear DS

Arrays, Linked Lists, Stack, Queue

Unit 2 โ€” Linear Data Structures


Arrays


What is an Array? A collection of elements stored in contiguous (side-by-side) memory locations. All elements must be the same type.


Types:


Complexity:


Operation Time Why

|-----------|------|-----|

Access arr[i] O(1) Direct index calculation
Search (unsorted)O(n)Must check each element
Insert at endO(1) amortizedPython list append
Insert at middleO(n)Must shift elements right
DeleteO(n)Must shift elements left

Array vs Linked List:


Array Linked List

|-|-------|-------------|

Memory Contiguous Scattered
AccessO(1) randomO(n) sequential
Insert/DeleteO(n) โ€” shiftO(1) at known node
SizeFixed/DynamicAlways Dynamic
Cache friendlyYESNO

---


Linked Lists โญ EXAM PRIORITY


What is it? A sequence of Nodes where each node has DATA + a NEXT pointer to the next node. Nodes are scattered in memory, connected by pointers.


HEAD โ†’ [10|โ†’] โ†’ [20|โ†’] โ†’ [30|None]

The Node class (memorize this):

class Node:
    def __init__(self, data):
        self.data = data   # stores value
        self.next = None   # points to next node

Singly Linked List

Each node points FORWARD only. HEAD โ†’ A โ†’ B โ†’ C โ†’ None


Doubly Linked List

Each node has BOTH prev and next pointers.

None โ† A โ†” B โ†” C โ†’ None

Use case: Browser history (back AND forward navigation)


Circular Linked List

Last node's next points BACK to HEAD. No None at end.

HEAD โ†’ A โ†’ B โ†’ C โ†’ (back to HEAD)

Use case: Round-robin scheduling, music playlist loop


Insert at HEAD โ€” O(1):

new_node.next = self.head   # point new node to old head
self.head = new_node         # head now points to new node

Traversal โ€” O(n):

current = self.head
while current:
    print(current.data)
    current = current.next

Delete by value โ€” O(n):

# Special case: deleting head
if self.head.data == target:
    self.head = self.head.next
    return
# General case: find node BEFORE target
current = self.head
while current.next:
    if current.next.data == target:
        current.next = current.next.next  # skip target
        return
    current = current.next

Complexity Summary:


Operation Time

|-----------|------|

Insert at head O(1)
Insert at tailO(n) โ€” walk to end first
Delete (known position)O(1)
Delete (by value)O(n) โ€” search first
SearchO(n)
TraverseO(n)

Real World Uses:


---


Stacks (LIFO) โญ EXAM PRIORITY


What is it? Last In, First Out. Like a stack of plates โ€” you add and remove from the TOP only.


     โ”Œโ”€โ”€โ”€โ”
     โ”‚ C โ”‚  โ† TOP (last added, first removed)
     โ”œโ”€โ”€โ”€โ”ค
     โ”‚ B โ”‚
     โ”œโ”€โ”€โ”€โ”ค
     โ”‚ A โ”‚  โ† BOTTOM (first added, last removed)
     โ””โ”€โ”€โ”€โ”˜

ADT Operations:


Operation Description Complexity

|-----------|-------------|------------|

push(x) Add to top O(1)
pop()Remove from topO(1)
peek()View top (don't remove)O(1)
isEmpty()Check if emptyO(1)

Python Implementation:

stack = []
stack.append(10)    # push
stack.append(20)
top = stack[-1]     # peek
stack.pop()         # pop โ†’ removes 20

Real World Uses:


---


Queues (FIFO) โญ EXAM PRIORITY


What is it? First In, First Out. Like a bank line โ€” people join at REAR and leave from FRONT.


FRONT โ†’ [A] [B] [C] [D] โ† REAR
dequeue โ†‘                  โ†‘ enqueue

ADT Operations:


Operation Description Complexity

|-----------|-------------|------------|

enqueue(x) Add to rear O(1)
dequeue()Remove from frontO(1)
peek()View front elementO(1)
isEmpty()Check if emptyO(1)

Python Implementation (use deque!):

from collections import deque
q = deque()
q.append(10)      # enqueue โ€” add to rear
q.append(20)
q.popleft()       # dequeue โ€” remove from front โ†’ returns 10
print(q[0])       # peek front

Why deque and not list? Because list.pop(0) is O(n) โ€” it shifts all elements. deque.popleft() is O(1). Always use deque for queues!

Types of Queues:


Real World Uses:


---


Stack vs Queue โ€” KEY COMPARISON


Feature Stack Queue

|---------|-------|-------|

Full Name LIFO FIFO
Add operationpush (top)enqueue (rear)
Remove operationpop (top)dequeue (front)
Access endsOne end onlyTwo ends
Algorithm useDFS, backtrackingBFS, scheduling
Real worldUndo, call stackPrinter, bank line

Viva Quick-Fire


Q: What is a Linked List?

A: A data structure where nodes are stored in non-contiguous memory. Each node contains data and a pointer (next) to the next node.


Q: Difference between Array and Linked List?

A: Arrays use contiguous memory with O(1) random access but O(n) insert/delete. Linked Lists use scattered memory with O(n) access but O(1) insert/delete at a known node.


Q: What is LIFO?

A: Last In, First Out. Stack uses LIFO โ€” the last element pushed is the first to be popped.


Q: What is FIFO?

A: First In, First Out. Queue uses FIFO โ€” the first element enqueued is the first to be dequeued.


Q: What is the difference between Stack and Queue?

A: Stack is LIFO (one end โ€” top for both push and pop). Queue is FIFO (two ends โ€” rear for enqueue, front for dequeue).


Q: What is a Circular Linked List?

A: A linked list where the last node's next pointer points back to the head instead of None. Used in round-robin scheduling.


Q: What is a Doubly Linked List?

A: Each node has two pointers โ€” next and prev. Allows traversal in both directions. Used in browser history.


Q: What is peek() in Stack?

A: Returns the top element without removing it from the stack. O(1) operation.


Q: Why use deque for Queue in Python?

A: list.pop(0) is O(n) because it shifts all elements. deque.popleft() is O(1) โ€” no shifting needed.


Q: What is a Priority Queue?

A: A queue where elements have priorities. The element with highest priority is dequeued first regardless of insertion order. Implemented using a Heap.



๐Ÿ”€ Unit 3 โ€” Sorting

Bubble, Merge, Quick, Heap

Unit 3 โ€” Sorting Algorithms


Key Terminology


Term Meaning Examples

|------|---------|---------|

Stable Equal elements keep their original relative order Bubble, Merge, Insertion
UnstableEqual elements may swap positionsQuick, Heap, Selection
In-placeUses O(1) extra spaceBubble, Selection, Insertion, Quick, Heap
Out-of-placeNeeds extra memoryMerge Sort โ€” O(n) extra
Comparison sortCompares element pairsBubble, Merge, Quick, Heap
Non-comparisonUses digit/count valuesCounting Sort, Radix Sort

---


Bubble Sort โญ EXAM PRIORITY


Idea: Compare adjacent elements. If left > right, swap them. The largest element "bubbles up" to its correct position after each pass.


Step-by-step with [64, 34, 25, 12]:

Pass 1: compare (64,34) โ†’ swap โ†’ compare (64,25) โ†’ swap โ†’ compare (64,12) โ†’ swap
        [34, 25, 12, 64]   โ† 64 is in final position!
Pass 2: [25, 12, 34, 64]
Pass 3: [12, 25, 34, 64]   โ† sorted!

The Code (memorize this pattern):

def bubble_sort(arr):
    n = len(arr)
    for i in range(n - 1):           # n-1 passes
        swapped = False
        for j in range(n - 1 - i):   # -i because last i are sorted
            if arr[j] > arr[j + 1]:  # wrong order?
                arr[j], arr[j+1] = arr[j+1], arr[j]  # swap!
                swapped = True
        if not swapped:               # already sorted โ€” optimization
            break
    return arr

Case Time Space

|------|------|-------|

Best (sorted array) O(n) O(1)
AverageO(nยฒ)O(1)
Worst (reverse sorted)O(nยฒ)O(1)

Stable: YES | In-place: YES


---


Insertion Sort


Idea: Like sorting playing cards in your hand. Pick one card, slide it into the correct position in your already-sorted hand.


[5, 2, 4, 6, 1]
Take 2: insert before 5 โ†’ [2, 5, 4, 6, 1]
Take 4: insert between 2 and 5 โ†’ [2, 4, 5, 6, 1]
Take 1: insert at start โ†’ [1, 2, 4, 5, 6]

Best case O(n) โ€” great for nearly-sorted data!

Stable: YES | In-place: YES


---


Selection Sort


Idea: Find the minimum in the unsorted portion, place it at the start. Repeat.


O(nยฒ) always. Unstable. In-place.


---


Merge Sort โญ EXAM PRIORITY


Idea: Divide & Conquer. Keep splitting array in half until you have individual elements (already sorted). Then merge sorted halves back together.


Trace with [38, 27, 43, 3]:

DIVIDE:
[38, 27, 43, 3]
  [38, 27]    [43, 3]
  [38] [27]   [43] [3]

MERGE (each pair in sorted order):
  [27, 38]    [3, 43]
[3, 27, 38, 43]  โ† done!

The TWO functions (learn separately!):

# Function 1: SPLIT (just divides recursively)
def merge_sort(arr):
    if len(arr) <= 1:        # BASE CASE: single element is sorted
        return arr
    mid = len(arr) // 2
    left  = merge_sort(arr[:mid])   # sort left half
    right = merge_sort(arr[mid:])   # sort right half
    return merge(left, right)       # merge sorted halves

# Function 2: MERGE (the actual work happens here)
def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:   # <= makes it STABLE
            result.append(left[i]); i += 1
        else:
            result.append(right[j]); j += 1
    result.extend(left[i:])    # add remaining
    result.extend(right[j:])
    return result

Case Time Space

|------|------|-------|

Best O(n log n) O(n)
AverageO(n log n)O(n)
WorstO(n log n)O(n)

Stable: YES | In-place: NO โ€” needs O(n) extra memory


Why O(n log n)? โ†’ log n levels of splitting ร— O(n) work to merge each level = O(n log n)

---


Quick Sort โญ EXAM PRIORITY


Idea: Choose a PIVOT element. Partition the array so all elements < pivot go LEFT, all > pivot go RIGHT. Pivot is now in its FINAL correct position! Recursively sort left and right sides.


Trace with [10, 7, 8, 9, 1, 5], pivot = 5 (last element):

Walk left to right, put elements <= 5 on left side:
1 โ‰ค 5 โ†’ left side
Result: [1, 5, 8, 9, 7, 10]
         โ†‘ โ† 5 is in final position! โ†’ โ†‘
Sort [1] and [8, 9, 7, 10] separately โ†’ done!

The Partition function (memorize this):

def partition(arr, low, high):
    pivot = arr[high]     # choose last element as pivot
    i = low - 1           # i = boundary of smaller elements

    for j in range(low, high):
        if arr[j] <= pivot:         # if current <= pivot
            i += 1                  # expand smaller boundary
            arr[i], arr[j] = arr[j], arr[i]  # swap into smaller zone

    # Place pivot in its correct final position
    arr[i+1], arr[high] = arr[high], arr[i+1]
    return i + 1   # return pivot's final index

def quick_sort(arr, low, high):
    if low < high:
        pivot_idx = partition(arr, low, high)
        quick_sort(arr, low, pivot_idx - 1)   # sort left
        quick_sort(arr, pivot_idx + 1, high)  # sort right

Case Time Space

|------|------|-------|

Best O(n log n) O(log n)
AverageO(n log n)O(log n)
Worst (sorted input)O(nยฒ)O(n)

Stable: NO | In-place: YES | Fastest in practice


Worst case happens when pivot is always min or max (e.g., sorted array). Fix: random pivot selection.

---


Heap Sort


Build a Max-Heap from array โ†’ repeatedly extract the max and place at end.


O(n log n) always | O(1) space | Unstable | In-place


---


Non-Comparison Sorts (Conceptual)


Counting Sort: Count frequency of each element. Works only for integers in a known range [0, k]. Time: O(n+k). Stable.


Radix Sort: Sort digit by digit (least to most significant). Uses Counting Sort internally. Time: O(d ร— (n+k)).


---


THE MASTER TABLE โ€” Memorize This!


Algorithm Best Average Worst Space Stable In-place

|-----------|------|---------|-------|-------|--------|----------|

Bubble O(n) O(nยฒ) O(nยฒ) O(1) YES YES
SelectionO(nยฒ)O(nยฒ)O(nยฒ)O(1)NOYES
InsertionO(n)O(nยฒ)O(nยฒ)O(1)YESYES
MergeO(n log n)O(n log n)O(n log n)O(n)YESNO
QuickO(n log n)O(n log n)O(nยฒ)O(log n)NOYES
HeapO(n log n)O(n log n)O(n log n)O(1)NOYES
CountingO(n+k)O(n+k)O(n+k)O(k)YESNO

---


Viva Quick-Fire


Q: What is a stable sort?

A: A sort where equal elements maintain their original relative order. Bubble, Merge, Insertion are stable.


Q: Which sort has guaranteed O(n log n) in ALL cases?

A: Merge Sort and Heap Sort.


Q: Why is Merge Sort O(n) space?

A: It needs an auxiliary array to merge two sorted halves โ€” cannot merge in-place.


Q: When is Quick Sort worst case O(nยฒ)?

A: When the pivot is always the smallest or largest element โ€” happens with sorted/reverse sorted arrays and bad pivot choice.


Q: How does Bubble Sort work?

A: Repeatedly compares adjacent elements and swaps if arr[j] > arr[j+1]. After each pass, the largest unsorted element reaches its correct position at the end.


Q: What is a pivot in Quick Sort?

A: An element used to partition the array. All elements smaller than pivot go left, all larger go right. The pivot ends up in its final sorted position.


Q: Which sort is fastest in practice?

A: Quick Sort โ€” excellent cache performance and low constant factors despite O(nยฒ) worst case.


Q: Best sort for nearly-sorted data?

A: Insertion Sort โ€” best case O(n).


Q: Best sort for linked lists?

A: Merge Sort โ€” doesn't require random access.



๐ŸŒณ Unit 4 โ€” Non-Linear DS

Trees, Graphs, Hashing, Tries

Unit 4 โ€” Non-Linear Data Structures


Trees


What is a Tree? A hierarchical data structure consisting of nodes connected by edges. It represents a parent-child relationship.


Terminology:


Binary Tree

A tree where each node has AT MOST 2 children (Left and Right).


Binary Search Tree (BST) โญ EXAM PRIORITY

A Binary Tree with a strict rule:

  1. All nodes in LEFT subtree are < (smaller than) the root.
  2. All nodes in RIGHT subtree are > (greater than) the root.

Why use BST? It allows for extremely fast searching. Each comparison cuts the search space in half.


Tree Traversals (Printing the tree)

Traversing means visiting every node exactly once.


  1. Pre-order (Root, Left, Right):

- Used to create a copy of the tree.

- Example: 5, 3, 2, 4, 7, 6, 8


  1. In-order (Left, Root, Right): โญ

- CRITICAL: Inorder traversal of a BST always prints elements in SORTED (ascending) order!

- Example: 2, 3, 4, 5, 6, 7, 8


  1. Post-order (Left, Right, Root):

- Used to delete the tree (delete children before parent).

- Example: 2, 4, 3, 6, 8, 7, 5


---


Graphs


What is a Graph? A collection of Vertices (Nodes) and Edges (connections between them). Trees are a special type of graph (a graph with no cycles).


Types:


Representations:

  1. Adjacency Matrix: 2D array. matrix[i][j] = 1 if edge exists.

- Space: O(Vยฒ)

- Best for: Dense graphs (many edges).

  1. Adjacency List: Array of lists. list[i] contains all neighbors of vertex i.

- Space: O(V + E)

- Best for: Sparse graphs (few edges). Most commonly used!


Graph Traversals


1. Breadth-First Search (BFS)


2. Depth-First Search (DFS)


---


Hashing


What is Hashing? A technique to map a large range of key values to a smaller range of array indices using a Hash Function.


Why? It allows for O(1) average time complexity for Search, Insert, and Delete!


Hash Table

An array that stores the values based on the index calculated by the Hash Function.


Hash Collision

What happens when the hash function maps two different keys to the SAME index? This is a collision.


Collision Resolution Techniques:


  1. Chaining (Open Hashing):

- Each slot in the array is a Linked List.

- If collision occurs, just append the new element to the linked list at that index.


  1. Open Addressing (Closed Hashing):

- All elements are stored in the array itself. If a collision happens, find the next empty slot.

- Linear Probing: Check next slot (index + 1), then next, until empty found. (Leads to clustering).

- Quadratic Probing: Check index + 1ยฒ, index + 2ยฒ, index + 3ยฒ, etc.

- Double Hashing: Use a second hash function to determine the step size.


---


Tries (Prefix Tree)


What is a Trie? A tree-like structure used for storing strings.


---


Viva Quick-Fire


Q: Difference between Tree and Graph?

A: A Tree is a hierarchical structure with a root and no cycles. A Graph is a network structure that can have cycles and disconnected components.


Q: Why use a Binary Search Tree (BST)?

A: Because it provides fast search, insertion, and deletion operations (O(log n) on average) by maintaining elements in a sorted structure.


Q: What is the worst-case time complexity of a BST?

A: O(n). This happens if the tree becomes skewed (e.g., inserting elements that are already sorted), turning it essentially into a linked list.


Q: Which traversal of a BST gives a sorted sequence?

A: In-order traversal (Left, Root, Right).


Q: What data structure does BFS use?

A: Queue.


Q: What data structure does DFS use?

A: Stack (or recursion, which uses the call stack).


Q: What is a Hash Collision?

A: When two distinct keys are mapped to the exact same index in a hash table by the hash function.


Q: Name two ways to handle Hash Collisions.

A: Chaining (using linked lists at each index) and Open Addressing (like Linear Probing, finding the next available slot).


Q: What is the main advantage of a Trie?

A: It provides extremely fast string searching based on prefixes, making it ideal for autocomplete features. Time complexity is proportional to word length, not dictionary size.



๐ŸŽค Master Viva Q&A

61 Questions with Gold-Standard Answers

Master Viva Q&A โ€” 61 Questions


This section contains rapid-fire questions covering all 4 units, specifically tailored for what an examiner will ask during your practical/viva.


General & Unit 1


Q1: What is a Data Structure?

A: A specialized format for organizing, processing, retrieving and storing data in memory.


Q2: What is an Algorithm?

A: A step-by-step procedure or set of rules to solve a specific problem.


Q3: What is Time Complexity?

A: The amount of time an algorithm takes to run, expressed as a function of the input size (n).


Q4: What is Space Complexity?

A: The amount of extra memory an algorithm requires to run, expressed as a function of input size (n).


Q5: What is Big O Notation?

A: It represents the UPPER BOUND or WORST-CASE scenario of an algorithm's time or space complexity.


Q6: What is Big Omega (ฮฉ) and Big Theta (ฮ˜)?

A: Omega is the lower bound (best case). Theta is the exact bound (average case).


Q7: Rank time complexities from fastest to slowest.

A: O(1) > O(log n) > O(n) > O(n log n) > O(nยฒ) > O(2^n) > O(n!)


Q8: What is an ADT (Abstract Data Type)?

A: A theoretical model that specifies WHAT operations can be performed on a data structure, but not HOW they are implemented (e.g., List, Stack, Queue).


Q9: What is Recursion?

A: When a function calls itself to solve a smaller instance of the same problem.


Q10: What is a Base Case in recursion?

A: The condition that stops the recursive calls, preventing an infinite loop and stack overflow.


---


Unit 2 โ€” Linear Data Structures


Q11: Difference between Array and Linked List?

A: Arrays use contiguous memory, sizes are fixed, access is O(1). Linked lists use non-contiguous memory connected by pointers, size is dynamic, access is O(n).


Q12: What is a Stack?

A: A linear data structure following LIFO (Last In First Out). Insertion (push) and deletion (pop) happen at the same end (top).


Q13: Name an application of Stack.

A: Undo mechanism in text editors, browser history, recursive function call stack, evaluating postfix expressions.


Q14: What is a Queue?

A: A linear data structure following FIFO (First In First Out). Insertion (enqueue) at the rear, deletion (dequeue) at the front.


Q15: Name an application of Queue.

A: Print queue, CPU task scheduling, breadth-first search in graphs.


Q16: Why use deque from collections for Queues in Python instead of lists?

A: In a normal Python list, pop(0) takes O(n) time because it shifts all elements. deque.popleft() takes O(1) time.


Q17: What is a Circular Queue?

A: A queue where the last position is connected back to the first position. It solves the problem of wasted space in standard array-based queues.


Q18: What is a Priority Queue?

A: A queue where each element has a priority. Elements with higher priority are dequeued before elements with lower priority, regardless of insertion order.


Q19: What is a Doubly Linked List?

A: A linked list where each node contains two pointers: one pointing to the next node and one pointing to the previous node.


Q20: Advantage of Doubly Linked List?

A: You can traverse the list in both forward and backward directions. Deletion is easier if you have a pointer to the node to be deleted.


Q21: What is a Circular Linked List?

A: A linked list where the last node points back to the head node instead of None.


Q22: How do you convert Infix to Postfix?

A: Use a stack for operators. Operands go directly to output. Operators are pushed to the stack based on precedence rules. Brackets control the flow.


Q23: Why do computers prefer Postfix notation?

A: Postfix eliminates the need for parentheses and precedence rules (BODMAS). It can be easily evaluated using a single stack.


Q24: What is the time complexity of inserting at the head of a Linked List?

A: O(1).


Q25: What is the time complexity of accessing the nth element in a Linked List?

A: O(n), because you must traverse from the head.


---


Unit 3 โ€” Sorting


Q26: What is a stable sort?

A: A sorting algorithm that preserves the original relative order of equal elements.


Q27: Name some stable sorts.

A: Bubble Sort, Insertion Sort, Merge Sort.


Q28: What is an in-place sort?

A: An algorithm that requires a small, constant amount of extra memory space O(1) to sort the data.


Q29: Name an out-of-place sort.

A: Merge Sort. It requires O(n) auxiliary space to merge the arrays.


Q30: How does Bubble Sort work?

A: It repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. The largest elements "bubble" to the end.


Q31: Time complexity of Bubble Sort?

A: Worst and Average: O(nยฒ). Best (if already sorted and optimized with a flag): O(n).


Q32: How does Merge Sort work?

A: Divide the array in half recursively until sub-arrays are size 1. Then repeatedly merge the sub-arrays to produce new sorted sub-arrays until one sorted array remains.


Q33: Time complexity of Merge Sort?

A: O(n log n) in Best, Average, and Worst cases.


Q34: How does Quick Sort work?

A: Pick a pivot element. Partition the array so elements smaller than the pivot are on the left, and larger elements are on the right. Recursively apply this to the left and right sub-arrays.


Q35: Time complexity of Quick Sort?

A: Average/Best: O(n log n). Worst: O(nยฒ) (happens when the array is already sorted and the pivot is chosen poorly).


Q36: Which is faster in practice: Merge Sort or Quick Sort?

A: Quick Sort is generally faster in practice due to better cache locality and smaller constant factors, even though its worst-case is O(nยฒ).


Q37: Best sorting algorithm for linked lists?

A: Merge Sort, because it does not require random access to elements.


Q38: What is Insertion Sort best used for?

A: Small datasets or arrays that are already mostly sorted.


Q39: How does Selection Sort work?

A: Repeatedly finds the minimum element from the unsorted part and puts it at the beginning.


Q40: Is Quick Sort stable?

A: No, Quick Sort is typically unstable.


---


Unit 4 โ€” Non-Linear Data Structures


Q41: What is a Tree?

A: A hierarchical non-linear data structure consisting of nodes connected by edges, with a single root.


Q42: What is a Binary Tree?

A: A tree where every node has at most two children (left and right).


Q43: What is a Binary Search Tree (BST)?

A: A binary tree where for every node, all elements in the left subtree are smaller, and all elements in the right subtree are larger.


Q44: Time complexity of searching in a BST?

A: Average: O(log n). Worst: O(n) (if the tree becomes skewed like a linked list).


Q45: Name the three depth-first tree traversals.

A: In-order (Left, Root, Right), Pre-order (Root, Left, Right), Post-order (Left, Right, Root).


Q46: Which traversal gives elements in sorted order for a BST?

A: In-order traversal.


Q47: What is the height of a tree?

A: The length of the longest path from the root to a leaf node.


Q48: What is a Graph?

A: A non-linear data structure consisting of a set of vertices (nodes) and a set of edges connecting them.


Q49: Difference between Directed and Undirected Graph?

A: In a directed graph, edges have a direction (one-way). In an undirected graph, edges have no direction (two-way).


Q50: How do you represent a Graph in memory?

A: Adjacency Matrix (2D array) or Adjacency List (array of linked lists).


Q51: When to use Adjacency Matrix vs Adjacency List?

A: Use Matrix for dense graphs (many edges). Use List for sparse graphs (few edges) to save memory.


Q52: What is Breadth-First Search (BFS)?

A: A graph traversal algorithm that explores the neighbor nodes first, level by level. It uses a Queue.


Q53: What is Depth-First Search (DFS)?

A: A graph traversal algorithm that explores as far as possible along each branch before backtracking. It uses a Stack (or recursion).


Q54: What is Hashing?

A: A technique to convert a range of key values into a range of indexes of an array using a hash function.


Q55: What is the main benefit of a Hash Table?

A: Average O(1) time complexity for search, insertion, and deletion.


Q56: What is a Hash Collision?

A: When a hash function generates the same index for more than one key.


Q57: How do you resolve Hash Collisions?

A: Chaining (using linked lists at each index) or Open Addressing (finding the next empty slot using linear or quadratic probing).


Q58: What is a Trie?

A: A tree data structure used for efficient retrieval of a key in a large dataset of strings (Prefix tree).


Q59: Application of a Trie?

A: Autocomplete, spell checking, IP routing.


Q60: What is a Heap?

A: A specialized tree-based data structure that satisfies the heap property (e.g., in a Max Heap, parent is always >= children).


Q61: What data structure is used to implement a Priority Queue?

A: A Heap.



โšก Cheatsheet

Last-10-min rapid reference

Last 10-Minute Exam Cheatsheet โšก


How to use this: Read this right before you walk into the exam hall. Do not try to learn new things here, just use it to trigger your memory.

---


โฑ๏ธ Time Complexity Cheat Codes


Data Structure / Algo Best Average Worst Key Characteristic
Array AccessO(1)O(1)O(1)Instant index lookup
Array Insert/DelO(n)O(n)O(n)Must shift elements
Linked List AccessO(n)O(n)O(n)Must traverse from head
Linked List InsertO(1)O(1)O(1)If position is known
Stack (Push/Pop)O(1)O(1)O(1)Top only
Queue (Enq/Deq)O(1)O(1)O(1)Front/Rear only
Bubble SortO(n)O(nยฒ)O(nยฒ)Compare adjacent
Merge SortO(n log n)O(n log n)O(n log n)Divide & Conquer (needs memory)
Quick SortO(n log n)O(n log n)O(nยฒ)Pivot based (fastest in practice)
BST SearchO(log n)O(log n)O(n)Skewed tree = worst case
Hash TableO(1)O(1)O(n)Worst case = many collisions

---


๐Ÿงฉ The "Explain Like I'm 5" Anchors


If your mind goes blank, remember these real-world objects:



---


๐Ÿ’ป 3-Line Code Skeletons


1. Linked List Traversal:

current = self.head
while current:
    print(current.data)
    current = current.next

2. Queue with Deque:

from collections import deque
q = deque()
q.append(x)     # Insert
q.popleft()     # Delete

3. BST Inorder Traversal (Always Sorted!):

def inorder(node):
    if node:
        inorder(node.left)
        print(node.data)
        inorder(node.right)

4. Bubble Sort Swap Logic:

if arr[j] > arr[j+1]:
    arr[j], arr[j+1] = arr[j+1], arr[j]

---


๐Ÿ—ฃ๏ธ Viva Survival Phrases


If you get stuck during the viva, use these phrases to show you know what you're talking about:



Deep Breath. You've got this. Good luck! ๐Ÿš€



๐Ÿ’ป Programs

Complete Source Code โ€” All 5 Programs

Exam-ready Python with comments explaining every line.

๐Ÿ”— Linked List โ€” Full Implementation

๐Ÿš‚ Think of it as a Train! Each carriage (Node) carries passengers (data) and is hooked to the next carriage (next pointer). To add a carriage you unhook one connection and insert. To remove you skip one. You NEVER need to shift everyone like in arrays!
The 3 Types:
Singly: Each node points FORWARD only โ†’ โ†’ โ†’
Doubly: Each node points both ways โ† โ†’ (e.g., browser history back/forward)
Circular: Last node points back to Head (e.g., round-robin scheduling)
โฑ Complexity Quick-Ref:
Insert at Head โ†’ O(1) (just change head pointer)
Insert at Tail โ†’ O(n) (must walk to end first)
Delete by value โ†’ O(n) (search then unlink)
Access element โ†’ O(n) (no random access, must walk from head)
๐Ÿง  HOW TO LEARN THIS CODE โ€” 3 Steps

Step 1: Understand the Node first. Before anything, burn this into your brain:
self.data = data โ€” stores the value
self.next = None โ€” pointer to next node (None = end of list)

Step 2: Learn Insert at Head (the easiest operation):
new_node.next = self.head  โ† new node points to OLD head
self.head = new_node      โ† head now points to NEW node
Just 2 lines. That's it.

Step 3: Learn Traversal (used in EVERY other operation):
current = self.head
while current:
    print(current.data)
    current = current.next

Memory Trick: Think HEAD โ†’ NODE โ†’ NODE โ†’ None as a treasure hunt. You start at the HEAD and keep following the "next" clue until you hit None (dead end).
๐Ÿ“„ View Full Code (click to expand)

๐Ÿšถ Queue โ€” 3 Implementations

๐ŸŽŸ๏ธ Think of a Movie Ticket Line! First person in line gets the ticket FIRST (FIFO). New people join at the REAR. People leave from the FRONT. Simple.
Two Ends, Two Rules:
enqueue(x) โ†’ Add to REAR
dequeue() โ†’ Remove from FRONT
peek() โ†’ View FRONT without removing
Stack vs Queue in 5 seconds:
Stack = LIFO (stack of plates - last plate placed is first removed)
Queue = FIFO (ticket line - first person in is first out)
๐Ÿง  HOW TO LEARN THIS CODE โ€” 3 Steps

Step 1: Just use Python's deque. It's the right way.
from collections import deque
q = deque()

Step 2: Two operations only. Remember them as REAR/FRONT:
q.append(x)    โ† add to REAR (enqueue)
q.popleft()   โ† remove from FRONT (dequeue)

Step 3: Know the difference from a list.pop():
list.pop(0) = O(n) โ€” slow! Shifts all elements
deque.popleft() = O(1) โ€” instant! That's why we use deque.

Memory Trick: "append = join the queue at the back. popleft = served at the counter."
๐Ÿ“„ View Full Code (click to expand)
"""
====================================================
QUEUE โ€” Complete Implementation (Exam Ready)
====================================================
Covers:
  - Queue using Python list (simple)
  - Queue using collections.deque (proper/efficient)
  - Queue using Linked List (manual implementation)
  - Operations: enqueue, dequeue, peek, isEmpty, size

KEY VIVA POINTS:
  - FIFO: First In, First Out
  - Enqueue = add to REAR
  - Dequeue = remove from FRONT
  - Real use: BFS, scheduling, printer queue
"""

from collections import deque


# ======================================
# METHOD 1: Queue using Python list
# (Simple โ€” but dequeue is O(n))
# ======================================
class QueueUsingList:
    def __init__(self):
        self.queue = []

    def enqueue(self, data):
        self.queue.append(data)           # add to rear โ€” O(1)
        print(f"Enqueued: {data}")

    def dequeue(self):
        if self.is_empty():
            print("Queue is empty! Underflow.")
            return None
        removed = self.queue.pop(0)       # remove from front โ€” O(n)!
        print(f"Dequeued: {removed}")
        return removed

    def peek(self):
        if self.is_empty():
            print("Queue is empty!")
            return None
        return self.queue[0]

    def is_empty(self):
        return len(self.queue) == 0

    def size(self):
        return len(self.queue)

    def display(self):
        print("Front ->", self.queue, "<- Rear")


# ======================================
# METHOD 2: Queue using deque (BEST)
# Both enqueue and dequeue are O(1)
# ======================================
class QueueUsingDeque:
    def __init__(self):
        self.queue = deque()

    def enqueue(self, data):
        self.queue.append(data)           # add to rear โ€” O(1)
        print(f"Enqueued: {data}")

    def dequeue(self):
        if self.is_empty():
            print("Queue is empty! Underflow.")
            return None
        removed = self.queue.popleft()    # remove from front โ€” O(1)!
        print(f"Dequeued: {removed}")
        return removed

    def peek(self):
        if self.is_empty():
            print("Queue is empty!")
            return None
        return self.queue[0]

    def is_empty(self):
        return len(self.queue) == 0

    def size(self):
        return len(self.queue)

    def display(self):
        print("Front ->", list(self.queue), "<- Rear")


# ======================================
# METHOD 3: Queue using Linked List
# (Manual โ€” shows deep understanding)
# ======================================
class QNode:
    def __init__(self, data):
        self.data = data
        self.next = None


class QueueLinkedList:
    def __init__(self):
        self.front = None   # points to front node (for dequeue)
        self.rear = None    # points to rear node (for enqueue)
        self._size = 0

    def enqueue(self, data):
        new_node = QNode(data)
        if self.rear is None:           # empty queue
            self.front = self.rear = new_node
        else:
            self.rear.next = new_node   # link to end
            self.rear = new_node        # update rear
        self._size += 1
        print(f"Enqueued: {data}")

    def dequeue(self):
        if self.is_empty():
            print("Queue is empty! Underflow.")
            return None
        removed = self.front.data
        self.front = self.front.next    # move front forward
        if self.front is None:          # queue became empty
            self.rear = None
        self._size -= 1
        print(f"Dequeued: {removed}")
        return removed

    def peek(self):
        if self.is_empty():
            print("Queue is empty!")
            return None
        return self.front.data

    def is_empty(self):
        return self.front is None

    def size(self):
        return self._size

    def display(self):
        current = self.front
        elements = []
        while current:
            elements.append(str(current.data))
            current = current.next
        print("Front -> [" + " -> ".join(elements) + "] <- Rear")


# =======================
# MAIN โ€” Demo & Test
# =======================
if __name__ == "__main__":
    print("=" * 50)
    print("QUEUE DEMO (using deque - recommended)")
    print("=" * 50)
    q = QueueUsingDeque()

    print("\n--- Enqueue operations ---")
    q.enqueue(10)
    q.enqueue(20)
    q.enqueue(30)
    q.enqueue(40)
    q.display()

    print(f"\nPeek (front): {q.peek()}")
    print(f"Size: {q.size()}")

    print("\n--- Dequeue operations ---")
    q.dequeue()
    q.dequeue()
    q.display()

    print(f"\nIs empty? {q.is_empty()}")
    q.dequeue()
    q.dequeue()
    q.dequeue()   # underflow check

    print("\n" + "=" * 50)
    print("QUEUE DEMO (using Linked List)")
    print("=" * 50)
    ql = QueueLinkedList()
    ql.enqueue("A")
    ql.enqueue("B")
    ql.enqueue("C")
    ql.display()
    ql.dequeue()
    ql.display()


"""
EXPECTED OUTPUT:

=== QUEUE DEMO (using deque) ===
Enqueued: 10
Enqueued: 20
Enqueued: 30
Enqueued: 40
Front โ†’ [10, 20, 30, 40] โ† Rear

Peek (front): 10
Size: 4

--- Dequeue operations ---
Dequeued: 10
Dequeued: 20
Front โ†’ [30, 40] โ† Rear

Is empty? False
Dequeued: 30
Dequeued: 40
Queue is empty! Underflow.

=== QUEUE DEMO (using Linked List) ===
Enqueued: A, B, C
Front โ†’ [A โ†’ B โ†’ C] โ† Rear
Dequeued: A
Front โ†’ [B โ†’ C] โ† Rear
"""

๐Ÿงฎ Infix to Postfix (Stack)

๐Ÿงฎ Humans write: A + B * C (Infix - operator is BETWEEN)
Computers prefer: A B C * + (Postfix - operator is AFTER)
Why? Postfix needs NO brackets and NO BODMAS rules. A single stack evaluates it directly.
The 4 Golden Rules (Algorithm):
1. See a Letter/Number โ†’ output it immediately
2. See ( โ†’ push to stack
3. See ) โ†’ pop to output until ( found
4. See an Operator โ†’ pop operators with higher/equal precedence, then push current
Precedence (High โ†’ Low): ^ > * / > + -

Trace A + B * C:
Aโ†’output, +โ†’stack, Bโ†’output, * has higher prec than + so push *, Cโ†’output
End: pop *, pop + โ†’ Result: ABC*+
๐Ÿง  HOW TO LEARN THIS CODE โ€” The 4-Rule Skeleton

Don't memorize the whole code. Memorize these 4 conditions inside the loop:

for char in expression:
  if is_operand(char):        output.append(char)
  elif char == '(':           stack.append(char)
  elif char == ')':           # pop till '('
  else: (operator)           # pop >= prec, then push
while stack: output.append(stack.pop())  # flush remaining

The ONE Key Line (heart of the algorithm):
while stack and precedence(stack[-1]) >= precedence(char):
    output.append(stack.pop())
This pops stronger operators before pushing the current one.

Memory Trick: "OPERAND goes out, BRACKET controls flow, OPERATOR fights for dominance."
๐Ÿ“„ View Full Code (click to expand)

๐Ÿ”€ Sorting โ€” Bubble, Merge, Quick

๐Ÿซง Bubble Sort: Compare neighbors. Swap if wrong order. Repeat. Largest bubbles to end each pass. Simple but O(nยฒ) - very slow.

โœ‚๏ธ Merge Sort: Cut pile in half, keep cutting. Sort single cards. Merge back in order. Always O(n log n) - consistent but needs extra memory.

โšก Quick Sort: Pick a Pivot. Put smaller numbers left, bigger right. Pivot is in its final home! Repeat for left and right halves. Average O(n log n) but O(nยฒ) worst case.
๐Ÿง  HOW TO LEARN THE 3 SORTS

Bubble Sort โ€” 1 Key Pattern (nested loop + swap):
for i in range(n-1):
  for j in range(n-1-i):
    if arr[j] > arr[j+1]:
      arr[j], arr[j+1] = arr[j+1], arr[j]
Trick: "n-1-i" because last i elements are already sorted โ€” don't waste time.

Merge Sort โ€” 2 functions. Learn them separately:
Function 1: merge_sort(arr) โ€” just splits. Base case: len <= 1.
Function 2: merge(left, right) โ€” walks both arrays, picks smaller, appends.
Trick: If you know merge(), the rest is just: left = sort(left half), right = sort(right half), return merge(left, right).

Quick Sort โ€” learn the partition() function:
pivot = arr[high]  โ† pick last element
i = low - 1       โ† boundary tracker
if arr[j] <= pivot: i += 1; swap(arr[i], arr[j])
swap(arr[i+1], arr[high])  โ† put pivot in final spot
Trick: "i tracks where small elements end. When done, i+1 is pivot's home."
๐Ÿ“„ View Full Code (click to expand)
"""
====================================================
SORTING ALGORITHMS โ€” Bubble, Merge, Quick Sort
====================================================

KEY VIVA COMPARISONS:
  Bubble: O(nยฒ) stable, in-place, simple
  Merge:  O(n log n) stable, NOT in-place (O(n) space)
  Quick:  O(n log n) avg, unstable, in-place, fastest in practice
"""


# ============================================================
# 1. BUBBLE SORT โ€” O(nยฒ)
# ============================================================
def bubble_sort(arr):
    """
    Compare adjacent elements, swap if out of order.
    Largest element 'bubbles up' to end each pass.
    
    Stable: YES | In-place: YES
    Best: O(n) | Average: O(nยฒ) | Worst: O(nยฒ)
    """
    arr = arr.copy()  # don't modify original
    n = len(arr)
    
    for i in range(n - 1):            # n-1 passes
        swapped = False
        
        for j in range(n - 1 - i):   # -i because last i elements are sorted
            if arr[j] > arr[j + 1]:  # if out of order
                arr[j], arr[j + 1] = arr[j + 1], arr[j]  # SWAP
                swapped = True
        
        # OPTIMIZATION: if no swaps occurred, array is already sorted
        if not swapped:
            break
    
    return arr


# ============================================================
# 2. MERGE SORT โ€” O(n log n)
# ============================================================
def merge_sort(arr):
    """
    Divide array in half โ†’ sort each half โ†’ merge them.
    
    Stable: YES | In-place: NO (O(n) extra space)
    Best/Average/Worst: O(n log n)
    """
    if len(arr) <= 1:       # BASE CASE: single element is already sorted
        return arr
    
    # DIVIDE: find midpoint
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])    # recursively sort left half
    right = merge_sort(arr[mid:])   # recursively sort right half
    
    # MERGE: combine sorted halves
    return merge(left, right)


def merge(left, right):
    """
    Merge two sorted arrays into one sorted array.
    The KEY step of merge sort โ€” O(n) time.
    """
    result = []
    i = j = 0
    
    # Compare elements from both halves, pick smaller
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:     # <= keeps it STABLE
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    
    # Add remaining elements (one side will have leftovers)
    result.extend(left[i:])
    result.extend(right[j:])
    
    return result


# ============================================================
# 3. QUICK SORT โ€” O(n log n) average
# ============================================================
def quick_sort(arr, low=None, high=None):
    """
    Choose pivot โ†’ partition (smaller left, larger right) โ†’ recurse.
    
    Stable: NO | In-place: YES
    Best/Average: O(n log n) | Worst: O(nยฒ) (sorted input, bad pivot)
    """
    arr = arr if low is not None else arr.copy()
    if low is None:
        low = 0
    if high is None:
        high = len(arr) - 1
    
    if low < high:
        # Partition: put pivot in correct position
        pivot_index = partition(arr, low, high)
        
        # Recursively sort elements before and after pivot
        quick_sort(arr, low, pivot_index - 1)
        quick_sort(arr, pivot_index + 1, high)
    
    return arr


def partition(arr, low, high):
    """
    Lomuto Partition Scheme:
    - Choose last element as pivot
    - Place all elements < pivot to the LEFT
    - Place all elements > pivot to the RIGHT
    - Put pivot in its CORRECT final position
    
    Returns the pivot's final index.
    """
    pivot = arr[high]   # choose last element as pivot
    i = low - 1         # i tracks boundary of smaller elements
    
    for j in range(low, high):
        if arr[j] <= pivot:         # if current < pivot
            i += 1                  # expand boundary
            arr[i], arr[j] = arr[j], arr[i]  # swap into smaller region
    
    # Place pivot in correct position
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1  # return pivot's final index


# ============================================================
# HELPER: Print array with step details
# ============================================================
def print_array(arr, label=""):
    print(f"{label}: {arr}")


# ============================================================
# MAIN โ€” Demo & Test All Three
# ============================================================
if __name__ == "__main__":
    
    original = [64, 34, 25, 12, 22, 11, 90]
    
    print("=" * 55)
    print(f"Original Array: {original}")
    print("=" * 55)

    # ---- BUBBLE SORT ----
    print("\n[BUBBLE SORT]")
    print("Logic: Compare adjacent pairs, swap if needed. Repeat.")

    arr = original.copy()
    n = len(arr)
    for i in range(n - 1):
        swapped = False
        for j in range(n - 1 - i):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        print(f"  After pass {i+1}: {arr}")
        if not swapped:
            break
    print(f"Sorted: {arr}")

    # ---- MERGE SORT ----
    print("\n[MERGE SORT]")
    print("Logic: Split -> sort halves -> merge")
    result = merge_sort(original)
    print(f"Sorted: {result}")

    # ---- QUICK SORT ----
    print("\n[QUICK SORT]")
    print("Logic: Pick pivot -> partition -> recurse on both sides")
    arr = original.copy()
    result = quick_sort(arr)
    print(f"Sorted: {result}")

    # ---- COMPARISON SUMMARY ----
    print("\n" + "=" * 55)
    print("COMPLEXITY SUMMARY")
    print("=" * 55)
    print(f"{'Algorithm':<15} {'Best':<15} {'Average':<15} {'Worst':<15} {'Space':<10} {'Stable'}")
    print("-" * 85)
    print(f"{'Bubble':<15} {'O(n)':<15} {'O(nยฒ)':<15} {'O(nยฒ)':<15} {'O(1)':<10} {'YES'}")
    print(f"{'Merge':<15} {'O(n log n)':<15} {'O(n log n)':<15} {'O(n log n)':<15} {'O(n)':<10} {'YES'}")
    print(f"{'Quick':<15} {'O(n log n)':<15} {'O(n log n)':<15} {'O(nยฒ)':<15} {'O(log n)':<10} {'NO'}")


"""
VIVA QUICK-FIRE:

Q: How does Bubble Sort work?
A: Compare adjacent elements, swap if arr[j] > arr[j+1]. After each pass,
   the largest element is in its correct position.

Q: Why Bubble Sort O(nยฒ)?
A: Two nested loops: outer (n-1 passes) ร— inner (n-1-i comparisons) = O(nยฒ)

Q: How does Merge Sort work?
A: Divide array to halves recursively until 1 element, then merge sorted halves.

Q: Why is Merge Sort O(n log n)?
A: log n levels of division ร— O(n) to merge each level = O(n log n)

Q: Why is Merge Sort NOT in-place?
A: The merge step needs a temporary array to store merged result.

Q: How does Quick Sort work?
A: Pick pivot (usually last element), partition so smaller elements are left
   of pivot and larger are right, then recursively sort both partitions.

Q: When is Quick Sort O(nยฒ)?
A: When pivot is always the smallest/largest (e.g., already sorted array).
   Fix: random pivot selection.

Q: Which sort is fastest in practice?
A: Quick Sort โ€” great cache performance and small constant factors.
"""

๐ŸŒณ Binary Search Tree โ€” Full

๐ŸŒณ One iron-clad rule for every single node:
Everything to the LEFT is SMALLER. Everything to the RIGHT is LARGER.

This means finding any number is like a game of hot/cold โ€” you cut the search space in HALF at every step. That's O(log n)!
3 Traversal Orders โ€” Memory Trick:
Preorder = Root FIRST (Root โ†’ Left โ†’ Right) - Use to COPY a tree
Inorder = Root MIDDLE (Left โ†’ Root โ†’ Right) - Gives SORTED output!
Postorder = Root LAST (Left โ†’ Right โ†’ Root) - Use to DELETE a tree
๐Ÿง  HOW TO LEARN THE BST โ€” Build it up in 3 stages

Stage 1: Insert (Recursive โ€” learn this first):
if data < node.data: go LEFT
if data > node.data: go RIGHT
if slot is None: place the node here

Stage 2: Inorder Traversal (3 lines only!):
def inorder(node):
  if node:
    inorder(node.left)    # Left first
    print(node.data)      # Root
    inorder(node.right)   # Right last
This ALWAYS prints sorted output. That's your viva trump card.

Stage 3: Delete (3 cases โ€” don't panic):
1. No children โ†’ return None
2. One child โ†’ return that child
3. Two children โ†’ find min of right subtree โ†’ copy its value โ†’ delete it

Memory Trick: "BST = Binary SEARCH Tree. Every decision at every node is a binary choice: go left or go right."
๐Ÿ“„ View Full Code (click to expand)
"""
====================================================
BINARY SEARCH TREE (BST) โ€” Complete Implementation
====================================================

BST PROPERTY:
  For every node:
    LEFT subtree values < node value
    RIGHT subtree values > node value

OPERATIONS:
  Insert    โ†’ O(log n) avg, O(n) worst
  Search    โ†’ O(log n) avg, O(n) worst
  Delete    โ†’ O(log n) avg, O(n) worst
  Traversal โ†’ O(n) always

KEY VIVA: Inorder traversal of BST = SORTED output!
"""


# ============================
# NODE CLASS
# ============================
class BSTNode:
    def __init__(self, data):
        self.data = data
        self.left = None    # left child (smaller values)
        self.right = None   # right child (larger values)


# ============================
# BST CLASS
# ============================
class BST:
    def __init__(self):
        self.root = None

    # --------------------------------------------------
    # INSERT โ€” O(log n) average
    # --------------------------------------------------
    def insert(self, data):
        """Insert data maintaining BST property."""
        if self.root is None:
            self.root = BSTNode(data)
        else:
            self._insert_recursive(self.root, data)
        print(f"Inserted: {data}")

    def _insert_recursive(self, node, data):
        if data < node.data:            # go LEFT for smaller values
            if node.left is None:
                node.left = BSTNode(data)
            else:
                self._insert_recursive(node.left, data)
        elif data > node.data:          # go RIGHT for larger values
            if node.right is None:
                node.right = BSTNode(data)
            else:
                self._insert_recursive(node.right, data)
        else:
            print(f"{data} already exists in BST")

    # --------------------------------------------------
    # SEARCH โ€” O(log n) average
    # --------------------------------------------------
    def search(self, data):
        """Search for data in BST."""
        result = self._search_recursive(self.root, data)
        if result:
            print(f"FOUND: {data}")
        else:
            print(f"NOT FOUND: {data}")
        return result

    def _search_recursive(self, node, data):
        if node is None:                # not found
            return False
        if node.data == data:           # found!
            return True
        elif data < node.data:          # search LEFT subtree
            return self._search_recursive(node.left, data)
        else:                           # search RIGHT subtree
            return self._search_recursive(node.right, data)

    # --------------------------------------------------
    # DELETE โ€” O(log n) average
    # 3 Cases:
    #   1. Leaf node โ†’ just delete
    #   2. One child โ†’ replace with child
    #   3. Two children โ†’ replace with inorder successor
    # --------------------------------------------------
    def delete(self, data):
        """Delete node with given data from BST."""
        self.root = self._delete_recursive(self.root, data)
        print(f"Deleted: {data}")

    def _delete_recursive(self, node, data):
        if node is None:
            return node

        if data < node.data:
            node.left = self._delete_recursive(node.left, data)
        elif data > node.data:
            node.right = self._delete_recursive(node.right, data)
        else:
            # CASE 1: Leaf node (no children)
            if node.left is None and node.right is None:
                return None

            # CASE 2: One child
            elif node.left is None:
                return node.right
            elif node.right is None:
                return node.left

            # CASE 3: Two children
            # Find inorder successor (smallest in right subtree)
            successor = self._find_min(node.right)
            node.data = successor.data          # copy successor value
            node.right = self._delete_recursive(node.right, successor.data)  # delete successor

        return node

    def _find_min(self, node):
        """Find minimum node (leftmost node)."""
        while node.left:
            node = node.left
        return node

    def _find_max(self, node):
        """Find maximum node (rightmost node)."""
        while node.right:
            node = node.right
        return node

    # --------------------------------------------------
    # TRAVERSALS โ€” O(n) all
    # --------------------------------------------------
    def inorder(self):
        """Left โ†’ Root โ†’ Right. Gives SORTED output for BST!"""
        result = []
        self._inorder_recursive(self.root, result)
        print(f"Inorder  (sorted): {result}")
        return result

    def _inorder_recursive(self, node, result):
        if node:
            self._inorder_recursive(node.left, result)   # left
            result.append(node.data)                      # root
            self._inorder_recursive(node.right, result)  # right

    def preorder(self):
        """Root โ†’ Left โ†’ Right. Used for copying tree."""
        result = []
        self._preorder_recursive(self.root, result)
        print(f"Preorder  (copy):  {result}")
        return result

    def _preorder_recursive(self, node, result):
        if node:
            result.append(node.data)                       # root FIRST
            self._preorder_recursive(node.left, result)    # left
            self._preorder_recursive(node.right, result)   # right

    def postorder(self):
        """Left โ†’ Right โ†’ Root. Used for deletion."""
        result = []
        self._postorder_recursive(self.root, result)
        print(f"Postorder (delete):{result}")
        return result

    def _postorder_recursive(self, node, result):
        if node:
            self._postorder_recursive(node.left, result)   # left
            self._postorder_recursive(node.right, result)  # right
            result.append(node.data)                       # root LAST

    # --------------------------------------------------
    # LEVEL ORDER (BFS) โ€” uses a Queue
    # --------------------------------------------------
    def level_order(self):
        """Level by level traversal using a Queue (BFS)."""
        if not self.root:
            return
        from collections import deque
        queue = deque([self.root])
        result = []
        while queue:
            node = queue.popleft()
            result.append(node.data)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        print(f"Level Order (BFS): {result}")
        return result

    # --------------------------------------------------
    # UTILITY: Height of tree
    # --------------------------------------------------
    def height(self):
        return self._height(self.root)

    def _height(self, node):
        if node is None:
            return 0
        return 1 + max(self._height(node.left), self._height(node.right))

    # --------------------------------------------------
    # UTILITY: Print tree visually
    # --------------------------------------------------
    def display(self):
        print("\nBST Structure:")
        self._print_tree(self.root, "", True)
        print()

    def _print_tree(self, node, indent, is_right):
        if node is not None:
            self._print_tree(node.right, indent + ("   " if is_right else "|  "), True)
            print(indent + ("+-- " if is_right else "+-- ") + str(node.data))
            self._print_tree(node.left, indent + ("   " if is_right else "|  "), False)


# =======================
# MAIN โ€” Demo & Test
# =======================
if __name__ == "__main__":
    bst = BST()

    print("=" * 50)
    print("BST DEMO")
    print("=" * 50)

    # Insert elements
    print("\n--- Insertions ---")
    for val in [50, 30, 70, 20, 40, 60, 80, 35, 45]:
        bst.insert(val)

    """
    BST after insertions:
            50
           /  \
          30   70
         / \   / \
        20  40 60  80
           / \
          35  45
    """

    bst.display()
    print(f"Tree Height: {bst.height()}")

    # Traversals
    print("\n--- Traversals ---")
    bst.inorder()      # Should be sorted: 20,30,35,40,45,50,60,70,80
    bst.preorder()
    bst.postorder()
    bst.level_order()

    # Search
    print("\n--- Search ---")
    bst.search(40)     # Found
    bst.search(99)     # Not found

    # Delete
    print("\n--- Deletions ---")
    bst.delete(20)     # Delete leaf
    bst.inorder()

    bst.delete(30)     # Delete node with two children
    bst.inorder()

    bst.display()


"""
VIVA QUICK-FIRE:

Q: What is BST property?
A: Left child < parent < right child, for every node.

Q: Inorder traversal of BST gives?
A: Sorted (ascending) output โ€” this is how BST sort works!

Q: Time complexity of BST operations?
A: Average O(log n), Worst O(n) when unbalanced (like a linked list).

Q: What is the inorder successor?
A: The smallest node in the right subtree โ€” used when deleting a node with 2 children.

Q: Difference between BST and Binary Tree?
A: BST has the ordering property (left < root < right). Binary Tree has no such constraint.

Q: When does BST become O(n)?
A: When all elements inserted in sorted order โ†’ tree becomes a linked list.
   Solution: Use balanced trees (AVL, Red-Black).

Q: Three deletion cases in BST?
A: 1) Leaf: just delete. 2) One child: replace with child. 
   3) Two children: replace with inorder successor.

Q: What is the height of a balanced BST with n nodes?
A: O(log n)
"""

๐Ÿ—บ๏ธ Visual Guide

Data Structures Mindmap

Data Structures
Linear
Static: Array
Dynamic: Linked List, Stack, Queue
Non-Linear
Hierarchical: Tree, BST
Network: Graph

โšก Flashcards

Quick Revision Keywords

O(1) Constant time complexity. Execution time does not change with input size. (e.g., Array index access).
O(n) Linear time complexity. Execution time grows proportionally to input size. (e.g., Linear search).
LIFO Last In, First Out. The fundamental principle of a Stack.
FIFO First In, First Out. The fundamental principle of a Queue.
Node A fundamental unit of a data structure (like Linked List or Tree) containing data and pointer(s).
Binary Search Tree (BST) A tree where left child < parent < right child. Average search time O(log n).
Hashing Mapping data of arbitrary size to fixed-size values. Used for O(1) lookups.
Graph Vertices (nodes) connected by edges. Can be directed/undirected, weighted/unweighted.

Generated for exam victory. You've got this! ๐Ÿš€