Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA) Module 1 – Foundations: problems, correctness and complexity

Data Structures & Algorithms (DSA): start here

What an algorithm, an abstract data type and a data structure are, how the examples and exercises of this track work, and which route through it fits you.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot

What you will learn

  • Distinguish an abstract data type, a data structure and an algorithm with examples
  • Read an example's recorded output, and use the Run buttons and sample tests
  • Choose a route through the modules that fits your goal
  • Write a brute-force solution as the first step towards a better one
On this page

A program that works on ten items can crawl on ten million. Whether it does depends on two choices: how the data is kept in memory, and the steps used to answer questions about it. Data Structures & Algorithms (DSA) is the study of those two choices, of how to tell that a choice is correct, and of how to predict what it costs before you run it.

This first lesson sets out the words the track uses, shows one idea stored three ways, and explains how to use the examples and exercises.

Algorithm, abstract data type, data structure

Three words come up in every lesson, and they mean different things.

  • A problem says which inputs are allowed and what the right output is for each of them. “Given the tickets in the order people took them, call them in that order” is a problem.
  • An algorithm is a finite list of precise steps that a computer could carry out to turn an input into an output. It solves a problem when, for every allowed input, it stops and its output is right. A recipe that works only for the examples you tried, or that never finishes on some input, does not solve the problem yet.
  • An abstract data type (ADT) is a promise about behaviour. It names the operations and says what each one does, and says nothing about how the data is stored. A queue is an ADT: enqueue adds an item at the back, dequeue removes and returns the item that has waited longest, size says how many are waiting. First in, first out.
  • A data structure is one concrete way of laying the data out in memory, together with the code of its operations. A Python list, a collections.deque and a ring buffer (a fixed row of slots whose start moves round) are three data structures that can all keep that promise.
An algorithm uses the queue abstract data type, which three data structures implement: a Python list, a deque and a ring buffer.Algorithmserve the tickets in order,using only the queue's operationsQueue: an abstract data typeenqueue, dequeue, sizefirst in, first outData structures that implement itPython listdequeue moves everywaiting ticketcollections.dequequick at both endsRing bufferfixed size, a front indexthat wraps roundusesimplemented by

One abstract data type, three data structures

Text description of the diagram

The diagram has three levels, from top to bottom.

  1. An algorithm serves the tickets in order. It uses only the operations of a queue, so it does not depend on how the queue is stored.
  2. The queue is the abstract data type: its operations are enqueue, dequeue and size, and it promises first in, first out.
  3. Three data structures implement that queue: a Python list, where every dequeue moves each waiting ticket; collections.deque, which is quick at both ends; and a ring buffer, a fixed number of slots with a front index that wraps round to the start.

The split matters because the algorithm above only needs the queue’s operations. Swap one data structure for another and the algorithm stays exactly as it is; only the cost of each operation changes.

One queue, three data structures

Here is the ticket counter as code. queues.py holds three classes with the same three operations (in Python the size is asked for with len(queue)), and check_queues.py runs the same sequence of operations against each of them: three tickets join, two are served, a fourth joins, and two more are served. Then it tries to add a fourth ticket to a ring that already holds three.

The same queue operations on three data structures

tickets/check_queues.py

from queues import DequeQueue, ListQueue, RingQueue

for queue in (ListQueue(), DequeQueue(), RingQueue(capacity=3)):
    for ticket in ("A1", "A2", "A3"):
        queue.enqueue(ticket)
    served = [queue.dequeue(), queue.dequeue()]
    queue.enqueue("A4")  # in the ring, A4 goes into slot 0, which A1 left free
    served += [queue.dequeue(), queue.dequeue()]
    print(f"{type(queue).__name__:<10} served {served}, {len(queue)} waiting")

ring = RingQueue(capacity=3)
for ticket in ("B1", "B2", "B3"):
    ring.enqueue(ticket)
try:
    ring.enqueue("B4")  # a fourth ticket while three are waiting
except OverflowError as error:
    print(f"RingQueue  with {len(ring)} waiting refused B4: {error}")

Output

ListQueue  served ['A1', 'A2', 'A3', 'A4'], 0 waiting
DequeQueue served ['A1', 'A2', 'A3', 'A4'], 0 waiting
RingQueue  served ['A1', 'A2', 'A3', 'A4'], 0 waiting
RingQueue  with 3 waiting refused B4: the queue is full

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_queues.py

tickets/queues.py

"""One contract, three data structures: a queue of tickets kept in three different ways."""

from collections import deque


class ListQueue:
    """A Python list: new tickets join at the end, the oldest leaves from the front."""

    def __init__(self):
        self._items = []

    def enqueue(self, ticket):
        self._items.append(ticket)

    def dequeue(self):
        if not self._items:
            raise IndexError("dequeue from an empty queue")
        return self._items.pop(0)  # every ticket still waiting moves one place to the left

    def __len__(self):
        return len(self._items)


class DequeQueue:
    """collections.deque: adding or removing at either end takes the same short time."""

    def __init__(self):
        self._items = deque()

    def enqueue(self, ticket):
        self._items.append(ticket)

    def dequeue(self):
        if not self._items:
            raise IndexError("dequeue from an empty queue")
        return self._items.popleft()

    def __len__(self):
        return len(self._items)


class RingQueue:
    """A list of fixed size used as a ring: the tickets stay put and the front index moves."""

    def __init__(self, capacity):
        self._slots = [None] * capacity
        self._front = 0  # where the oldest ticket is
        self._size = 0

    def enqueue(self, ticket):
        if self._size == len(self._slots):
            raise OverflowError("the queue is full")
        back = (self._front + self._size) % len(self._slots)  # wraps round to slot 0
        self._slots[back] = ticket
        self._size += 1

    def dequeue(self):
        if self._size == 0:
            raise IndexError("dequeue from an empty queue")
        ticket = self._slots[self._front]
        self._slots[self._front] = None
        self._front = (self._front + 1) % len(self._slots)
        self._size -= 1
        return ticket

    def __len__(self):
        return self._size

All three serve A1, A2, A3, A4 in that order, so all three behave as a queue. Look at what happens inside the ring when A4 arrives. Its three slots are full of A1, A2 and A3 at first; after two tickets leave, the front index points at slot 2, and the back of the queue wraps round to slot 0, which A1 left empty. Nothing is moved.

What they do not share is cost. This is how the cost of each operation grows when n tickets are waiting:

Cost of the queue operations, with n tickets waiting
Operation Time Extra space Note
dequeue on a list O(n) O(1) list.pop(0) moves every waiting ticket left
dequeue on a deque O(1) O(1) deque.popleft() moves nothing
dequeue on a ring O(1) O(1) only the front index moves
enqueue on any O(1) O(1) for a list, on average over many appends

The notation O(n) is the subject of a later lesson; for now read it as “grows in step with n” and O(1) as “does not grow with n”. Python’s documentation gives the reason for the first row: removing the item at position k of a list of n items moves the n − k − 1 items after it, so position 0 is the worst place to remove from. The deque documentation draws the same contrast: a deque appends and pops at either end in about the same O(1) time, while a list pays O(n) to pop or insert at position 0. The ring buffer, also called a circular queue, gets the same speed from a plain list by never moving anything, and pays for it with a fixed capacity: as the last line of the output shows, RingQueue(capacity=3) refuses a fourth waiting ticket with OverflowError.

Choosing a data structure is choosing which operations will be cheap. A ticket counter that serves thousands of people should not use ListQueue.

Measuring: the same work at different speeds

The other way to compare two solutions is to time them. This program adds up a million numbers twice, once with a for loop and once with the built-in sum(), and keeps the fastest of five runs of each:

A for loop against sum() on a million numbers Python · sum_timing.py
import time

numbers = list(range(1_000_000))


def loop_sum(values):
    total = 0
    for value in values:
        total += value
    return total


def fastest_ms(function, repeats=5):
    """The fastest of several runs, in milliseconds: slower runs were held up by something else."""
    best = float("inf")
    for _ in range(repeats):
        start = time.perf_counter()
        function(numbers)
        best = min(best, time.perf_counter() - start)
    return best * 1000


assert loop_sum(numbers) == sum(numbers) == 499_999_500_000
loop_ms = fastest_ms(loop_sum)
builtin_ms = fastest_ms(sum)
print(f"for loop: {loop_ms:6.1f} ms")
print(f"sum():    {builtin_ms:6.1f} ms")
print(f"sum() was {loop_ms / builtin_ms:.1f} times as fast")

Output

for loop:   82.3 ms
sum():       6.0 ms
sum() was 13.6 times as fast

This output changes from run to run: The times change on every run and on every computer.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sum_timing.py

Both versions do one addition per number, so the time of both grows in step with n: a list twice as long would take each of them about twice as long. What differs is the cost of each step, which analysis calls the constant factor: on the computer that recorded this output, sum() was many times as fast. time.perf_counter() is the clock to use for this: it has the highest resolution available, and on CPython it never runs backwards.

Timings move with the computer, with whatever else it is doing and with the Python version, which is why most lessons here count steps instead: a count is the same on every computer and on every run. Timings return when the constant factor is the point.

How this track works

  • Python first. Lessons teach in Python 3.14, and the examples run with CPython 3.14.8. Where a JavaScript version helps, the example has a second tab, run with Node.js 24.21.0. You need the basics of the Python track (variables, loops, functions and lists); nothing else is assumed.
  • Real output. Each example is a real file, and the panel under it shows what it printed and which version printed it. When an output can change from run to run, such as the timing above, the panel says so.
  • Run it yourself. Examples with a Run button also run in your browser, on your own device: Python in Pyodide 314.0.7, which contains CPython 3.14.2, and JavaScript in QuickJS. The first run downloads that runtime. You can edit the code and run it again.
  • Exercises and quizzes. Most lessons end with an exercise: starter code, a prompt and sample tests you can run in the browser. A pass there is feedback for you, not a grade. The quiz after it shows the answer and the reasoning once you have answered.

The Python Online Compiler and the JavaScript Online Compiler are handy for trying your own variations of any example.

The method this track teaches

Every module uses the same habit, and the next lesson works through it in full:

  1. Restate the problem: what goes in, what must come out, and how large the input can be.
  2. Write the obviously correct solution first, even if it is slow. This is the brute force.
  3. Find its bottleneck: the step that is repeated far more often than necessary.
  4. Remove the bottleneck with a better data structure or algorithm, and say in one sentence why the faster version still gives the right answer.
  5. Keep the brute force, and check the fast version against it on many inputs.

The exercise of this lesson is step 2 on its own: a brute force, written carefully.

Choose a route

Whatever your goal, start with this module. Complexity, correctness and testing are used in every lesson after it. Then follow the modules in order, or take the route that fits:

  • Coding interviews. Arrays and strings, hashing, recursion, sorting, binary search, linked lists, stacks and queues, trees, heaps, graphs, greedy algorithms and dynamic programming, then tries and bit manipulation. Choose this if you want to recognise a problem’s type quickly.
  • University exams. The same core modules, with extra care for the proofs and the exact step counts, then maths for DSA, shortest paths and spanning trees, and hard problems. Choose this if you follow a course or prepare for a written exam.
  • Programming contests. All the core modules, then maths for DSA, range queries and string algorithms. Choose this if you want to solve timed contest problems.

This track explains each structure and algorithm once, in full. The Coding Interview Patterns track builds timed drills on top of it, and the Competitive Programming track covers contest-level variants.

For GATE CS and DA

The GATE 2027 syllabus for Computer Science (CS) covers this subject in two sections. Programming and Data Structures names recursion and these structures: arrays, stacks and queues, linked lists, trees of several kinds (binary search trees and binary heaps) and graphs. Algorithms names searching, sorting and hashing, asymptotic worst-case time and space complexity, the greedy, dynamic-programming and divide-and-conquer techniques, graph traversals, minimum spanning trees and shortest paths. All of these topics are in the modules of this track; the track page shows which of its lessons are written. The CS syllabus names C as its programming language; the algorithms are the same, and the C track covers the language itself.

The Data Science and AI (DA) syllabus asks for the same basics in Python: stacks, queues, linked lists, trees, hash tables, linear and binary search, the simple sorts, merge sort, quicksort and basic graph algorithms. For both papers, the lesson on counting steps practises the kind of exact count that a numerical-answer question can ask for.

Key takeaways

  • An algorithm solves a problem when it stops with the right output on every allowed input; an abstract data type promises operations and their behaviour; a data structure is one way to store the data and carry out those operations.
  • One ADT can have several data structures behind it, all correct, with different costs: a list, a deque and a ring buffer all make a queue, but only list.pop(0) moves every waiting item.
  • Timings show constant factors and change from computer to computer; step counts are the same everywhere, which is why this track counts steps first.
  • Each example’s panel shows what the file printed and which version printed it; Run buttons run it again in your browser.
  • Start with the Foundations module, then follow the route for interviews, exams or contests.

Exercise

Exercise · Easy · Python, JavaScript

Count the pairs that add up to a target

A warm-up in the brute-force style of this lesson. Write count_pairs_with_sum(nums, k) in Python (or countPairsWithSum(nums, k) in JavaScript). It returns how many pairs of positions i < j have nums[i] + nums[j] == k.

  • Each pair of positions counts once: in [1, 4, 4] with k = 5 the answer is 2, because the 1 pairs with each of the two 4s.
  • A number never pairs with itself at the same position, so [3] with k = 6 gives 0.
  • An empty list gives 0, and the numbers may be negative.

Try every pair of positions with two loops: the outer one picks i, the inner one every j after it. Do not worry about speed yet; a later lesson makes this faster. The sample tests call your function on six small lists.

Python · Starter code · count_pairs.py

def count_pairs_with_sum(nums, k):
    """Return how many pairs of positions i < j have nums[i] + nums[j] == k."""
    # Replace this line with your code.
    return 0
The sample tests · test_count_pairs.py
from count_pairs import count_pairs_with_sum


def test_two_pairs():
    """finds both pairs in [1, 2, 3, 4] for k = 5"""
    assert count_pairs_with_sum([1, 2, 3, 4], 5) == 2


def test_repeated_values():
    """counts each pair of positions once: [1, 4, 4] has two pairs for k = 5"""
    assert count_pairs_with_sum([1, 4, 4], 5) == 2


def test_all_equal():
    """counts every pair of three equal numbers"""
    assert count_pairs_with_sum([3, 3, 3], 6) == 3


def test_not_with_itself():
    """does not pair a number with itself"""
    assert count_pairs_with_sum([3], 6) == 0


def test_empty():
    """returns 0 for an empty list"""
    assert count_pairs_with_sum([], 4) == 0


def test_negatives():
    """handles negative numbers and a target of 0"""
    assert count_pairs_with_sum([-2, 2, 0, 0, 5], 0) == 2

JavaScript · Starter code · count_pairs.mjs

/** How many pairs of positions i < j have nums[i] + nums[j] === k. */
export function countPairsWithSum(nums, k) {
  // Replace this line with your code.
  return 0;
}
The sample tests · count_pairs.test.mjs
import { test, assert } from 'toolverse:test';
import { countPairsWithSum } from './count_pairs.mjs';

test('finds both pairs in [1, 2, 3, 4] for k = 5', () => assert.equal(countPairsWithSum([1, 2, 3, 4], 5), 2));
test('counts each pair of positions once: [1, 4, 4] has two pairs for k = 5', () => assert.equal(countPairsWithSum([1, 4, 4], 5), 2));
test('counts every pair of three equal numbers', () => assert.equal(countPairsWithSum([3, 3, 3], 6), 3));
test('does not pair a number with itself', () => assert.equal(countPairsWithSum([3], 6), 0));
test('returns 0 for an empty list', () => assert.equal(countPairsWithSum([], 4), 0));
test('handles negative numbers and a target of 0', () => assert.equal(countPairsWithSum([-2, 2, 0, 0, 5], 0), 2));
A hint

Start the inner loop one place after the outer one, at i + 1, so that a position is never paired with itself and each pair is counted once. Add one to a counter whenever the two numbers add up to k, and return the counter after both loops.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

8 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 8 A ticket queue can be kept in a Python list, a collections.deque or a ring buffer. In this picture, what is the abstract data type?

    Choose one answer.

    Show the answer to question 1

    Answer: The queue itself: enqueue, dequeue and size, with first in, first out

    An abstract data type is the promise: the operations and what they do, with nothing said about storage. The list, the deque and the ring buffer are data structures that keep that promise, and the loop that serves the tickets is an algorithm that uses it.

  2. Question 2 of 8 Which of these must be true of an algorithm that solves a problem?

    Choose every answer that is right.

    Show the answer to question 2

    Answer:

    • Its output is right for every allowed input
    • It stops after a finite number of steps on every allowed input

    To solve a problem, an algorithm must finish and be right for every allowed input. It can be written in any language, or in plain words, and passing a few sample tests shows only that it works on those inputs.

  3. Question 3 of 8 What does this print?

    Read the code, then choose one answer.

    from collections import deque
    
    q = deque(["A1", "A2"])
    q.append("A3")
    q.popleft()
    q.append("A4")
    print(list(q))
    Show the answer to question 3

    Answer: ['A2', 'A3', 'A4']

    append adds at the back and popleft removes from the front, so the deque behaves as a queue: A3 joins, A1 leaves, A4 joins, and A2, A3 and A4 are left in the order they arrived.

  4. Question 4 of 8 ListQueue.dequeue uses list.pop(0). Why does it get slower as more tickets wait?

    Choose one answer.

    Show the answer to question 4

    Answer: Removing the first item moves every item after it one place to the left

    A list keeps its items in consecutive slots, so removing the item at position 0 shifts all the others down by one. A deque, or a ring buffer that only moves its front index, removes from the front without moving anything.

  5. Question 5 of 8 Why do most lessons in this track count steps instead of timing programs?

    Choose one answer.

    Show the answer to question 5

    Answer: A count is the same on every computer and every run, and a timing is not

    Timings are real measurements, but they change with the computer, the other programs running and the Python version, so they are best for comparing constant factors. A count of the steps an algorithm takes depends only on its input.

  6. Question 6 of 8 You press Run under a Python example in this track. Where does the code run?

    Choose one answer.

    Show the answer to question 6

    Answer: In your browser, on your own device, in Pyodide

    Run starts Pyodide, a build of CPython for the browser, on your own device; the first run downloads it. The panel under the example keeps a recorded output, with the version that printed it, so you can compare the two.

  7. Question 7 of 8 You want to solve timed programming-contest problems. After the core modules, which modules does this lesson point you to?

    Choose one answer.

    Show the answer to question 7

    Answer: Maths for DSA, range queries and string algorithms

    The contest route adds maths for DSA, range queries and string algorithms to the core modules. Tries and bit manipulation close the interview route, and the extra care for proofs belongs to the exam route.

  8. Question 8 of 8 Put the steps of this track's method in order.

    Give each item its position, from 1 (first).

    Show the answer to question 8

    Answer:

    1. Restate the problem, its inputs and its limits
    2. Write the obviously correct brute force
    3. Find the step that repeats more than it needs to
    4. Remove that bottleneck and say why the result is still right
    5. Check the fast version against the brute force

    Understand the problem first, then get a correct but slow solution, find what makes it slow, improve it with a reason, and keep the slow version as a check on the fast one.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.