Recursion is a functional heritage and so using it with functional style yields the best results. This means avoiding things like mutations, variable reassignments and other side effects. We can use inductive reasoning to solve the problem in a principled way -
- If the input,
n, has only a single bit, the base case has been reached. Return the zero streak, z, if n is 1 or return z plus 1 for the final zero
- (inductive)
n has more than one bit remaining. If the first bit is a 1, the streak is broken. Yield the current zero streak, z, and recur on the sub-problem n >> 1 with a fresh zero streak of 0
- (inductive)
n has more than one bit and the first bit is a 0. Recur on the sub-problem n >> 1 with an incremented zero streak
def consecutive_zeroes(n, z = 0):
if n < 2: # 1
yield z if n == 1 else z + 1
elif n & 1: # 2
yield z
yield from consecutive_zeroes(n >> 1, 0)
else: # 3
yield from consecutive_zeroes(n >> 1, z + 1)
Notice the generator is disentangled from the max logic. The built-in max function already accepts an iterable input, so there's little sense in duplicating that.
for x in range(20):
print(f"{x:b}", max(consecutive_zeroes(x)))
0 1
1 0
10 1
11 0
100 2
101 1
110 1
111 0
1000 3
1001 2
1010 1
1011 1
1100 2
1101 1
1110 1
1111 0
10000 4
10001 3
10010 2
10011 2
for x in range(60, 80):
print(f"{x:b}", max(consecutive_zeroes(x)))
111100 2
111101 1
111110 1
111111 0
1000000 6
1000001 5
1000010 4
1000011 4
1000100 3
1000101 3
1000110 3
1000111 3
1001000 3
1001001 2
1001010 2
1001011 2
1001100 2
1001101 2
1001110 2
1001111 2
for x in range(500, 520):
print(f"{x:b}", max(consecutive_zeroes(x)))
111110100 2
111110101 1
111110110 1
111110111 1
111111000 3
111111001 2
111111010 1
111111011 1
111111100 2
111111101 1
111111110 1
111111111 0
1000000000 9
1000000001 8
1000000010 7
1000000011 7
1000000100 6
1000000101 6
1000000110 6
1000000111 6
prevent leaked parameters
If you don't want extra parameters like z or expecting caller to require max, we can embed a nested helper, loop, in our function -
def consecutive_zeroes(n): # <- new wrapper, with no leaked parameters
def loop(n, z): # <- our function above, renamed to loop
if n < 2:
yield z if n == 1 else z + 1
elif n & 1:
yield z
yield from loop(n >> 1, 0)
else:
yield from loop(n >> 1, z + 1)
return max(loop(n, 0)) # <- max
Now when you call it, max is no longer required by the caller -
for x in range(20):
print(f"{x:b}", consecutive_zeroes(x)) # <- no need to call max
# ...
related Q&A
Nan inputnumber? Ismaxmthemaximum_count, andcntthecurrent_count? In how far have you "tried in many ways but the result is wrong."?