{
const scripts = Array.from(
document.querySelectorAll("script.fcpython-ojs-quiz-config")
);
const script = scripts.find(
(node) => node.dataset.fcpythonRendered !== "true"
);
if (!script) {
return html`<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>`;
}
script.dataset.fcpythonRendered = "true";
const quiz = JSON.parse(script.textContent);
const container = html`<div class="fcpython-quiz"></div>`;
const title = document.createElement("h3");
title.textContent = quiz.title;
container.appendChild(title);
const instructions = document.createElement("p");
instructions.textContent = quiz.instructions;
container.appendChild(instructions);
const progress = document.createElement("div");
progress.className = "fcpython-quiz-progress";
const counter = document.createElement("p");
counter.className = "fcpython-quiz-counter";
counter.setAttribute("aria-live", "polite");
progress.appendChild(counter);
const tabs = document.createElement("div");
tabs.className = "fcpython-quiz-steps";
tabs.setAttribute("role", "tablist");
tabs.setAttribute("aria-label", "Quiz questions");
progress.appendChild(tabs);
container.appendChild(progress);
const questions = document.createElement("div");
questions.className = "fcpython-quiz-questions";
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0;
function setStepStatus(questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz.questions[questionIndex];
const statuses = {
answered: "answered",
correct: "correct",
incorrect: "incorrect",
unanswered: "not answered",
};
step.classList.remove(
"is-answered",
"is-correct",
"is-incorrect",
"is-unanswered"
);
if (status) {
step.classList.add(`is-${status}`);
}
const statusLabel = status ? `, ${statuses[status]}` : "";
step.setAttribute(
"aria-label",
`Question ${questionIndex + 1}: ${question.prompt}${statusLabel}`
);
}
function showQuestion(questionIndex, focusPanel = false) {
currentQuestion = Math.max(
0,
Math.min(questionIndex, quiz.questions.length - 1)
);
questionPanels.forEach((panel, index) => {
panel.hidden = index !== currentQuestion;
});
stepButtons.forEach((step, index) => {
const isCurrent = index === currentQuestion;
step.classList.toggle("is-current", isCurrent);
step.setAttribute("aria-selected", String(isCurrent));
step.tabIndex = isCurrent ? 0 : -1;
});
counter.textContent = `Question ${currentQuestion + 1} of ${quiz.questions.length}`;
const selected = questionPanels[currentQuestion].querySelector(
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz.questions.length - 1;
previous.hidden = currentQuestion === 0;
next.hidden = isLastQuestion || !selected;
check.hidden = !isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion].focus();
}
}
quiz.questions.forEach((question, questionIndex) => {
const tabId = `${quiz.id}-question-tab-${questionIndex + 1}`;
const panelId = `${quiz.id}-question-panel-${questionIndex + 1}`;
const step = document.createElement("button");
step.type = "button";
step.className = "fcpython-quiz-step";
step.id = tabId;
step.textContent = String(questionIndex + 1);
step.setAttribute("role", "tab");
step.setAttribute("aria-controls", panelId);
step.setAttribute("aria-selected", "false");
step.tabIndex = -1;
step.addEventListener("click", () => showQuestion(questionIndex));
step.addEventListener("keydown", (event) => {
let destination = null;
if (event.key === "ArrowRight") {
destination = (questionIndex + 1) % quiz.questions.length;
} else if (event.key === "ArrowLeft") {
destination =
(questionIndex - 1 + quiz.questions.length) % quiz.questions.length;
} else if (event.key === "Home") {
destination = 0;
} else if (event.key === "End") {
destination = quiz.questions.length - 1;
}
if (destination !== null) {
event.preventDefault();
showQuestion(destination);
stepButtons[destination].focus();
}
});
stepButtons.push(step);
tabs.appendChild(step);
setStepStatus(questionIndex, "");
const panel = document.createElement("div");
panel.className = "fcpython-quiz-panel";
panel.id = panelId;
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", tabId);
panel.tabIndex = -1;
const fieldset = document.createElement("fieldset");
fieldset.className = "fcpython-quiz-question";
const legend = document.createElement("legend");
legend.textContent = question.prompt;
fieldset.appendChild(legend);
question.options.forEach((option, optionIndex) => {
const label = document.createElement("label");
label.className = "fcpython-quiz-option";
const input = document.createElement("input");
input.type = "radio";
input.name = `${quiz.id}-${question.id}`;
input.value = String(optionIndex);
input.addEventListener("change", () => {
setStepStatus(questionIndex, "answered");
feedbackNodes[questionIndex].textContent = "";
feedbackNodes[questionIndex].className = "fcpython-quiz-feedback";
score.textContent = "";
showQuestion(questionIndex);
});
const text = document.createElement("span");
text.textContent = option;
label.appendChild(input);
label.appendChild(text);
fieldset.appendChild(label);
});
const feedback = document.createElement("p");
feedback.className = "fcpython-quiz-feedback";
feedback.setAttribute("aria-live", "polite");
feedbackNodes.push(feedback);
fieldset.appendChild(feedback);
panel.appendChild(fieldset);
questionPanels.push(panel);
questions.appendChild(panel);
});
container.appendChild(questions);
const actions = document.createElement("div");
actions.className = "fcpython-quiz-actions";
const previous = document.createElement("button");
previous.type = "button";
previous.className = "fcpython-quiz-secondary";
previous.textContent = "Previous";
previous.addEventListener("click", () => {
showQuestion(currentQuestion - 1, true);
});
const next = document.createElement("button");
next.type = "button";
next.textContent = "Next question";
next.addEventListener("click", () => {
showQuestion(currentQuestion + 1, true);
});
const check = document.createElement("button");
check.type = "button";
check.textContent = "Check answers";
const reset = document.createElement("button");
reset.type = "button";
reset.className = "fcpython-quiz-secondary fcpython-quiz-reset";
reset.textContent = "Reset";
const score = document.createElement("p");
score.className = "fcpython-quiz-score";
score.setAttribute("aria-live", "polite");
check.addEventListener("click", () => {
let correctCount = 0;
let firstQuestionToReview = null;
quiz.questions.forEach((question, questionIndex) => {
const selected = container.querySelector(
`input[name="${quiz.id}-${question.id}"]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (!selected) {
feedback.textContent = "Choose an answer before checking.";
feedback.className = "fcpython-quiz-feedback";
setStepStatus(questionIndex, "unanswered");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
return;
}
const selectedIndex = Number(selected.value);
if (selectedIndex === question.answer_index) {
correctCount += 1;
feedback.textContent = `✅ Correct. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-correct";
setStepStatus(questionIndex, "correct");
} else {
const answer = question.options[question.answer_index];
feedback.textContent = `❌ Not yet. Correct answer: ${answer}. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-incorrect";
setStepStatus(questionIndex, "incorrect");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
}
});
score.textContent = `Score: ${correctCount}/${quiz.questions.length}`;
if (firstQuestionToReview !== null) {
showQuestion(firstQuestionToReview, true);
}
});
reset.addEventListener("click", () => {
container.querySelectorAll("input[type='radio']").forEach((input) => {
input.checked = false;
});
feedbackNodes.forEach((feedback) => {
feedback.textContent = "";
feedback.className = "fcpython-quiz-feedback";
});
stepButtons.forEach((_, questionIndex) => {
setStepStatus(questionIndex, "");
});
score.textContent = "";
showQuestion(0, true);
});
actions.appendChild(previous);
actions.appendChild(next);
actions.appendChild(check);
actions.appendChild(reset);
container.appendChild(actions);
container.appendChild(score);
showQuestion(0);
return container;
}
FreeCampus Python
Know It Works and Understand the Cost
Explain why a small algorithm is correct, show that it terminates, and compare alternatives through input size, operation growth, and extra-space trade-offs.
python-foundations
problem-solving-algorithms
correctness
complexity
Course progress
0%
This lesson studies a trail-marker cleanup tool. Hikers record marker codes in encounter order, but some markers are repeated. The program must keep the first occurrence of each code and preserve order.
Keep these questions beside the notebook:
- Which cases could disprove a confident claim that the algorithm works?
- What remains true after each processed marker?
- Which changing value guarantees that the loop eventually stops?
- What does input size
ncount for this problem? - How does work change when
ndoubles? - What time, memory, order, and input-type promises differ among alternatives?
1. One passing trail does not cover every route
This implementation passes the opening sample:
Run it on adjacent duplicates:
But the actual contract says remove every later repeat, not only adjacent repeats:
The code does what its docstring says. It does not meet the trail cleanup contract. A program can be internally consistent and still solve the wrong problem.
The small input ['oak', 'river', 'oak'] is a counterexample to the claim that neighbor comparison is sufficient.
2. Give every case a reason to exist
Build a behavior table before repairing the code:
| Case | Input | Expected | Rule exposed |
|---|---|---|---|
| empty | [] |
[] |
output shape for no values |
| one marker | ['oak'] |
['oak'] |
smallest successful input |
| all repeated | ['oak', 'oak', 'oak'] |
['oak'] |
repeated-value removal |
| all distinct | ['oak', 'river'] |
['oak', 'river'] |
preserve every new value |
| separated repeat | ['oak', 'river', 'oak'] |
['oak', 'river'] |
duplicates need not be adjacent |
| alternating | ['oak', 'river', 'oak', 'river'] |
['oak', 'river'] |
repeated lookup over time |
| order-sensitive | ['ridge', 'oak', 'ridge'] |
['ridge', 'oak'] |
preserve first-seen order |
Represent these cases with plain data and assertions:
Unit 13 will organize cases with pytest and parametrization. Here, the important skill is selecting cases that challenge distinct rules.
3. Frame the claim with preconditions and postconditions
A precondition describes supported input before the algorithm begins. A postcondition describes what must be true when it finishes.
For a set-assisted cleanup algorithm:
- Precondition:
valuesis a finite iterable of hashable values. - Postcondition 1: the result contains each distinct input value once.
- Postcondition 2: result order follows each value’s first input occurrence.
- Postcondition 3: the input is unchanged.
Hashability is part of the precondition because a set needs hashable elements. The function does not need to catch or recover from unsupported values in this unit; Unit 8 teaches exception boundaries.
A separate algorithm could support unhashable values such as lists by using list membership. The algorithms therefore do not have identical input contracts. That difference belongs in any comparison.
4. Shrink a failure until one rule is visible
Suppose a long trail fails:
Remove irrelevant values while keeping the failure. If the adjacent-only algorithm still fails on this input, shrink again:
Now one relationship is visible: the repeated oak is separated from its first occurrence. This minimal counterexample suggests that the algorithm must remember more than the immediately previous value.
TipA counterexample is a design instrument
Do not collect a large failing input only as proof that something went wrong. Reduce it until the missing state or rule becomes obvious enough to guide the next algorithm.
5. State what is true for the processed prefix
Use the stable-order implementation from Lesson 3:
An invariant is a statement that remains true at a chosen point during the loop. Immediately after each iteration:
resultcontains the first occurrence of every distinct value in the processed prefix, in encounter order, andseencontains exactly the same values.
Trace the opening input:
| Processed prefix | seen |
result |
Invariant true? |
|---|---|---|---|
[] |
set() |
[] |
yes |
['oak'] |
{'oak'} |
['oak'] |
yes |
['oak', 'river'] |
{'oak', 'river'} |
['oak', 'river'] |
yes |
['oak', 'river', 'oak'] |
unchanged | unchanged | yes |
['oak', 'river', 'oak', 'ridge'] |
adds ridge |
appends ridge |
yes |
Why this statement supports correctness:
- Before the loop, no values are processed; both structures are empty, so the statement is true.
- For a new value, adding it to both structures preserves membership and order.
- For a repeated value, changing neither structure preserves the first occurrence only.
- After the loop, the processed prefix is the entire input, so the invariant becomes the promised result.
This is a correctness argument in ordinary language, not formal proof notation.
Checkpoint: cases, contracts, and invariants
6. Explain why the loop eventually stops
Correct partial results are not enough if the algorithm can run forever. unique_in_order uses a for loop over a finite input. Each iteration consumes one next value, so the number of unprocessed values decreases by one. Eventually none remain.
A while version makes the progress variable visible:
index += 1 is essential. Without it, the condition remains true for any non-empty input and the same value is processed repeatedly.
For this loop, a progress measure is len(values) - index. It begins as a non-negative integer, decreases by one, and cannot decrease forever without reaching zero.
You do not need that expression for every ordinary for loop. Use it when the stopping argument is unclear, especially for while, nested state, or a manually updated position.
7. Preserve behavior while changing an implementation
Complete the set-assisted function, then run every case:
Check the ownership promise separately:
Value equality checks behavior; identity checks that the function returned a separate list. Neither check replaces the other.
If you later change the algorithm, keep these checks unchanged. A faster result that reorders markers or mutates the input does not preserve the same contract.
8. Define the input size before discussing speed
Use n for the number of input markers:
For other problems, n could mean characters, records, rows, or vertices. State it. A grid may need two sizes—rows r and columns c—rather than hiding both inside n.
Wall-clock measurements of tiny inputs vary with the computer and background work. Begin by counting an operation whose repetition explains the algorithm. For cleanup, membership checks are important.
Each input value causes one set-membership check:
Doubling n doubles the number of membership checks in this loop.
9. Describe growth before naming its category
Consider four common shapes:
One lookup independent of collection traversal
For a non-empty list, retrieving index 0 does not scan all values. We call this constant growth, written O(1).
Repeatedly halve the remaining search range
If a sorted search space is cut roughly in half at each step, doubling its size adds about one additional step. This is logarithmic growth, written O(log n). Binary search is a familiar example; its full implementation is not required in this unit.
Inspect every value once
The number of iterations grows with n. This is linear growth, O(n).
Compare many pairs
For an all-pairs comparison, doubled input approaches four times as many pairs. This is quadratic growth, O(n²).
Sorting comparison-based data belongs to another important category, O(n log n). You already use sorted; implementing sorting algorithms is outside this Foundations unit.
Big-O notation describes how growth behaves as input becomes large. It does not state exact seconds, and it intentionally ignores fixed multipliers and lower-order terms.
10. Best and worst cases can differ
A first-match search may stop immediately or inspect the whole input:
Compare positions:
- Best case: the target is first; one comparison.
- Worst case: the target is last or absent;
ncomparisons. - Ordinary observed case: depends on real input distribution.
When the function must support any listed input, worst-case growth is a useful shared comparison. It does not predict how often each case occurs in a specific product.
Checkpoint: input size and growth
11. A nested duplicate scan repeats earlier work
A baseline algorithm can support equality-comparable values even when they are unhashable:
It is correct for the stated order contract:
But value not in result can scan the growing result list. On all-distinct input, approximate equality comparisons grow like this:
n |
Earlier result items inspected across iterations |
|---|---|
| 1 | 0 |
| 2 | 1 |
| 4 | 6 |
| 8 | 28 |
| 16 | 120 |
The sum is 0 + 1 + 2 + ... + (n - 1). Doubling n makes the count approach four times as large. This worst case is O(n²).
“Only one visible for loop” does not imply linear time. The membership operation inside it performs its own work.
12. A set trades extra memory for faster ordinary membership
The set-assisted version performs one ordinary set lookup per item:
For ordinary set behavior, lookup and insertion are treated as average O(1), so the whole traversal has average O(n) time. The function also stores up to n values in seen in addition to the result, so its extra working space is O(n).
Be precise about the claim:
- it is an ordinary/average hash-table cost, not a promise that every conceivable lookup under every collision pattern takes identical time;
- it requires hashable values;
- it preserves order because
result, not the set, is returned; - it does not mutate the input.
The list-membership version supports a wider class of equality-comparable values and uses no separate membership collection, but its worst-case time grows quadratically. The “better” choice depends on the input contract and scale.
13. Sorting, scanning, and hashing preserve different promises
A sorted-copy strategy can group equal values together:
It returns a different order:
Compare the contracts before comparing speed:
| Strategy | Order returned | Supported values | Typical/worst growth discussed here | Extra working space |
|---|---|---|---|---|
| result-list membership | first-seen | equality-comparable | worst O(n²) |
result only |
| set-assisted | first-seen | hashable | average O(n) |
set plus result |
| sorted copy + adjacent scan | sorted | mutually orderable | O(n log n) for sort plus scan |
sorted copy plus result |
The sorted version is not a faster replacement for a first-seen-order contract. It solves a different output-order problem.
14. Big-O does not choose the program for you
Suppose a configuration contains at most six marker codes and accepts small nested lists. The result-list version may be the clearest supported solution. Its theoretical quadratic worst case is unlikely to matter at that scale.
Suppose a stream contains millions of hashable identifiers and order must be preserved. The set-assisted version avoids repeated scans and has a compelling trade-off.
Ask:
- What scale is actually supported?
- Is the code on a frequent path?
- Which input types must work?
- Is first-seen order required?
- May the program use additional memory?
- Is the simpler alternative already comfortably within the constraint?
Measured performance is useful after these questions. The standard-library timeit module and profiling tools appear naturally in later project/tooling work. A tiny timing run should not replace a correctness suite or a growth explanation.
15. Improve only after behavior is protected
Use this sequence:
The improvement cycle preserves the same contract while replacing one source of repeated work.
flowchart LR A[State the contract] --> B[Run behavior cases] B --> C[Count repeated work] C --> D[Change one strategy] D --> E[Rerun every case] E --> F[Explain the tradeoff] E -->|Behavior changed| A
For trail cleanup:
- freeze the first-seen-order, non-mutation contract;
- keep all acceptance cases unchanged;
- identify repeated result-list membership scanning;
- add a
seenset for hashable inputs; - rerun value, order, empty, repeat, and ownership checks;
- document average linear time and linear extra working space; and
- retain the baseline if unhashable values remain a supported requirement.
An optimization is complete only when both behavior and trade-offs are explicit.
Checkpoint: compare complete contracts
16. Lab: compare campsite-conflict detectors
A campsite request contains site IDs. A conflict exists when an ID appears more than once.
Implement and compare three functions:
def has_conflict_nested(site_ids):
"""Return True when any two positions contain the same ID."""
pass
def has_conflict_seen(site_ids):
"""Return True when a hashable ID has appeared before."""
pass
def has_conflict_sorted(site_ids):
"""Return True when adjacent values in a sorted copy are equal."""
passBehavior checks
conflict_cases = [
([], False),
(["A03"], False),
(["A03", "A03"], True),
(["A03", "B07", "A03"], True),
(["A03", "B07", "C12"], False),
]
for site_ids, expected in conflict_cases:
assert has_conflict_nested(site_ids) is expected
assert has_conflict_seen(site_ids) is expected
assert has_conflict_sorted(site_ids) is expectedOwnership checks
Reasoning tasks
- State the precondition for each function.
- Write an invariant for the
seenversion. - Explain why each loop terminates.
- Add counters for equality or membership checks on all-distinct inputs of length 4, 8, and 16.
- Classify worst-case time growth and extra working space.
- Recommend a version for at most six possibly unhashable IDs.
- Recommend a version for one million hashable string IDs where order is not part of the Boolean result.
Hint: nested pairs need two indexes
For each left index, compare only positions to its right. Return True on the first equal pair and False after every pair has been considered.
Show a comparison strategy after attempting the lab
The nested version uses two index ranges and no extra membership structure; it supports equality-comparable values but has quadratic worst-case comparisons. The seen version returns on the first repeated ID and has average linear time with linear extra set space for hashable IDs. The sorted-copy version sorts without mutating the input, then checks adjacent pairs; it needs mutually orderable values and O(n log n) sorting time. For six unhashable values, the nested version may be simplest. For one million hashable strings, the seen version is usually the strongest starting choice.
17. Defend the choice in plain language
Finish the lab with six sentences:
- Contract: what behavior and input types the chosen function supports.
- Cases: which input most strongly challenges the implementation.
- Invariant: what remains true after each completed iteration.
- Termination: what progresses toward the stopping condition.
- Cost: how time and extra working space grow with
n. - Trade-off: why one rejected alternative is less suitable for this contract and scale.
This explanation is part of the result. It allows a future maintainer to decide whether a changed requirement invalidates the choice.
Key points
- Passing one familiar example does not establish correctness; purposeful boundaries and counterexamples challenge distinct rules.
- Preconditions frame supported input, postconditions frame the result, and a loop invariant connects partial work to the finished promise.
- A termination argument identifies progress through a finite problem.
- Define input size and count important repeated operations before naming a growth category.
- Compare time, extra space, supported values, ordering, and mutation behavior together.
- Improve an algorithm only while preserving the same behavior suite—or state clearly that the contract changed.