AI 230 · Notes 01
Session 1 · Sep 3, 2026
Notes 01 · Week 1 · Session 1

Algorithmic Thinking, Problem Specification, and Correctness

AI 230 · Introduction to Algorithms Prof. Yair Chaya Long Island University, Brooklyn Thursday, September 3, 2026 · 4:00–5:50 PM

This is the first of fifteen sets of notes. It does two jobs. First, it sets out how the course runs and what you will be graded on. Second, it introduces the three habits that everything else in the semester rests on: saying precisely what a problem is, designing a procedure that solves it, and being able to argue that the procedure is right — before anyone runs it.

Three live demonstrations · marked ● in the contents

Sections marked with a dot contain a working demonstration you can drive directly in this page — a specification attacked by cheating procedures, a loop invariant stepped one iteration at a time, a recursion unwinding its call stack. Every number they show is computed, not pre-written. Click through them while you read; they are the argument, not decoration.

Session planHow we will spend the 110 minutes

0:00
Course overview — format, projects, tools, expectations (§1)
0:20
The two protagonists: algorithms and data structures (§2)
0:38
Algorithmic thinking — the four habits (§3)
0:52
Problem specification — specification breaker (§4)
1:12
Pseudocode as a design tool (§5)
1:26
Correctness — invariant stepper, recursion trace (§6)
1:42
Setup checklist, and what to bring Tuesday (§7)

Section 1How this course works

AI 230 is a three-credit course with an integrated two-hour laboratory component, delivered online. We meet twice a week, Tuesday and Thursday, 4:00–5:50 PM, live over Google Meet. There is no separate lab period — the lab happens inside these sessions. That has a practical consequence you should absorb now: these are working sessions, not lectures you can watch passively. This is a tutorial-size section, and every student is expected to present current work and take part in critique at every meeting.

There are no examinations. Your entire grade comes from four projects, each worth 25%. Each project is a real build with a written component, submitted as a Git repository plus a report. Specifications and rubrics are released at least two weeks before each due date and posted to the course repository. Late submissions lose 10% per calendar day unless an extension was granted in advance.

ProjectWhat you buildWeight
1 · Complexity LaboratorySorting and searching algorithms from scratch, plus a benchmarking harness, plus a written analysis reconciling measured runtimes with predicted behavior — including where theory and the machine disagree.25%
2 · Data Structures in PracticeA hash table and a balanced search tree or priority queue, written without library support, applied to a substantial real dataset. Test suite, performance comparison against the language built-ins, and a design justification.25%
3 · Optimization EngineOne realistic optimization problem solved twice — once greedily, once with dynamic programming — with a report on correctness, optimality, and cost, stating exactly when the greedy approach is safe and when it fails.25%
4 · Capstone: Graph-Based ApplicationA working application driven by graph algorithms, containing at least one computationally hard subproblem addressed by a heuristic. Working system, technical report, live presentation.25%

There is no textbook and nothing to purchase. All notes, worked examples, code, and project specifications are written for this course and posted to the course GitHub repository before the session that uses them. Brightspace at lms.liu.edu carries announcements, submissions, and grades.

Tools you need working by next session

Where this course is going

Weeks 2–3 build the analytical machinery: asymptotic notation, loop analysis, recurrences, and the Master Theorem. Weeks 4–7 apply it to sorting, searching, hashing, trees, and heaps. Weeks 8–10 are the strategy weeks — greedy methods and dynamic programming. Weeks 11–13 are graphs: traversal, spanning trees, shortest paths, and a first look at flow. Weeks 14–15 ask what happens when no efficient algorithm exists: P, NP, reductions, and the approximation and randomization techniques you fall back on. Today is the vocabulary and the reasoning discipline that all of that assumes.

Section 2Two protagonists: algorithms and data structures

Everything in this course is a conversation between two ideas. One of them is in the course title; the other shares top billing anyway. This section introduces both — and deliberately stops at the introductions. The machinery behind each is a story for later weeks; on day one, the two protagonists only need to be named precisely.

Definition

An algorithm is a finite, unambiguous, step-by-step procedure that transforms a specified input into a specified output.

Each adjective in that definition is doing work, and it is worth naming what each one rules out.

Notice what the definition does not mention: a programming language, a machine, a runtime. An algorithm is an idea. Code is one realization of it. The same algorithm can be written in Python, in C++, on paper, or carried out by hand — and it is the same algorithm each time, with the same fundamental cost behavior. Confusing the algorithm with its implementation is the most common beginner error in this field, and it is why we write pseudocode alongside code all semester.

Why algorithms matter

The second protagonist

Definition

A data structure is a way of organizing and storing data in memory so that it can be accessed and manipulated efficiently.

That definition is short enough to memorize, so give it just enough depth to be honest. Memory is fundamentally flat — an addressable array of bytes with no notion of "customer" or "route" or "word count." A data structure is the shape you impose on that flatness, together with the set of operations the shape makes cheap. And the second half of that sentence carries a warning worth keeping all semester: a data structure is not just a layout; it is a layout plus a bargain. Every way of organizing data makes some operations fast and, in exchange, makes others slow. There is no structure that wins everywhere — only structures that fit, or fail to fit, what a particular program actually does.

How deep that bargain runs — what arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs each pay and what each gets back — is the business of Weeks 6–7, and you will feel it in your hands in Project 2, where you build the important ones without library support. Today, the definition and the bargain are all we need.

Why they are taught together

Data structures and algorithms are usually taught as one subject because separating them produces nonsense. The structure determines what operations are cheap; the algorithm is a sequence of operations. Change the structure and the algorithm's cost changes, sometimes catastrophically, without a single line of the algorithm being edited.

Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they'll be obvious. — Fred Brooks, The Mythical Man-Month (1975)

Brooks is making a strong claim: choose the representation well and the procedure often writes itself. Testing that claim is a running theme of the whole semester, and it begins in earnest once we can measure cost. For now, take it as the reason this course keeps both protagonists on stage at once.


Section 3Algorithmic thinking

Everything so far has been vocabulary. This section is the actual subject of the course.

Algorithmic thinking is the discipline of moving from "I know what I want" to "I know exactly how to get it, I can say why it works, and I can say what it costs." Most people who can program have the first half — they can produce something that runs and produces plausible output. What separates an engineer from a coder is the second half: the ability to defend the thing.

Four habits make up that discipline, and we practice all four every week this semester.

  1. State the problem before solving it. Most bad solutions are answers to a question nobody asked precisely. §4.
  2. Find what stays true. Loops and recursions are hard to reason about all at once. They become easy the moment you identify the one property that holds at every step. §6.
  3. Argue, don't just test. Tests can show that a bug exists. They cannot show that none does. §6.
  4. Count the cost. Correct is the floor, not the ceiling. Two correct programs can differ by a factor of a million — Week 2 is devoted to measuring exactly that.
The habit to unlearn

The instinct most students bring in is: read the problem, start typing, run it, see if the output looks right, adjust until it does. That loop works on toy problems and fails silently on everything else — it produces code that is right on the cases you happened to try and wrong on the ones you did not. For the rest of this course, the order is: specify, design, argue, then implement. You will write less code and throw away far less of it.

Section 4Problem specification

A specification is a contract. It states what the caller must guarantee before invoking your procedure, and what your procedure guarantees in return. Nothing outside the contract is promised.

The three parts of a specification
  • Input and precondition — what the procedure receives, and what must be true of it. If the precondition is violated, the procedure owes nothing.
  • Output and postcondition — what the procedure returns, expressed as a relationship to the input. Not "a sorted list" but "a list that is a permutation of the input and is in non-decreasing order."
  • What is not promised — the freedoms you are keeping. Stability, in-place operation, behavior on ties, order of equal elements.

A weak postcondition, demonstrated

Here is the claim this section rests on, and it is worth stating before you see it work: a specification is only as strong as the stupidest procedure that satisfies it. Writing "sort the list" feels complete. It is not, and the way to find out is to hunt for a procedure that obeys every word you wrote and is obviously not a sort.

Below, four candidate procedures run against the same input. Switch on the clauses of your postcondition one at a time and watch which candidates survive. Your goal is a specification that admits the real sort and rejects everything else — using the fewest clauses that achieve it.

Demonstration 1Specification breaker — how weak is your postcondition?

INPUT  A = [3, 1, 2, 1]    PROBLEM  "sort the list"

Your postcondition — tick the clauses you require
Candidate procedureReturnsOrderingContentsMultiplicityVerdict

Work through it and the lesson lands on its own. Ordering alone lets [0,0,0,0] through — perfectly non-decreasing, and it threw your data away. Add contents and [1,2,3] still slips past, having quietly deduplicated. Only all three together pin down the real sort. Every clause you left out was a bug you had authorized in advance.

Two things worth naming. First, "my sort silently loses elements" is a genuine class of production bug, not a classroom contrivance — and the specification is where you would have caught it, for free, before writing a line. Second, notice that the identity procedure fails only the ordering clause while satisfying the other two: each clause is doing distinct work, and dropping any one of the three reopens a different hole.

Check yourself — Is the three-clause specification above complete? Name something a caller might reasonably assume that it still does not promise.

Several things, and this is the point. It does not promise stability — that equal elements keep their original relative order, which matters the moment you sort records by one field having already sorted by another. It does not say whether A is modified — sorting in place versus returning a new list is a difference a caller will notice painfully. It says nothing about behavior when elements are not comparable. A specification is never "finished"; it is explicit about exactly as much as you have decided, and honest about the rest. That is why "what is not promised" is a required part of the contract, not an afterthought.

Specification is where correctness gets its meaning

A program cannot be "correct" in the abstract — it can only be correct with respect to a specification. Ask "is this function right?" and the only honest answer is "right about what?" Every correctness argument in this course, and every test you write in your four projects, is ultimately an argument that the postcondition holds whenever the precondition did.

In your projects

Every function you submit this semester should carry a specification in its docstring or header comment: what it takes, what must be true of it, what it returns, and what it does not promise. This is graded. It is also, in practice, the thing that will save you the most debugging time — a surprising number of bugs are found while writing the specification, before any code exists.

Section 5Pseudocode as a design tool

Pseudocode is a description of a procedure precise enough to be unambiguous, but free of the syntax and bookkeeping of any real language. It is not a lesser form of code. It is a different tool, used at a different stage, for a different purpose: pseudocode is where you think, code is where you commit.

The reason to use it is that programming languages force decisions before you are ready. Do you use a list or a NumPy array? Do you handle the empty case with an exception or a sentinel? Which of these does the algorithm actually care about, and which are incidental? In pseudocode you can defer all of that and see the shape of the idea. When the idea is right, translating it into Python is mechanical — and if it is not mechanical, that is a signal the pseudocode was hiding a decision you had not made.

Conventions we use in this course

Here is MAXIMUM — the procedure §6 will step through — as pseudocode, then in both languages, so you can see exactly what the translation adds and what it leaves untouched.

MAXIMUM(A, n) // precondition: n ≥ 1 m ← A[0] for i ← 1 to n-1 // invariant: m = max(A[0 .. i-1]) if A[i] > m m ← A[i] return m // postcondition: m ∈ A and m ≥ A[i] for all i
def maximum(A):
    """Return the largest element of A.

    Precondition:  len(A) >= 1 and elements are mutually comparable.
    Postcondition: result is an element of A, and is >= every element of A.
    Not promised:  which occurrence is returned on ties. A is not modified.
    """
    if len(A) == 0:
        raise ValueError("maximum() requires a non-empty sequence")

    m = A[0]
    for i in range(1, len(A)):
        # invariant: m == max(A[0 .. i-1])
        if A[i] > m:
            m = A[i]
    return m

Compare the three. The C++ version adds types, a container, and an iteration index of the right unsigned type; the Python version adds an exception and a docstring. Neither changes the algorithm. The comparison count is identical, the memory usage is identical, and the invariant is the same sentence in both. Everything the two implementations argue about is bookkeeping — which is precisely why we design in pseudocode first.

Why the empty-list check appears in the code but not in the pseudocode

Because the pseudocode has a stated precondition: n ≥ 1. Under a contract, code is not obliged to handle inputs that violate the precondition — the caller broke the deal.

Real implementations usually check anyway, for a practical reason: a violated precondition that goes undetected produces a wrong answer far from where the mistake happened, and those are the expensive bugs. Raising immediately turns a silent wrong answer into a loud, local failure. This is defensive programming layered on top of the contract, not a replacement for it — and it is exactly the kind of decision that belongs in code rather than in the design.

Section 6Correctness

Program testing can be used to show the presence of bugs, but never to show their absence. — Edsger W. Dijkstra, 1970

Take that literally for a moment. You write a function and run twenty tests. All pass. What have you established? That the function is correct on twenty inputs. If the input space is all arrays of integers, twenty is not a meaningful fraction of it — and the inputs you chose came from the same brain that wrote the bug, so they are systematically biased toward the cases you already thought about. Tests are indispensable, and you will write many in this course. But testing alone is a sampling strategy applied to an infinite space.

The alternative is not to stop testing. It is to be able to argue, over all inputs at once, that the postcondition follows from the precondition. Two tools do almost all the work, and they are really the same tool wearing different clothes: loop invariants for iteration, and induction for recursion.

6.1 · Loop invariants

Definition

A loop invariant is a statement about the program's variables that is true at the start of every iteration of the loop. It captures the partial progress the loop has made so far.

Finding the invariant is the creative step; once you have it, the argument is a template with three parts.

  1. Initialization. The invariant is true before the first iteration.
  2. Maintenance. If it is true at the start of an iteration, it is still true at the start of the next one.
  3. Termination. The loop ends, and when it does, the invariant together with the exit condition gives you the postcondition.

Initialization and maintenance are the base case and the inductive step of a proof by induction on the number of iterations. Termination is the payoff — where you cash the invariant in for the thing you wanted to prove. An invariant that is true but does not imply the postcondition at exit is a true statement that is useless; choosing an invariant that is too weak is the common failure.

Step MAXIMUM through below. At every iteration the demonstration shows which slice of the array has been examined, what m currently holds, and whether the invariant claim actually holds — computed against the array, not asserted. Then try the empty array, which is the interesting one.

Demonstration 2Loop invariant stepper — MAXIMUM, one iteration at a time
Input
i
m
Invariant: m = max(A[0 .. i-1])
not started
Press Step to run initialization, then each iteration in turn.

Now read the same argument in prose, which is what you will write in your project reports.

Initialization. Before the first iteration, i = 1 and m = A[0]. The claim is that m = max(A[0 .. 0]), a one-element range whose maximum is A[0]. True. Note where the precondition n ≥ 1 was used — without it A[0] does not exist and initialization fails immediately.

Maintenance. Assume m = max(A[0 .. i-1]) at the top of the iteration. The body compares A[i] with m and sets m to A[i] exactly when A[i] is larger. So after the body, m = max(max(A[0 .. i-1]), A[i]) = max(A[0 .. i]). Then i increases by one, so the invariant for the new i reads "m = max(A[0 .. i-1])" — which is what we just established. True.

Termination. The loop variable increases by one each pass and is bounded by n, so the loop ends, with i = n. Substituting into the invariant: m = max(A[0 .. n-1]) — the maximum of the entire array. And m was only ever assigned from an element of A, so mA. Both clauses of the postcondition hold.

Notice how much that did. It checked no particular array. It covers every array of every length with every arrangement of values, including the ones you would never think to test. That is what an argument buys you that a test suite cannot.

Check yourself — Run the demonstration on the empty array. Which of the three steps fails, and what does that tell you?

Initialization fails — m ← A[0] has nothing to read, and "max of an empty range" is undefined. Maintenance and termination are untouched; they were never the problem. This is the useful part: the correctness argument does not merely say "it breaks," it points at exactly which assumption was load-bearing. Preconditions are not decoration. They are the hypotheses your proof depends on, and a violated precondition is a broken proof.

A second invariant — linear search

Same template, different postcondition, and a negative invariant this time. Specification: given array A of n elements (n ≥ 0) and a target t, return True if t occurs in A and False otherwise.

def linear_search(A, target):
    """Return True if target occurs in A, otherwise False.

    Precondition:  none (A may be empty).
    Postcondition: result is True iff there exists i with A[i] == target.
    Not promised:  the position of the match.
    """
    for i in range(len(A)):
        # invariant: target does not occur in A[0 .. i-1]
        if A[i] == target:
            return True
    return False

The invariant is a negative statement — "the target is not in the part already examined" — and that is what makes the argument work. Initialization: before the first iteration the examined range A[0 .. -1] is empty, and the target trivially does not occur in an empty range. Maintenance: we only reach the end of the body when A[i] != target, so extending the examined range by one preserves the claim. Termination: two exits. Returning True happens only having just observed A[i] == target, so a match genuinely exists. Falling out of the loop means i = n, and the invariant says the target does not occur in A[0 .. n-1] — the whole array — so False is right. The empty array needs no special handling: the body never runs and the invariant already covers it.

A rule of thumb worth keeping

If you cannot state the invariant of a loop you wrote, you do not yet understand the loop. This is not a criticism — it is a diagnostic, and the fastest one available. In practice, "what is true here every time round?" is the question that finds off-by-one errors before the debugger does.

6.2 · Recursion and induction

Recursive procedures get the same treatment with the names changed. Instead of initialization and maintenance you have a base case and an inductive step, and the induction runs on the size of the input rather than the number of iterations.

def array_sum(A, i=0):
    """Return the sum of A[i .. len(A)-1].

    Precondition:  0 <= i <= len(A).
    Postcondition: result equals the sum of elements from index i onward
                   (0 when the range is empty).
    """
    if i == len(A):          # base case: empty range
        return 0
    return A[i] + array_sum(A, i + 1)

Watch it run. Each step pushes a frame; the base case returns 0 and the stack unwinds, each frame adding its own element to what came back. Then flip the switch to the broken version, where the recursive call forgets to advance i.

Demonstration 3Recursion trace — the call stack, and what happens without progress
Version A = [3, 1, 4, 1, 5]
Press Step to push the first call.
Stack depth
0
Returned

Base case. When i = n the range is empty and the procedure returns 0, the sum of no elements. Correct.

Inductive step. Assume the procedure is correct for ranges of size k. For a range of size k+1 starting at i, it returns A[i] plus the result of the call on the range starting at i+1 — a range of size k, which by hypothesis returns the correct sum. Adding A[i] gives the sum of the full range. Correct.

Termination. Each call increases i by exactly one and i is bounded above by n, so the base case is reached after finitely many calls.

That third paragraph is not optional, and it is the one people leave out — which is what the broken version is for. Its base case is still perfectly correct and its arithmetic is still fine; only progress is missing, and the stack grows until the runtime kills it. Correctness of a recursive procedure has two obligations: produce the right answer if it returns, and return. When we reach recurrence relations and the Master Theorem in Week 3, "how fast does the input shrink" becomes the central question for cost as well as for termination — the same structural fact answering two different questions.

Next week — Notes 02

Correctness is the floor, not the ceiling: two procedures can both be perfectly correct and yet differ in cost by a factor that grows without limit as the input does. Next week we learn to measure that cost precisely — asymptotic notation and the analysis of loops, in Notes 02, starting Tuesday, September 8.

Section 7Before next session

To do — due before Tuesday, September 8
  1. Install Python 3.11 or later and confirm it from a terminal with python --version.
  2. Install Git and confirm with git --version. Set your name and email with git config --global user.name and user.email.
  3. Create a GitHub account if you do not have one, and send me the username.
  4. Join the course repository and clone it locally. All notes, examples, and specifications live there.
  5. Install a code editor (VS Code recommended) and Jupyterpip install notebook.
  6. Re-read §4 and §6. Then take any function you have written in the past and write its specification: precondition, postcondition, and what it does not promise. Bring it Tuesday; we will look at a few.

If any part of the setup fails, email me before Tuesday rather than arriving stuck — we lose real working time to environment problems, and they are always faster to fix in advance. Setup instructions are in the repository, and we run a guided session this week for anyone who wants to do it together.