Week 2 - Lists
Week 2 - Lists
Warm-up
Problem 1. Suppose we implement a stack using a singly linked list. What would
be the complexity of the push and pop operations? Try to be as efficient as possible.
Problem 2. Suppose we implement a queue using a singly linked list. What would
be the complexity of the enqueue and dequeue operations? Try to be as efficient as
possible.
Problem solving
Problem 3. We want to extend the queue that we saw during the lectures with an
operation getAverage() that returns the average value of all elements stored in the
queue. This operation should run in O(1) time and the running time of the other
queue operations should remain the same as those of a regular queue.
a) Design the getAverage() operation. Also describe any changes you make to
the other operations, if any.
b) Briefly argue the correctness of your operation(s).
c) Analyse the running time of your operation(s).
Solution 3.
1
comp2123 Solution 2: Lists s1 2023
a) Recall that the average is the total sum of the elements divided by the number
of elements. Since we already store the size of the queue, it suffices to add a
single new variable that stores the sum of the elements, say sum. When a new
element get enqueued or dequeued, the sum needs to be updated.
1: function getAverage()
2: if isEmpty() then
3: return ”queue empty”
4: else
5: return sum/size
1: function newEnqueue(e)
2: sum ← sum + e
3: enqueue(e)
1: function newDequeue()
2: e ← dequeue()
3: sum ← sum − e
4: return e
Problem 4. Given a singly linked list, we would like to traverse the elements of the
list in reverse order.
a) Design an algorithm that uses O(1) extra space. What is the best time com-
plexity you can get?
√
b) Design an algorithm that uses O( n) extra space. What is the best time
complexity you can get?
You are not allowed to modify the list, but you are allowed to use position/cursors
to move around the list.
Solution 4.
2
comp2123 Solution 2: Lists s1 2023
a) We keep a cursor (position) that initially points to the last element of the list.
We iteratively scan the list until we find the node before the cursor, visit the
element at the cursor, and update the cursor to the previous node. The time
complexity is Θ(n + n − 1 + · · · + 1) = Θ(n2 ).
√
b) We store n cursors evenly spaced along the list. We traverse the span be-
tween
√ two of these cursors using the previous
√ 2 strategy. Each of these segments
is
√ n long, so each segment takes Θ ( n ) = Θ(n) time to traverse. There are
n many such segments, so the total time is Θ(n3/2 ).
We can even do better if we are more aggressive on the data that we store. To
scan between two cursors, we can traverse the √ chunk in the forward direction
storing all the elements on a stack of size n. Then √ we can use the smaller
stack to traverse√that segment in reverse order in O( n) time. In this way, the
space is still O( n) and the overall time is O(n).
Solution 5. The simplest solution is to push elements as they arrive into the first
stack. When we are required to carry out a dequeue operation, we transfer all the
elements to the second stack, pop once to later return the element on the top of
the second stack, and then transfer back all the remaining elements back to the first
stack.
This strategy works because when we transfer the elements from one stack to
the next, we reverse the order of the elements. Before we transfer things, the most
recent element to be queued is at the top of the first stack. After we transfer we
have the oldest element queued at the top of the second stack. Finally, when we
transfer the elements back to the first, we go back to the original stack order.
If the queue holds n elements each enqueue operation takes O(1) time and each
dequeue takes O(n) time since we need to transfer all n elements twice.
Solution 6. The helper function outputs permutations of the input array A that
have the first i elements fixed.
The correctness of the algorithm hinges on the fact that while the helper function
and its many recursive calls may modify the array during their execution, when a
call to a helper function finally returns, the input array is always restored to the
state it was in when the call started executing. For a formal argument we prove by
induction the property that when we call helper(A, i) the algorithm outputs all
of the array A that leaves all the entries A[0 : i ] fixed while trying all permutations
of A[i : n]. The full proof is left to the reader.
1: function permutations-recursive(n)
2: # input: integer n
3
comp2123 Solution 2: Lists s1 2023
1: function helper(A, i)
2: if i = size( A) then
3: Print A
4: for j ← i; j < n; j++ do
5: Swap A[i ] and A[j]
6: helper(A, i + 1)
7: Swap A[i ] and A[j]
Solution 7. For the non-recursive version we simulate the calls to the helper func-
tion with a stack. We use the tuple (c, i, j) to denote stages of a call. The tuple
(”start”, i, j) corresponds to the start of the for loop for some choice of (i, j) and the
tuple (”finish”, i, j) to the part of the body of the for loop after the recursive call to
helper.
1: function permutations(n)
2: # input: integer n
3: # do: print all permutations of order n
4: A ← [1, 2, ..., n]
5: S ← a stack with the tuple (”start”, 0, 0)
6: while S is not empty do
7: c, i, j ← S.pop()
8: if c = ”start” then
9: if i = n then
10: Print A
11: else
12: A [ i ], A [ j ] ← A [ j ], A [ i ]
13: S.push(”finish”, i, j)
14: S.push(”start”, i + 1, i + 1)
15: if c = ”finish” then
16: A [ i ], A [ j ] ← A [ j ], A [ i ]
17: if j < n − 1 then
18: S.push(”start”, i, j + 1)