简体   繁体   中英

Turning a while loop to a recursion

I'm trying to turn this code ( that gives the maximum number of consecutive 0's in a binary) to a recursive code. I tried in many ways but the result is wrong. This is the code using a while loop:

def consecutivezeros(N):
    if N==0 :
        return 1
    # variable to store the length
    # of longest consecutive 0's
    maxm = -1

    # to temporary store the
    # consecutive 0's
    cnt = 0
    while (N):
        if (not (N & 1)):
            cnt += 1
            N >>= 1
            maxm = max(maxm, cnt)
        else:
            maxm = max(maxm, cnt)
            cnt = 0
            N >>= 1

    return maxm

Here is the principled way:

def consecutivezeros(N):
    if N == 0:
        return 1
    def _consecutivezeros(N, maxm, cnt):
        if not N:
            return maxm
        elif (not (N & 1)):
            new_cnt = cnt + 1
            return _consecutivezeros(N>>1, max(maxm, new_cnt), new_cnt)
        else:
            return _consecutivezeros(N>>1, max(maxm, cnt), 0)
    return _consecutivezeros(N, -1, 0)

Here is the dumb, unprincipled way, basically just use a recursive helper function that mutates variables in it's closure, this is just to demonstrate how recursion can basically replace a while-loop directly, but it's not very elegant:

def consecutivezeros(N):
    if N == 0:
        return 1
    maxm = -1
    cnt = 0
    def _looper():
        nonlocal maxm, cnt, N
        if not N:
            return
        if (not (N & 1)):
            cnt += 1
            maxm = max(maxm, cnt)
        else:
            maxm = max(maxm, cnt)
            cnt = 0
        N >>= 1
    _looper(N)
    return maxm

EDIT:

If you don't want to use an auxilliary helper function, you need additional arguments:

def consecutivezeros(N, _maxm=-1, _cnt=0, _first=True):
    if N == 0 and _first:
        return 1
    elif not N:
        return _maxm
    elif (not (N & 1)):
        incr = _cnt + 1
        return consecutivezeros(N>>1, max(_maxm, incr), incr, False)
    else:
        return consecutivezeros(N>>1, max(_maxm, _cnt), 0, False)
def rec_count(s: str, c: int = 0, m: int = 0):
    if s:
        if s[0] == "0":
            if (c := c+1) > m:
                m = c

            return rec_count(s[1:], m=m, c=c)
        else:
            return rec_count(s[1:], m=m)
    else:
        return m


N = 100000100101
print(rec_count(s=str(N)))
# 5

EDIT: According to comment, input is int instead of binary or str . Changed solution accordingly.

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 -

  1. 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
  2. (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
  3. (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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM