Exercise 1
Problem Description
At first, I thought this exercise was about dividing by a modulus, where the modulus would always be incremented by one. But then I realized that in some cases, there’s no “2” in the division sequence. For example, for 12
, the answer is 12 6 3 1\n
. This indicates that the number is always being divided by 2
.
When we look at the examples of these answers, we can see that the format aligns each number to the right, based on the width of the largest number.
1 | %run_and_test python3 -c "from exercise_4_1 import f1; f1(13)" |
So we have the following solution:
1 | # Assume that the argument n is an integer at least equal to 1. |
Complexity Analysis
The time complexity of this algorithm is O(n log n)
, where n
is the input number. This is because we are dividing the number by 2
until we reach 1
. The space complexity is O(1)
because we are not using any additional data structures.
Exercise 2
Problem Description
This exercise asks us to repeatedly remove the maximum number from the set until only one element remains. The line print(“A list of sets:”, all(isinstance(x, set) for x in S)) checks if the return value of f2 is a list of sets.
In my solution:
1 | def f2(S: set[int]): |
I convert the set to a list because tuples don’t have a sort() method. So I convert it, sort it, and then pop the largest element (which is at the end of the list) while the length is greater than 1.
The standard solution is:
1 | def f2(S): |
The standard solution is simpler; it uses max() to find and remove the largest element, then appends the result to the list.
Standard solution is better because it is more concise and uses less memory.
Exercise 3
Problem Description
As per the description of Exercise 4, we need to remove elements that are equal to the mean value of their neighboring elements (i.e., the element is the average of the one just before and the one just after it).
Please pay attention to the special challenge: use only one loop without any break or continue statements, and only one if statement (with no elif nor else statements).
In my solution:
1 | # Assume that the argument L is an increasing list of integers. |
Explanation:
The function iterates through the list, checking if any element (except the first and last) is the average of its neighbors.
If an element meets this condition, it’s removed using
pop()
, and the function calls itself recursively to continue checking the remaining list.This approach ensures that all elements satisfying the condition are removed.
The main reason for using recursion in the original code is to avoid
i
going beyond the scope of the list, because the length of the list changes each time an element is deleted, so in the original for loop, the index may exceed the length of the list. Recursion avoids this situation by re-calling the function and re-traversing the list from the beginning.
When we use for i in sth.
in Python, i
is essentially a “view” or a “shallow copy” of the elements in sth.
. Modifying i
directly will not change sth.
itself. Additionally, we cannot modify the value of i
, because in each iteration of the loop, i
is reset to the next element in the sequence.
This means that if we use a for loop directly in this context, and we remove an element from the list during the loop, the list will get shorter. As a result, i
could end up being out of range in subsequent iterations of the loop.
The standard solution is:
1 | def f3(L): |
Explanation:
The while loop version of the code does avoid recursion, and manually manages the value of i to ensure that the index does not exceed the range after the list is deleted. This reduces the complexity of the recursive call while achieving the same effect.
Key points:
- The function uses only one loop and one if statement as required by the challenge.
- Recursion is used to continue processing after each removal.
Exercise 4
Problem Description
You are given an integer n
, and the goal is to express n
as a sum (or difference) of powers of 2
. This should be done by leveraging the binary representation of n
.
Specifically, you need to:
- Convert n to its binary form.
- Identify which positions in the binary form have a 1.
- Use these positions to generate the corresponding powers of 2.
- If n is positive, add the powers of 2 together. If n is negative, subtract the powers of 2.
The output should be in the form of an equation, displaying the original number as the sum (or difference) of powers of 2.
Example 1:
For n = 13
:
The binary representation of 13 is 1101.
This corresponds to 2^3
+ 2^2
+ 2^0
, because the 3rd, 2nd, and 0th positions in the binary number are 1
.
Output: 13 = 2^3 + 2^2 + 2^0
Explanation:
The function f4(n) converts an integer n into a sum of powers of 2 based on its binary representation. Let’s go through the code:
Handling the case when n is zero:
1
2
3if not n:
print('0 = 0')
returnThis part checks
if n is zero
using if not n. Ifn
is zero, the function prints0 = 0
and returns immediately, since zero doesn’t need further decomposition into powers of2
.Converting the number to binary:
1
binary_n = f'{n:b}'
The
f'{n:b}'
converts the integern
to its binary form as a string (without the0b
prefix). For example, ifn = 13
,binary_n
will be1101
.Finding the positions of 1s in the binary representation:
1
powers = (i for i in range(len(binary_n)) if binary_n[i] == '1')
This list comprehension creates a list of indices where the binary digit is
1
. For1101
, thepositions
list will be[0, 2, 3]
.- This is a generator expression that finds the positions (indices) in the binary string where there is a
1
. range(len(binary_n))
iterates over each character in the binary string, and the conditionbinary_n[i] == '1'
filters out the positions where there is a1
.- For example, if
n = 13
andbinary_n = '1101'
, the positions of 1s would be(0, 2, 3)
, because the first, third, and fourth bits from the right are1
.
- This is a generator expression that finds the positions (indices) in the binary string where there is a
print(operator.join(f’2^{len(binary_n) - i - 1}’ for i in powers))
1
print(operator.join(f'2^{len(binary_n) - i - 1}' for i in powers))
- This is the key part of the function that prints the powers of 2 based on the positions of 1s in the binary representation.
f'2^{len(binary_n) - i - 1}'
creates strings like2^3
,2^2
,2^0
by converting each position i into the corresponding exponent for 2. The exponent is calculated as len(binary_n) - i - 1, where i is the position of the 1 in the binary string.operator.join(...)
joins all these terms together with either' + '
or' - '
depending on the value ofn
.
- This is the key part of the function that prints the powers of 2 based on the positions of 1s in the binary representation.
Exercise 5
To solve next problems, lets first understand the zip
function in Python.
- Let’s try:
1 | L1 = [1, 2, 3] |
Output:
1 | [(1, 4), (2, 5), (3, 6)] |
And zip
in dictionary:
1 | L1 = [1, 2, 3] |
Output:
1 | [(1, 4), (2, 5), (3, 6)] |
The finally we try using zip
with *
, which is used to unpack the elements of a list or tuple:
1 | L = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] |
Output:
1 | ([1, 2, 3],) |
Problem Description
In this problem, we can identify the pattern by observing the example: start from the first and last elements of a list and progressively make pairs towards the center of the list.
My initial thought was to use a two-pointer approach: one pointer starts at the beginning of the list, and the other starts at the end, and they move towards each other. However, I missed the specific requirement in the problem: “Return an expression built only from list, reversed, and zip.”
Wrong Solution:
1 | def f5(*L): |
Correct Solution:
1 | def f5(*L): |
The idea behind the correct solution is as follows:
zip(L, reversed(L))
creates pairs of elements where the first comes fromL
and the second comes fromreversed(L)
(i.e.,L
reversed).
Example: IfL = (1, 2, 3)
, thenreversed(L)
is(3, 2, 1)
. So,zip(L, reversed(L))
results in[(1, 3), (2, 2), (3, 1)]
.reversed(list(zip(L, reversed(L))))
: Here, we convert the result ofzip(L, reversed(L))
into a list and then reverse the entire list.
Example: Ifzip(L, reversed(L))
produced[(1, 3), (2, 2), (3, 1)]
, thenreversed(list(zip(L, reversed(L))))
will return[(3, 1), (2, 2), (1, 3)]
.zip(zip(L, reversed(L)), reversed(list(zip(L, reversed(L)))))
: This part pairs the result ofzip(L, reversed(L))
with its reversed version. It takes corresponding elements from both sequences and forms pairs.
Example: Ifzip(L, reversed(L))
is[(1, 3), (2, 2), (3, 1)]
and its reverse is[(3, 1), (2, 2), (1, 3)]
, thenzip(zip(L, reversed(L)), reversed(list(zip(L, reversed(L)))))
produces pairs of tuples like:((1, 3), (3, 1))
((2, 2), (2, 2))
((3, 1), (1, 3))
Finally, this expression is wrapped in list()
to convert the zip object into a list of tuples.
Exercise 6
Problem Description
Exercise 6 is similar to Exercise 5, requiring us to use zip
, *
, and list
to solve the problem. We are given a variable number of arguments *L
, and the goal is to output them in the form of [(...), (...)]
. This task is simpler than the previous one, as the pairing only needs to reverse and mirror the elements.
At first, I attempted the following approach:
Wrong Solution:
1 | def f6(*L): |
I tried using zip
to pair elements in forward and reverse order:
zip(L, reversed(L))
pairs the elements of L
with the reversed version of L
, while zip(reversed(L), L)
pairs them in the reverse order.
However, the output of this solution was:
[((0, 1), (1, 0)), ((1, 0), (0, 1))]
This did not meet the requirements, as the pairs (0, 1)
and (1, 0)
were grouped separately. The correct format should have 0, 1, 1, 0
in the same tuple.
Correct Solution:
1 | def f6(*L): |
Explanation of the Fix:
To achieve the desired output, we need to flatten or “unpack” the pairs created by zip(L, reversed(L))
and zip(reversed(L), L)
into individual elements and combine them correctly. This is where the *
operator comes in, which unpacks the results from the zip
function.
By using *
before zip(L, reversed(L))
and zip(reversed(L), L)
, you effectively unpack the tuples into separate elements, allowing the outer zip
function to combine them in the desired form.
*zip(L, reversed(L))
: This unpacks the paired tuples fromL
andreversed(L)
. Instead of getting pairs like(0, 1)
, it unpacks them into0, 1, 1, 0
.*zip(reversed(L), L)
: Similarly, this unpacks the reversed pairs.
The final zip
call then combines these unpacked elements into the correct tuple format. The result is:
[(0, 1, 1, 0), (1, 0, 0, 1)]
This ensures that the mirrored pairs are placed together in the same tuple as required by the problem.