Question 1 · easy
Which structure guarantees constant-time access by arbitrary numeric index i in the standard fixed-size-word RAM model?
Answer: option B. The address of slot i is start + i × item size.
Making lists, reading items, searching with in — and feeling work grow with size.
What happens to the work when the data gets bigger?
values = [12, 7, 30, 4, 18]
print(values[0], values[-1]) # 12 18
print(values[1:4]) # [7, 30, 4]A slice includes its start and excludes its stop. append(x) adds at the end; pop() removes and returns the last value. Access outside the list raises IndexError.
total = 0
largest = values[0] # requires a nonempty list
for x in values:
total += x
if x > largest:
largest = x
print(total, total / len(values), largest)For an empty list, sum is 0; a mean or maximum needs a separate rule. Do not initialise the maximum to 0: that fails for an all-negative list.
x in values hides a search. A comprehension such as [x for x in values if x % 2 == 0] also visits every item. Short code need not mean little work.
a = [2, 4]
b = a
c = a.copy()
b.append(6)
print(a, b, c)a, bboth point to [2, 4, 6]cpoints to separate [2, 4]Assignment does not copy a list. copy() makes a new outer list; nested objects are still shared. Test empty, one-item, repeated and all-negative inputs before measuring speed.
10 test questions · 4 written questions · 14 total
Each question is followed by its answer and explanation. Hard questions also include a starting hint and smaller reasoning steps.
Choose one option. Cost questions state their model and whether the bound is tight, expected or worst-case. Here lg means log₂, and heap positions start at 1.
Question 1 · easy
Which structure guarantees constant-time access by arbitrary numeric index i in the standard fixed-size-word RAM model?
Answer: option B. The address of slot i is start + i × item size.
Question 2 · easy
Push 5, 6, 7 onto a stack, then pop once. You get:
Answer: option C. Last in, first out.
Question 3 · easy
Enqueue 5, 6, 7 into a queue, then dequeue once. You get:
Answer: option A. First in, first out.
Question 4 · easy
In the ordered-dictionary interface used here (search, insert, delete, min, max, predecessor, successor), which operation belongs instead to the stack interface?
Answer: option B. Push belongs to stacks; the dictionary operations are search, insert, delete, min, max, predecessor and successor.
Question 5 · easy · course question
For a = [10, 20, 30], what is a[1]?
Answer: option B. Python list indices start at 0. The indices 0, 1 and 2 refer to 10, 20 and 30.
Question 6 · easy · course question
After a = [2, 4] and a.append(6), what is a?
Answer: option D. append adds one item at the end and changes the existing list.
Question 7 · easy · course question
Which expression is True for a = [3, 5, 3]?
Answer: option A. Membership asks whether an equal value appears anywhere. The value 5 is present; repeated 3s still occupy separate list positions.
Question 8 · easy · course question
Starting with a = [1, 2], execute b = a and then b.append(3). What is a?
Answer: option C. a and b refer to the same list. Appending through either name changes that shared list.
Question 9 · easy · course question
For a = [8, 6, 4, 2], what is a[1:3]?
Answer: option B. The slice includes indices 1 and 2 but excludes stop index 3. It produces a new list [6, 4].
Question 10 · easy · course question
A linear membership search checks an unsorted five-item list for a missing value. With one equality check per item, how many checks are needed?
Answer: option D. A missing value cannot be ruled out until every item has been checked. The worst case here is five checks.
Read each question together with its explanation, trace or proof. Numbering continues from the test questions.
Question 11 · easy
Which gives O(1) access to the i-th item, an array or a linked list? Which one can grow without a fixed size limit?
An array gives constant-time indexed access in the RAM model. A linked list follows pointers to reach its i-th item, taking linear time in the index. Both linked lists and dynamic arrays can grow subject to memory limits; only a fixed-size array has a predetermined capacity. Python lists are dynamic arrays of references.
Question 12 · easy
Push 1, 2, 3 onto a stack and pop twice: what comes out, in order? Enqueue 1, 2, 3 into a queue and dequeue twice?
stack: 3 then 2 (last in, first out). Queue: 1 then 2 (first in, first out).
Question 13 · easy
Which container for the “to-do list” turns a graph traversal into depth-first search, and which into breadth-first search?
a stack gives DFS, a queue gives BFS.
Question 14 · easy
What is a sentinel, and what does it buy you?
a dummy item placed at a boundary that is never removed, such as a key of +∞ at the start of a list; it removes special-case tests (empty list, running off the end) without changing the Big-O.
Predict on paper, run the code, then explain any difference. Use the animations below to inspect individual steps.
Run the maximum logic on [−8, −3, −10]. List largest after each item.
−8, −3, −3. Starting at 0 would give a false result.
How many equality checks do 20 absent-target searches need in a 10,000-item list?
20 × 10,000 = 200,000.
Change b = a to b = a.copy() in the sharing example. Predict all three lists.
a and c remain [2, 4]; b becomes [2, 4, 6].
Given [12, 7, 30, 4, 18], compute the total, average, largest and
smallest using a single loop and no built-in functions except len.
data = [12, 7, 30, 4, 18]
total = 0
biggest = data[0]
smallest = data[0]
for x in data:
total = total + x
if x > biggest:
biggest = x
if x < smallest:
smallest = x
print(total, total / len(data), biggest, smallest)One pass, four answers — and the cost is one visit per item however long the list is.
Build data = list(range(10000)). Search for 20 different values that
are all missing, adding up the total looks. How many comparisons did those 20
innocent-looking searches cost?
200 000. Twenty searches × 10 000 looks. Now imagine 10 000 searches over 10 000 items: 100 million comparisons — a nested loop in disguise. Week 11 shows how to make this same job almost free.
Write code that reports whether a list contains any repeated value, comparing every item with every later item. Count the comparisons for lists of 100, 200 and 400 random numbers.
import random
data = random.sample(range(1000000), 400)
comparisons = 0
found = False
for i in range(len(data)):
for j in range(i + 1, len(data)):
comparisons = comparisons + 1
if data[i] == data[j]:
found = True
print(found, comparisons)About 4 950, 19 900 and 79 800 comparisons. Doubling the list roughly quadruples the work — the nested-loop signature from week 3, now on real data.
From list(range(20)), build a list of just the even numbers twice:
once with an empty list and an append loop, once with a one-line comprehension.
Confirm both lists are equal.
data = list(range(20))
loop_way = []
for x in data:
if x % 2 == 0:
loop_way.append(x)
comp_way = [x for x in data if x % 2 == 0]
print(loop_way == comp_way) # True — same result
print(comp_way)Same answer, same cost (one visit per item) — the comprehension is just the compact spelling.
Write a linear search over ["Ada", "Bilal", "Cem", "Dilek", "Ece"] that
returns the position of a target, or -1 if it is missing. Report
the position and the number of looks for a first item, a last item, and a missing one.
names = ["Ada", "Bilal", "Cem", "Dilek", "Ece"]
target = "Dilek"
position = -1
looks = 0
for i in range(len(names)):
looks = looks + 1
if names[i] == target:
position = i
break
print(f"position {position}, {looks} looks")A first item costs 1 look (best case), a last or missing item costs 5 (worst case) — the three rows of the §4.4 table, measured directly.
This code should leave original untouched while building a doubled copy,
but it corrupts original. Explain why, then fix the assignment by
making a full-slice copy.
original = [1, 2, 3]
copy = original
for i in range(len(copy)):
copy[i] = copy[i] * 2
print("original:", original) # [2, 4, 6] — wrong!
copy = original makes a second name for the same list, so editing
copy edits original. Take a real copy with a slice:
original = [1, 2, 3] # restore the starting data after the buggy run
copy = original[:] # the [:] makes a separate outer list
for i in range(len(copy)):
copy[i] = copy[i] * 2
print("original:", original)
print("copy:", copy)Now the loop changes only copy, and original stays [1, 2, 3].
colours = ["red", "green", "blue"]. What is colours[1]?
Searching an unsorted list of 1 000 items for something that is not there costs about:
Which pair of operations costs the same no matter how long the list is?
After b = a where a = [1, 2, 3], you run b.append(4). What is a?
b = a does not copy; both names point at one list, so appending through b is visible through a. Use a[:] for a real copy.What does [x * x for x in range(4)] produce?
range(4) gives 0, 1, 2, 3, and the comprehension squares each: 0, 1, 4, 9.So far every variable held one thing. A list holds many, in order, under one name — written in square brackets, separated by commas.
grades = [72, 88, 45, 91, 63]
students = ["Ada", "Bilal", "Cem", "Dilek"]
mixed = ["Ada", 72, True] # allowed, but rarely a good idea
print(grades)
print(len(grades)) # how many items
Items are numbered from zero. The last item is at position
len(list) - 1, or more conveniently at -1.
print(students[0]) # Ada — the first
print(students[2]) # Cem — the third
print(students[-1]) # Dilek — the last
print(students[1:3]) # ['Bilal', 'Cem'] — a slice, stops before 3
Asking for students[9] in a four-item list gives
IndexError: list index out of range — "there is no tenth shelf" (index 9).
For a list long enough that both indices are valid, reading students[0]
and students[999999] has the same constant-growth cost model: Python can
go straight to a known position. This describes how access cost depends on list length,
not a guarantee of exactly identical measured times. Remember this in week 10, where
it becomes the first row of an important table.
queue = [] # start empty
queue.append("Ada") # add to the end
queue.append("Bilal")
queue.append("Cem")
print(queue)
queue[1] = "Burak" # replace an item
last = queue.pop() # take the last one off
print(queue, "| removed:", last)
append and pop work at the end of the list, which
turns out to be the cheap end. There are also insert(0, x) and
pop(0) for the front, and those are quietly expensive — week 10 measures
exactly how expensive, and the answer is worse than most people expect.
grades = [72, 88, 45, 91, 63]
total = 0
passed = 0
best = grades[0]
for g in grades:
total = total + g
if g >= 60:
passed = passed + 1
if g > best:
best = g
print(f"average {total / len(grades):.1f}, passed {passed}, best {best}")One pass over the list answers three questions at once. And notice the shape of the cost: one visit per item. A list ten times longer takes ten times the work. Your step counter from week 3 will confirm it.
for g in grades hands you each value in turn — use it when you
only care about the items. When you also need the position, loop over
range(len(grades)) and read grades[i]. You will want the
position version in §4.4, where the point is precisely how far along the
match was found.
"Is this name in my list?" Python has a short way to ask, and it is worth writing the long way first so you can see what really happens.
names = ["Ada", "Bilal", "Cem", "Dilek", "Ece"]
target = "Dilek"
found = False
looks = 0
for name in names:
looks = looks + 1
if name == target:
found = True
break # stop early — we found it
print(f"found: {found} after {looks} looks")The short way does exactly the same work behind the scenes:
print("Dilek" in names) # True
print("Zeynep" in names) # False| Situation | Looks needed | Name for it |
|---|---|---|
| The item is first | 1 | best case |
| Successful target equally likely at each of n positions | (n + 1) / 2 | average under this assumption |
| The item is last, or missing entirely | n | worst case |
This one-by-one method has a name we will use for the rest of the course: linear search. The worst case is the one we plan around, and it is the interesting one for another reason: every failed search costs a full pass. A program that checks thousands of items against a list of thousands is doing millions of comparisons — and it looks like two innocent lines of code.
Very often you do not want to change a list in place — you want to make a
new one from it: the passing grades only, each price with tax added, the names
in capitals. The reliable pattern is: start with an empty list and append
as you go.
grades = [72, 88, 45, 91, 63, 30]
passing = [] # start empty
for g in grades:
if g >= 60:
passing.append(g) # add the ones we want to keep
print(passing)
print(f"{len(passing)} of {len(grades)} passed")The same shape builds a transformed list — here, each grade rounded up by 5 marks of generous marking:
grades = [72, 88, 45, 91, 63]
boosted = []
for g in grades:
boosted.append(g + 5)
print(boosted)Python has a compact one-line spelling for exactly this pattern, the list comprehension, which you will meet properly in the next section. Whichever spelling you use, the cost is the same and familiar: one visit per item, so a list ten times longer takes ten times the work.
From next week you will need lists far too long to type. Two ways to conjure them:
import random
numbers = list(range(100000)) # 0, 1, 2, ... 99999
squares = [x * x for x in range(10)] # a list comprehension
shuffled = random.sample(range(100000), 100000) # same numbers, random order
print(len(numbers), squares[:5], shuffled[:5])
The middle line is a list comprehension: "make a list of
x * x for every x in this range". It is the compact Python
idiom for building a list from another sequence — the same job as the append loop in
§4.5, written in one line. You can add a condition too, which reads like plain
English:
evens = [x for x in range(20) if x % 2 == 0]
passing = [g for g in [72, 45, 88, 30, 63] if g >= 60]
print(evens)
print(passing)Read it left to right: the value to keep, then where the items come from, then the condition they must pass. It appears in nearly every benchmark in this course.
Run this, then change size to 10 000 and 100 000. Predict the numbers
before each run and write your predictions down.
size = 1000
data = list(range(size))
target = -1 # deliberately not in the list
looks = 0
for item in data:
looks = looks + 1
if item == target:
break
print(f"list of {size}: {looks} looks for a missing item")Ten times the data, ten times the looks — every time, no exceptions. You have just measured your first growth law with your own hands. Week 5 puts a stopwatch next to the counter, and week 6 turns the pair into a graph.
Lists bring two surprises that catch nearly everyone, because they involve how Python shares and returns things. Meet them now.
1. Two names, one list. Writing b = a does not copy the
list — it gives the same list a second name. Change it through one name and the other
sees it too:
a = [1, 2, 3]
b = a # NOT a copy — same list, two names
b.append(4)
print("a:", a) # a changed too!
print("b:", b)
c = a[:] # a real copy (a full slice)
c.append(99)
print("a:", a, "c:", c)
When you genuinely want a separate list, copy it with a[:] (or
list(a)). Otherwise mutations are shared while the names refer to the
same list; giving one name a different object does not change what the other name refers to. A
slice copies the outer list, so nested mutable objects can still be shared.
2. append changes the list and returns nothing. A very
common bug is to assign its result back:
nums = [3, 1, 2]
# WRONG: append changes nums in place and hands back None
# nums = nums.append(5) -> nums becomes None, next line crashes
nums.append(5) # RIGHT: just call it; the list is already changed
print(nums)
Methods such as append and sort change the list in place
and return None; do not replace the list name with that result.
pop also changes the list, but returns the removed item, so
last = queue.pop() is a valid way to save that item.
| Mistake | Symptom | Fix |
|---|---|---|
b = a then editing b | The other list changes too | Copy with a[:] or list(a). |
x = mylist.append(v) | x is None | Write mylist.append(v) on its own line. |
| Index past the end | IndexError | Valid positions run 0 to len(list) - 1. |
From the beginner notes · Lecture 4
A list stores items, but a stack or queue also specifies which item leaves next. A stack removes the most recently added item; a queue removes the earliest. These rules describe what comes out next, whatever code stores the items.
Imagine three inspection jobs arriving as 1, 2 and 3. A stack (last in, first out) returns 3 then 2. A queue (first in, first out) returns 1 then 2. The same stored values produce different behaviour because the service rule changed.
A Python list gives direct access to a position. A linked list reaches a position by following links. Both can grow, but they pay different costs for access and rearrangement. Do not assume that a container's name proves it is appropriate for the job.
Engineering use. Choose a service rule for inspection jobs and a different rule for undoing a sequence of edits. Explain the consequence of each.
in to ask "is this here?" and know roughly what it costs;range and random;00:00–01:00: launch, predict–run–explain cycle, questions · 01:00–01:10: break · 01:10–02:00: worked variation, peer instruction, questions · 02:00–02:10: break · 02:10–03:00: core mechatronics practice, exam bridge, and exit ticket.
Ask at any point. Weekly self-checks stay private; optional extensions are not collected.
Need a slower explanation? Open the English + Türkçe reference guide.
AA_Week04.ipynb and solve Tasks 1–6.== that they match.| Term | Meaning in plain words |
|---|---|
| list | An ordered collection of values under one name. |
| index | The position of an item, counted from zero. |
| append / pop | Add to / remove from the end of a list. |
| linear search | Checking items one by one until you find it or run out. |
| aliasing | Two names pointing at the same list; a change through one shows through the other. |
| best / worst / average case | The luckiest, unluckiest and typical amount of work for the same method. |
| list comprehension | [f(x) for x in things] — build a list from a sequence in one line. |
These are exercises from Skiena's Algorithm Design Manual, Chapter 2, matched to this week and solved step by step. Both are about the little loop you used to find a maximum or minimum — why it is correct, and how much work it really does.
Here is the standard way to find the largest value in a list A: remember the
first item, then walk through the rest, keeping whatever is bigger. Argue that this really
does return the maximum, for any nonempty list. The first-item initialization
requires at least one item; an empty input needs separately specified behavior.
m = A[0] # current champion
for i in range(1, len(A)):
if A[i] > m:
m = A[i] # a bigger value dethrones it
print(m) # claim: this is the maximum
The idea: a running champion. Think of m as the current
title-holder in a "who is biggest?" contest. We claim something simple stays true the
whole way through the loop — a loop invariant:
After we have looked at the first k items, m holds the largest of those k.
If that sentence is true right up to the last item, then when the loop ends —
having looked at all of them — m must hold the largest of the whole list.
So we just need to see why it can never become false. Two small checks do it.
Base case (before the loop, k = 1). We set m = A[0]. The
largest of just the first item is, of course, the first item. So the invariant is true
at the start — with one item examined, m holds the biggest of that one item.
Each step keeps it true. Suppose it is true after k items:
m is the biggest of the first k. Now we look at the next item,
A[k]. Only two things can happen:
A[k] is bigger than the champion — the if fires,
m becomes A[k], and now m is the biggest of all
k + 1 items (it beat the old best, which beat everyone before it).
A[k] is not bigger — we leave m alone, and it is still the
biggest of the first k. This maximum is at least as big as the newcomer,
so it is the biggest of all k + 1 too. The earlier items need not
individually exceed the newcomer; only their maximum must do so.
Either way the invariant survives the step. Since it is true for one item, and each pass
carries it from k to k + 1, it is still true after the last item —
so the printed m is the maximum of the whole list. (This is the "running
champion" version of a formal induction proof; it is exactly the argument you traced by
hand on tiny inputs in §4.3.)
Answer: the loop keeps the invariant "m is the maximum of
A seen so far". It holds before the loop (one item), each pass preserves it,
so after the final pass m is the maximum of the entire list — the algorithm
is correct.
To find the minimum of a list, set tmp = A[0], then compare
tmp against A[1], A[2], …, A[n] in order, doing
tmp = A[i] whenever A[i] is smaller. Assume the values are
distinct and every ordering is equally likely. How many times, on average, does
that assignment tmp = A[i] run?
tmp = A[0]
assignments = 0
for i in range(1, len(A)):
if A[i] < tmp:
tmp = A[i] # how often does THIS line run?
assignments = assignments + 1
print(tmp, "updated", assignments, "times")
What is counted. The initial tmp = A[0] is set-up, not one of the assignments we count. We count the assignments made during the scan, at positions i = 1 … n (the array is A[0..n], i.e. n+1 numbers). When does position i assign? Only when A[i] is the smallest of the first i+1 numbers seen, A[0..i]. (A new "record low".)
Among the first i+1 numbers, in a random order each is equally likely to be the smallest, so the chance A[i] is a new record is exactly 1/(i+1). As i grows a new record gets rarer.
Add the chances over the scanned positions i = 1 … n:
expected assignments = Σi=1n 1/(i+1) = 1/2 + 1/3 + … + 1/(n+1) = Hn+1 − 1
Here Hm = 1 + 1/2 + … + 1/m is the harmonic number; Hn+1 − 1 is it with the leading 1 removed. It grows about like ln n — about 2.0 at n = 10, 4.2 at 100, 6.5 at 1000, 13.4 at a million. A quick simulation over random shuffles agrees. (If the array has exactly n elements, the same argument gives Hn − 1.)
Answer: the assignment fires at position i with probability 1/(i+1), so the expected number is Hn+1 − 1 = 1/2 + 1/3 + … + 1/(n+1) ≈ ln n — that is O(log n). It is not the full Hn: the initial tmp = A[0] is not a counted assignment.
You searched a list by hand and felt the work grow with n. Time to stop counting by hand: week 5 wraps work in a function and times it with a real stopwatch.