Find the target difference in a pair with recursion

Viewed 299

Given a list of unsorted integers and a target integer, find out if any pair's difference in the list is equal to the target integer with recursion.

>>> aList = [5, 4, 8, -3, 6]
>>> target = 9
return True

>>> aList = [-1, 5, 4]
>>> target = 3
return False
  • For and while loops are not allowed.
  • No imports allowed.
  • .sort() is not allowed.

I tried this and it didn't work.

def calculate(aList, target):

    if len(aList) == 0 and diff != 0:
        return False

    startIndex = 0
    endIndex = len(aList) - 1

    return resursive_sum(aList, target, startIndex, endIndex)

def resursive_sum(aList, targ, start, end):

    print(f'Start: {start}')
    print(f'End: {end}')

    if start == end:
        return False
    elif aList[end] - aList[start] == targ:
        return True
    elif aList[end] - aList[start] < targ:
        return resursive_sum(values, targ, start, end - 1)
    return resursive_sum(aList, targ, start + 1, end)

I'm unsure of how this problem could be solved if we aren't able to use loops to sort the list. Even if we could use recursion to sort the list, how should the recursion look so that it can scan every pair's difference?

6 Answers

So I actually implemented it, but for educational purposes I'm not gonna post it until a bit later (I'll update it in a few hours) as I assume this is for a class or some other setting where you should figure it out on your own.

Assume you are trying to hit a difference target t = 5 and you are evaluating an arbitrary element 8. There are only two values that would allow 8 to have a complement in the set: 8 + 5 = 13 and 8 - 5 = 3.

If 3 or 13 had been in any previous elements, you would know that the set has a pair of complements. Otherwise, you'd want to record the fact that 8 had been seen. Thereby, if 3 was found later, 8 would be queried as 3 + 5 = 8 would be considered.

In other words, I am proposing a method where you recursively traverse the list and either

  • (base case) Are at the end of the list
  • Have a current element a such that a + t or a - t has been seen
  • Record that the current element has been seen and go to the next element

Ideally, this should have O(n) time complexity and O(n) space complexity in the worst case (assuming efficient implementation with pass-by-reference or similar, and also amortized constant-time set query). It can also be implemented using a basic array, but I'm not going to say that's better (in python).

I'll post my solution in a few hours. Good luck!


EDIT 1: Hopefully, you had enough time to get it to work. The method I described can be done as follows:

def hasDiffRecur(L, t, i, C):
    """
    Recursive version to see if list has difference
    :param L: List to be considered
    :param t: Target difference
    :param i: Current index to consider
    :param C: Cache set
    """
    # We've reached the end. Give up
    if i >= len(L): 
        return False
    
    print(f" > L[{i}] = {L[i]:2}; is {L[i]-t:3} or {L[i]+t:2} in {C}")
    
    # Has the complement been cached?
    if L[i] - t in C: 
        print(f"! Difference between {L[i]} and {L[i]-t} is {t}")
        return True
    
    if L[i] + t in C: 
        print(f"! Difference between {L[i]} and {L[i]+t} is {t}")
        return True
    
    # Complement not seen yet. Cache element and go to next element
    C.add(L[i])
    return hasDiffRecur(L, t, i+1, C)

###################################################################

def hasDiff(L, t):
    """
    Initialized call for hasDiffRecur. Also prints intro message. 
    See hasDiffRecur for param info
    """
    print(f"\nIs a difference of {t} present in {L}?")
    return hasDiffRecur(L, t, 0, set())

###################################################################

hasDiff([5, 4, 8, -3, 6], 9)
hasDiff([-1, 5, 4], 3)
hasDiff([-1, 5, 4, -1, 7], 0) # If concerned about set non-duplicity 

OUTPUT:

Is a difference of 9 present in [5, 4, 8, -3, 6]?
 > L[0] =  5; is  -4 or 14 in set()
 > L[1] =  4; is  -5 or 13 in {5}
 > L[2] =  8; is  -1 or 17 in {4, 5}
 > L[3] = -3; is -12 or  6 in {8, 4, 5}
 > L[4] =  6; is  -3 or 15 in {8, -3, 4, 5}
! Difference between 6 and -3 is 9

Is a difference of 3 present in [-1, 5, 4]?
 > L[0] = -1; is  -4 or  2 in set()
 > L[1] =  5; is   2 or  8 in {-1}
 > L[2] =  4; is   1 or  7 in {5, -1}

Is a difference of 0 present in [-1, 5, 4, -1, 7]?
 > L[0] = -1; is  -1 or -1 in set()
 > L[1] =  5; is   5 or  5 in {-1}
 > L[2] =  4; is   4 or  4 in {5, -1}
 > L[3] = -1; is  -1 or -1 in {4, 5, -1}
! Difference between -1 and -1 is 0

EDIT 2:

This is a pretty clever and efficient solution. I do realize that maybe it is the intention to not allow any traversal at all (i.e. no existance querying for set). If that is the case, the above approach can be done with a constant-size list that is pre-allocated to size equal to the range of the values of the list.

If the notion of pre-allocating to the size of the range of the list is still too much iteration, I can think of the exhaustive approach implemented recursively. There is likely a more efficient approach for this, but you could boil the problem down to a double-for-loop-like problem (O(n^2) time complexity). This is a trivial algorithm and I think you can understand it without documentation, so I'll just throw it in there to be complete:

def hasDiffRecur(L, t, i = 0, j = 1):
    if i >= len(L): return False
    if j >= len(L): return hasDiffRecur(L, t, i+1, i+2)
    if abs(L[i] - L[j]) == t: return True
    return hasDiffRecur(L, t, i, j+1)

###################################################################

print(hasDiffRecur([5, 4, 8, -3, 6], 9))  # True
print(hasDiffRecur([-1, 5, 4], 3))        # False
print(hasDiffRecur([-1, 5, 4, -1, 7], 0)) # True

choose

I'll start with a generic function that takes a list, t, and a number of elements to choose, n -

def choose(t, n):
  if n == 0:
    return [[]]
  elif not t:
    return []
  else:
    return append \
      ( map \
          ( choose(rest(t), n - 1)
          , lambda c: append([first(t)], c)
          )
      , choose(rest(t), n)
      )
print(choose(["a", "b", "c", "d"], 2))
[['a', 'b'], ['a', 'c'], ['a', 'd'], ['b', 'c'], ['b', 'd'], ['c', 'd']]

helpers

Your question imposes quite a few restrictions and Python is a multi-paradigm language and so we're going to use a number of helpers to make things readable

def first(t):
  return t[0]

def rest(t):
  return t[1:]

def append(t0, t1):
  return t0 + t1

I don't know if map counts as an import, but we will define our own just in case -

def map(t, f):
  if not t:
    return []
  else:
    return append \
      ( [f(first(t))]
      , map(rest(t), f)
      )

solve

Great, now that we've finished implementing choose, let's see how we can apply it to our problem

print(choose([5, 4, 8, -3, 6], 2))
[[5, 4], [5, 8], [5, -3], [5, 6], [4, 8], [4, -3], [4, 6], [8, -3], [8, 6], [-3, 6]]

As you can see, we've found all combinations of 2 elements. We just need to loop through these and check if a pair can be subtracted to reach our target, q -

def solve(t, q):
  def check(p):
    (x, y) = p
    return x - y == q or y - x == q
  def loop(c):
    if not c:
      return False
    else:
      return check(first(c)) or loop(rest(c))    
  return loop(choose(t, 2))
print(solve([5, 4, 8, -3, 6], 9))
print(solve([-1, 5, 4], 3))
True
False

allowing for

This is a great exercise to build your recursion skills. Disallowing for is the most challenging restriction to overcome. Here's what it could look like if we could use it -

def choose(t, n):
  if n == 0:
    yield []
  elif not t:
    return
  else:
    for c in choose(t[1:], n - 1):
      yield [t[0]] + c
    yield from choose(t[1:], n)

def solve(t, q):
  for (x,y) in choose(t, 2):
    if x - y == q or y - x == q:
      return True
  return False
print(solve([5, 4, 8, -3, 6], 9))
print(solve([-1, 5, 4], 3))
True
False

This variant has an added advantage that it will stop computing combinations as soon as a solution is found. The first variant must compute all combinations first and then begin iterating through them.

allowing other built-ins

Python built-in functions include map and any and offer us another way to get around the for restriction, but I'm unsure if those are allowed -

def choose(t, n):
  if n == 0:
    yield []
  elif not t:
    return
  else:
    yield from map \
      ( lambda c: [t[0]] + c
      , choose(t[1:], n - 1)
      )
    yield from choose(t[1:], n)

def solve(t, q):
  def check(p):
    (x,y) = p
    return x - y == q or y - x == q
  return any(map(check, choose(t, 2)))
print(solve([5, 4, 8, -3, 6], 9))
print(solve([-1, 5, 4], 3))
True
False

Problem:

  • given an array of int aList and int target,
  • check if the difference between each element in aList equals to target
  • use recursion
  • do not use .sort()
  • do not use while and for
  • do not use import

Example:

>>> aList = [5, 4, 8, -3, 6]
>>> target = 9
return True

>>> aList = [-1, 5, 4]
>>> target = 3
return False

Comparing the differences:

      5  4  8  -3 6
-------------------------
5  |  X
4  |  1  X
8  |  3  4  X
-3 |  6  7  11 X
6  |  1  2  2  9  X

where X means that there's no difference (same number)
since we find 9 there, so it should return True (target is 9)

Traditional for loops

To solve recursion problem, first try to solve it with traditional for loops:

def compareAll(lst, tgt):
  for x in lst: # let's call this x loop
    for y in lst: # let's call this y loop
      if abs(x-y) == tgt:
        return True
  return False
print( compareAll([5,4,8,-3,6],9) )
print( compareAll([-1,5,4],3) )

This returns True then False

Recursion

Now we can try using recursion loop. Since we already got the for loop, we can convert it like this:

def compareAll(lst, tgt, x=0, y=0):
  if(len(lst)-1 == x and len(lst) == y):
    return False
  if(len(lst) == x or len(lst) == y):
    return compareAll(lst, tgt, x+1, 0)
  if(abs(lst[x] - lst[y])==tgt):
    return True
  return compareAll(lst, tgt, x, y+1)
print( compareAll([5,4,8,-3,6],9) )
print( compareAll([-1,5,4],3) )

How I convert for loop into this:

  • python's for loop is actually foreach loop in most other languages
  • so, pure for loop in python will be like:
def compareAll(lst, tgt):
  x = 0
  while x < len(lst): # let's call this x loop
    y = 0
    while y < len(lst): # let's call this y loop
      if abs(lst[x]-lst[y]) == tgt:
        return True
      y = y+1
    x = x+1
  return False
print( compareAll([5,4,8,-3,6],9) )
print( compareAll([-1,5,4],3) )
  • notice the stopping condition of x loop: when all the array element have been looped
    • so we add stopping condition here: if(len(lst)-1 == x and len(lst) == y): return False
  • notice the stopping condition of y loop: when all the array element have been looped
    • so we add stopping condition here: if(len(lst) == x or len(lst) == y): return compareAll(lst, tgt, x+1, 0)
    • this stops the current y loop and continue with the x loop
  • then, we add the actual content of the loop: if(abs(lst[x] - lst[y])==tgt): return True
  • last, we have to continue the loop: return compareAll(lst, tgt, x, y+1)

The key to convert for loop into recursive loop is just to identify when the loop should end, and when the loop should continue.

This should work and is quite concise:

def q(target, aList, memo=set()):
    if len(aList) == 0:
        return False
    num = aList.pop()
    memo.add(num)    
    if target + num in memo:
        return True
    return q(target, aList, memo)


q(target=9, aList=[5, 4, 8, -3, 6]) # True    
q(target=3, aList=[-1,5,4]) # False

The key insight for me is that a target t and a given number n, the difference d is known. Dict/set/hashmaps are fast at detecting membership, regardless of how many items are added. So... just pop through the list of values and chuck them into a hashmap for later comparison.

The problem can be solved by checking every possible pair of numbers in the list for a solution. If you are allowed to use Python's standard library, then the solution is pretty straight forward.

from itertools import product

def check(xs, target):
    return any(map(lambda x: x[0]-x[1] == target, product(xs, xs)))

Breakdown

  • product(xs, xs) gives the cross product of xs with itself
  • any(iterable) returns true if any element of iterable is truthy
  • map(function, iterable) lazily (applies function to every element of iterable)
  • lambda arg_tuple: expression annonymous function with arguments arg_tuple and returns the result of expression

The return statement in check uses lazy structures so it only does as much work as is needed, and is space efficient.

Assuming that this is just an exercise in recursion, it probably doesn't preclude a "brute force" approach. You can use the recursion to pair up every value with the remaining ones until you find a matching difference.

For example:

def hasDiff(L,diff,base=None):
    if not L: return False    # empty list: no match
    if base is None:          # search with & without first as base      
        return hasDiff(L[1:],diff,L[0]) or hasDiff(L[1:],diff)
    return abs(base-L[0]) == diff or hasDiff(L[1:],diff,base) # match or recurse
    
print(hasDiff([5, 4, 8, -3, 6],9)) # True
print(hasDiff([-1, 5, 4],3))       # False

When the function recurses with a base value, it merely checks the first item in the remainder of the list and recurses for the other values. When the function recurses without a base value, it tries to find new pairs that don't involve the first item (i.e. in the remainder of the list)

Related