SynfraCore
Synfracore
Start Learning
Navigation

Academies

Platform

RoadmapsLabsCertificationsInterviewPYQsAI AssistantCareer
Start Learning Free🗺️ Learning Roadmaps

Data Structures & AlgorithmsFundamentals

Core concepts and foundational knowledge

✍️
Written by senior engineers. Reviewed for technical accuracy.· Updated 2025 · SynfraCore Data Structures & Algorithms Team
Expert Content

Data Structures & Algorithms — Fundamentals

Arrays and Strings

An array stores elements in contiguous memory, accessed by index — this is what makes array access O(1): the computer can jump directly to any position by calculating its memory address, without searching. The techniques below are the standard ways to solve array/string problems efficiently instead of falling back to nested loops (O(n²)).

Two pointers: use two index variables moving through the array (often from opposite ends, or one ahead of the other) instead of checking every possible pair — this turns many O(n²) "check every pair" problems into O(n).

python
def two_sum_sorted(arr, target):
    l, r = 0, len(arr) - 1
    while l < r:
        s = arr[l] + arr[r]
        if s == target: return [l, r]
        elif s < target: l += 1
        else: r -= 1
    return []

Sliding window: for problems about a contiguous sub-range (a "window") of the array, instead of recalculating the sum/condition for every possible window from scratch, slide the window by one position and adjust incrementally — subtract what left the window, add what entered it.

python
def max_sum_window(arr, k):
    window_sum = sum(arr[:k])
    max_sum = window_sum
    for i in range(k, len(arr)):
        window_sum += arr[i] - arr[i-k]   # add new element, remove the one that left the window
        max_sum = max(max_sum, window_sum)
    return max_sum

Prefix sums: precompute cumulative sums once, so any range-sum query afterward is O(1) instead of re-summing that range every time it's asked.

python
def build_prefix(arr):
    prefix = [0] * (len(arr) + 1)
    for i, v in enumerate(arr):
        prefix[i+1] = prefix[i] + v
    return prefix

def range_sum(prefix, l, r):  # inclusive
    return prefix[r+1] - prefix[l]

Linked Lists

Unlike an array, a linked list doesn't store elements contiguously — each node holds a value and a pointer to the next node. This means no O(1) random access by index (you have to walk from the start), but insertion/deletion at a known position doesn't require shifting every other element the way an array does.

python
class Node:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# Reverse a linked list -- walk through, flipping each node's pointer
# to point backward instead of forward
def reverse(head):
    prev, curr = None, head
    while curr:
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt
    return prev

# Detect a cycle (Floyd's algorithm): a fast pointer moves 2 steps for
# every 1 step the slow pointer takes. If there's a cycle, the fast
# pointer will eventually lap the slow one and they'll meet. If there's
# no cycle, the fast pointer simply reaches the end first.
def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast: return True
    return False

# Merge two already-sorted linked lists into one sorted list
def merge(l1, l2):
    dummy = Node()
    cur = dummy
    while l1 and l2:
        if l1.val <= l2.val:
            cur.next = l1; l1 = l1.next
        else:
            cur.next = l2; l2 = l2.next
        cur = cur.next
    cur.next = l1 or l2
    return dummy.next

Stacks and Queues

These are two of the simplest but most-used data structures, distinguished entirely by which end you add/remove from:

Stack — LIFO (Last In, First Out): the most recently added item is the first one removed. Think of a physical stack of plates — you add to the top, and you can only take from the top.
Queue — FIFO (First In, First Out): the earliest added item is the first one removed. Think of a line at a shop — first person in line is served first.
python
# Stack -- LIFO
stack = []
stack.append(x)   # push (add to top)
stack.pop()       # pop (remove from top)
stack[-1]         # peek (look at top without removing)

# Queue -- FIFO
from collections import deque
q = deque()
q.append(x)       # enqueue (add to back)
q.popleft()       # dequeue (remove from front) -- O(1), unlike list.pop(0) which is O(n)

Monotonic stack — a stack kept in increasing or decreasing order by only pushing elements that maintain that order, popping off anything that violates it first. Useful for "next greater/smaller element" style problems:

python
def next_greater(arr):
    result = [-1] * len(arr)
    stack = []  # stores indices, kept so their values are decreasing
    for i, v in enumerate(arr):
        while stack and arr[stack[-1]] < v:
            result[stack.pop()] = v
        stack.append(i)
    return result

Valid parentheses — a classic stack application: every closing bracket must match the most recently opened, unclosed bracket, which is exactly what a stack naturally tracks.

python
def is_valid(s):
    stack = []
    pairs = {')':'(', ']':'[', '}':'{'}
    for c in s:
        if c in '([{': stack.append(c)
        elif not stack or stack[-1] != pairs[c]: return False
        else: stack.pop()
    return not stack

Binary Search

Binary search finds a target in a sorted array in O(log n) by repeatedly eliminating half of the remaining possibilities — check the middle element; if the target is smaller, it must be in the left half (discard the right half entirely), and vice versa. This only works because the array is sorted; on unsorted data, there's no way to know which half to discard.

python
def binary_search(arr, target):
    l, r = 0, len(arr) - 1
    while l <= r:
        mid = (l + r) // 2
        if arr[mid] == target: return mid
        elif arr[mid] < target: l = mid + 1
        else: r = mid - 1
    return -1

# Lower bound -- find the leftmost position where target could be inserted
# to keep the array sorted (useful even when target isn't present)
def lower_bound(arr, target):
    l, r = 0, len(arr)
    while l < r:
        mid = (l + r) // 2
        if arr[mid] < target: l = mid + 1
        else: r = mid
    return l

# "Binary search on the answer" -- a less obvious application: instead of
# searching an array, search a RANGE OF POSSIBLE ANSWERS for the smallest
# (or largest) value that satisfies some condition, using the same
# halve-the-search-space idea.
def min_days_bloom(bloomDay, m, k):
    def can_bloom(days):
        bouquets = flowers = 0
        for d in bloomDay:
            if d <= days:
                flowers += 1
                if flowers == k: bouquets += 1; flowers = 0
            else: flowers = 0
        return bouquets >= m

    l, r = min(bloomDay), max(bloomDay)
    while l < r:
        mid = (l + r) // 2
        if can_bloom(mid): r = mid
        else: l = mid + 1
    return l if can_bloom(l) else -1

Recursion — The Idea Everything After This Section Depends On

A recursive function solves a problem by calling itself on a smaller version of the same problem, until it reaches a version simple enough to answer directly without recursing further — that stopping condition is called the base case. Without a base case, a recursive function calls itself forever until Python raises a RecursionError.

python
def factorial(n):
    if n <= 1:              # base case -- stops the recursion
        return 1
    return n * factorial(n - 1)   # recursive case -- smaller version of the same problem

print(factorial(5))   # 5 * 4 * 3 * 2 * 1 = 120

Tracing through factorial(3) to see what actually happens:

factorial(3) = 3 * factorial(2)
             = 3 * (2 * factorial(1))
             = 3 * (2 * 1)                <- factorial(1) hits the base case, returns 1 directly
             = 6

Each call waits for the one below it to return before it can complete — this is why recursive calls use up call stack space (space complexity O(n) for n levels of recursion), and why a recursive solution to a problem a loop could also solve isn't automatically "better," just often clearer to read for problems that are naturally recursive in structure.

Why this matters going forward: trees and graphs (this course's Intermediate section) are naturally recursive structures — a tree is, by definition, a node connected to smaller trees (its subtrees). Dynamic programming (Advanced) is built directly on top of recursion, adding one idea: caching results so the same smaller subproblem is never recomputed. Nothing past this point in the course makes sense without being comfortable with the idea of a function calling itself on a smaller version of the same problem.

Share:
Join our Community
Exam tips, study groups, PYQ discussions — join learners preparing together
Up Next
Data Structures & AlgorithmsIntermediate
Applied knowledge and worked examples
Also Worth Exploring
← Back to all Data Structures & Algorithms modules
OverviewIntermediate